diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
commit | 975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch) | |
tree | 89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/dellemc/powerflex/plugins | |
parent | Initial commit. (diff) | |
download | ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip |
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/dellemc/powerflex/plugins')
15 files changed, 12340 insertions, 0 deletions
diff --git a/ansible_collections/dellemc/powerflex/plugins/doc_fragments/powerflex.py b/ansible_collections/dellemc/powerflex/plugins/doc_fragments/powerflex.py new file mode 100644 index 000000000..349680345 --- /dev/null +++ b/ansible_collections/dellemc/powerflex/plugins/doc_fragments/powerflex.py @@ -0,0 +1,61 @@ +# Copyright: (c) 2020, Dell Technologies. +# Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class ModuleDocFragment(object): + # Documentation fragment for PowerFlex + DOCUMENTATION = r''' + options: + hostname: + required: true + description: + - IP or FQDN of the PowerFlex host. + type: str + aliases: + - gateway_host + username: + type: str + required: true + description: + - The username of the PowerFlex host. + password: + type: str + required: true + description: + - The password of the PowerFlex host. + validate_certs: + type: bool + default: true + aliases: + - verifycert + description: + - Boolean variable to specify whether or not to validate SSL + certificate. + - C(true) - Indicates that the SSL certificate should be verified. + - C(false) - Indicates that the SSL certificate should not be + verified. + port: + description: + - Port number through which communication happens with PowerFlex + host. + type: int + default: 443 + timeout: + description: + - Time after which connection will get terminated. + - It is to be mentioned in seconds. + type: int + required: False + default: 120 + requirements: + - A Dell PowerFlex storage system version 3.5 or later. + - Ansible-core 2.12 or later. + - PyPowerFlex 1.6.0. + - Python 3.9, 3.10 or 3.11. + notes: + - The modules present in the collection named as 'dellemc.powerflex' + are built to support the Dell PowerFlex storage platform. +''' diff --git a/ansible_collections/dellemc/powerflex/plugins/module_utils/storage/dell/__init__.py b/ansible_collections/dellemc/powerflex/plugins/module_utils/storage/dell/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/powerflex/plugins/module_utils/storage/dell/__init__.py diff --git a/ansible_collections/dellemc/powerflex/plugins/module_utils/storage/dell/logging_handler.py b/ansible_collections/dellemc/powerflex/plugins/module_utils/storage/dell/logging_handler.py new file mode 100644 index 000000000..7436cbb12 --- /dev/null +++ b/ansible_collections/dellemc/powerflex/plugins/module_utils/storage/dell/logging_handler.py @@ -0,0 +1,24 @@ +# Copyright: (c) 2022, Dell Technologies +# Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt) + +"""Custom rotating file handler for PowerFlex""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from datetime import datetime +from logging.handlers import RotatingFileHandler + + +class CustomRotatingFileHandler(RotatingFileHandler): + def rotation_filename(self, default_name): + """ + Modify the filename of a log file when rotating. + :param default_name: The default name of the log file. + """ + src_file_name = default_name.split('.') + dest_file_name = "{0}_{1}.{2}.{3}".format( + src_file_name[0], '{0:%Y%m%d}'.format(datetime.now()), + src_file_name[1], src_file_name[2] + ) + return dest_file_name diff --git a/ansible_collections/dellemc/powerflex/plugins/module_utils/storage/dell/utils.py b/ansible_collections/dellemc/powerflex/plugins/module_utils/storage/dell/utils.py new file mode 100644 index 000000000..8503aeb0c --- /dev/null +++ b/ansible_collections/dellemc/powerflex/plugins/module_utils/storage/dell/utils.py @@ -0,0 +1,186 @@ +# Copyright: (c) 2021, Dell Technologies +# Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import logging +import math +import re +from decimal import Decimal +from ansible_collections.dellemc.powerflex.plugins.module_utils.storage.dell.logging_handler \ + import CustomRotatingFileHandler +import traceback +from ansible.module_utils.basic import missing_required_lib + +"""import PyPowerFlex lib""" +try: + from PyPowerFlex import PowerFlexClient + from PyPowerFlex.objects.sds import Sds + from PyPowerFlex.objects import protection_domain + from PyPowerFlex.objects import storage_pool + from PyPowerFlex.objects import sdc + from PyPowerFlex.objects import volume + from PyPowerFlex.objects import system + from PyPowerFlex.objects.system import SnapshotDef + + HAS_POWERFLEX_SDK, POWERFLEX_SDK_IMP_ERR = True, None +except ImportError: + HAS_POWERFLEX_SDK, POWERFLEX_SDK_IMP_ERR = False, traceback.format_exc() + +"""importing pkg_resources""" +try: + from pkg_resources import parse_version + import pkg_resources + + PKG_RSRC_IMPORTED, PKG_RSRC_IMP_ERR = True, None +except ImportError: + PKG_RSRC_IMPORTED, PKG_RSRC_IMP_ERR = False, traceback.format_exc() + +"""importing dateutil""" +try: + import dateutil.relativedelta + HAS_DATEUTIL, DATEUTIL_IMP_ERR = True, None +except ImportError: + HAS_DATEUTIL, DATEUTIL_IMP_ERR = False, traceback.format_exc() + + +def get_powerflex_gateway_host_parameters(): + """Provides common access parameters required for the + ansible modules on PowerFlex Storage System""" + + return dict( + hostname=dict(type='str', aliases=['gateway_host'], required=True), + username=dict(type='str', required=True), + password=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', aliases=['verifycert'], required=False, default=True), + port=dict(type='int', required=False, default=443), + timeout=dict(type='int', required=False, default=120) + ) + + +def get_powerflex_gateway_host_connection(module_params): + """Establishes connection with PowerFlex storage system""" + + if HAS_POWERFLEX_SDK: + conn = PowerFlexClient( + gateway_address=module_params['hostname'], + gateway_port=module_params['port'], + verify_certificate=module_params['validate_certs'], + username=module_params['username'], + password=module_params['password'], + timeout=module_params['timeout']) + conn.initialize() + return conn + + +def ensure_required_libs(module): + """Check required libraries""" + + if not HAS_DATEUTIL: + module.fail_json(msg=missing_required_lib("python-dateutil"), + exception=DATEUTIL_IMP_ERR) + + if not PKG_RSRC_IMPORTED: + module.fail_json(msg=missing_required_lib("pkg_resources"), + exception=PKG_RSRC_IMP_ERR) + + if not HAS_POWERFLEX_SDK: + module.fail_json(msg=missing_required_lib("PyPowerFlex V 1.6.0 or above"), + exception=POWERFLEX_SDK_IMP_ERR) + + min_ver = '1.6.0' + try: + curr_version = pkg_resources.require("PyPowerFlex")[0].version + supported_version = (parse_version(curr_version) >= parse_version(min_ver)) + if not supported_version: + module.fail_json(msg="PyPowerFlex {0} is not supported. " + "Required minimum version is " + "{1}".format(curr_version, min_ver)) + except Exception as e: + module.fail_json(msg="Getting PyPowerFlex SDK version, failed with " + "Error {0}".format(str(e))) + + +def get_logger(module_name, log_file_name='ansible_powerflex.log', log_devel=logging.INFO): + """ + Initialize logger and return the logger object. + :param module_name: Name of module to be part of log message + :param log_file_name: Name of file in which the log messages get appended + :param log_devel: Log level + :return LOG object + """ + FORMAT = '%(asctime)-15s %(filename)s %(levelname)s : %(message)s' + max_bytes = 5 * 1024 * 1024 + logging.basicConfig(filename=log_file_name, format=FORMAT) + LOG = logging.getLogger(module_name) + LOG.setLevel(log_devel) + handler = CustomRotatingFileHandler(log_file_name, maxBytes=max_bytes, backupCount=5) + formatter = logging.Formatter(FORMAT) + handler.setFormatter(formatter) + LOG.addHandler(handler) + LOG.propagate = False + return LOG + + +KB_IN_BYTES = 1024 +MB_IN_BYTES = 1024 * 1024 +GB_IN_BYTES = 1024 * 1024 * 1024 +TB_IN_BYTES = 1024 * 1024 * 1024 * 1024 + + +def get_size_bytes(size, cap_units): + """Convert the given size to bytes""" + + if size is not None and size > 0: + if cap_units in ('kb', 'KB'): + return size * KB_IN_BYTES + elif cap_units in ('mb', 'MB'): + return size * MB_IN_BYTES + elif cap_units in ('gb', 'GB'): + return size * GB_IN_BYTES + elif cap_units in ('tb', 'TB'): + return size * TB_IN_BYTES + else: + return size + else: + return 0 + + +def convert_size_with_unit(size_bytes): + """Convert size in byte with actual unit like KB,MB,GB,TB,PB etc.""" + + if not isinstance(size_bytes, int): + raise ValueError('This method takes Integer type argument only') + if size_bytes == 0: + return "0B" + size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") + i = int(math.floor(math.log(size_bytes, 1024))) + p = math.pow(1024, i) + s = round(size_bytes / p, 2) + return "%s %s" % (s, size_name[i]) + + +def get_size_in_gb(size, cap_units): + """Convert the given size to size in GB, size is restricted to 2 decimal places""" + + size_in_bytes = get_size_bytes(size, cap_units) + size = Decimal(size_in_bytes / GB_IN_BYTES) + size_in_gb = round(size) + return size_in_gb + + +def is_version_less_than_3_6(version): + """Verifies if powerflex version is less than 3.6""" + version = re.search(r'R\s*([\d.]+)', version.replace('_', '.')).group(1) + return \ + pkg_resources.parse_version(version) < pkg_resources.parse_version('3.6') + + +def is_invalid_name(name): + """Validates string against regex pattern""" + if name is not None: + regexp = re.compile(r'^[a-zA-Z0-9!@#$%^~*_-]*$') + if not regexp.search(name): + return True diff --git a/ansible_collections/dellemc/powerflex/plugins/modules/device.py b/ansible_collections/dellemc/powerflex/plugins/modules/device.py new file mode 100644 index 000000000..a321315e3 --- /dev/null +++ b/ansible_collections/dellemc/powerflex/plugins/modules/device.py @@ -0,0 +1,1105 @@ +#!/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 device on Dell Technologies (Dell) PowerFlex""" + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +DOCUMENTATION = r''' +module: device +version_added: '1.1.0' +short_description: Manage device on Dell PowerFlex +description: +- Managing device on PowerFlex storage system includes + adding new device, getting details of device, and removing a device. +author: +- Rajshree Khare (@khareRajshree) <ansible.team@dell.com> +extends_documentation_fragment: + - dellemc.powerflex.powerflex +options: + current_pathname: + description: + - Full path of the device to be added. + - Required while adding a device. + type: str + device_name: + description: + - Device name. + - Mutually exclusive with I(device_id). + type: str + device_id: + description: + - Device ID. + - Mutually exclusive with I(device_name). + type: str + sds_name: + description: + - The name of the SDS. + - Required while adding a device. + - Mutually exclusive with I(sds_id). + type: str + sds_id: + description: + - The ID of the SDS. + - Required while adding a device. + - Mutually exclusive with I(sds_name). + type: str + storage_pool_name: + description: + - Storage Pool name. + - Used while adding a storage device. + - Mutually exclusive with I(storage_pool_id), I(acceleration_pool_id) and + I(acceleration_pool_name). + type: str + storage_pool_id: + description: + - Storage Pool ID. + - Used while adding a storage device. + - Media type supported are C(SSD) and C(HDD). + - Mutually exclusive with I(storage_pool_name), I(acceleration_pool_id) and + I(acceleration_pool_name). + type: str + acceleration_pool_name: + description: + - Acceleration Pool Name. + - Used while adding an acceleration device. + - Media type supported are C(SSD) and C(NVDIMM). + - Mutually exclusive with I(storage_pool_id), I(storage_pool_name) and + I(acceleration_pool_name). + type: str + acceleration_pool_id: + description: + - Acceleration Pool ID. + - Used while adding an acceleration device. + - Media type supported are C(SSD) and C(NVDIMM). + - Mutually exclusive with I(acceleration_pool_name), I(storage_pool_name) and + I(storage_pool_id). + type: str + protection_domain_name: + description: + - Protection domain name. + - Used while identifying a storage pool along with I(storage_pool_name). + - Mutually exclusive with I(protection_domain_id). + type: str + protection_domain_id: + description: + - Protection domain ID. + - Used while identifying a storage pool along with I(storage_pool_name). + - Mutually exclusive with I(protection_domain_name). + type: str + external_acceleration_type: + description: + - Device external acceleration types. + - Used while adding a device. + type: str + choices: ['Invalid', 'None', 'Read', 'Write', 'ReadAndWrite'] + media_type: + description: + - Device media types. + - Required while adding a device. + type: str + choices: ['HDD', 'SSD', 'NVDIMM'] + state: + description: + - State of the device. + choices: ['present', 'absent'] + required: true + type: str +notes: + - The value for device_id is generated only after successful addition of the + device. + - To uniquely identify a device, either I(device_id) can be passed or one of + I(current_pathname) or I(device_name) must be passed with I(sds_id) or I(sds_name). + - It is recommended to install Rfcache driver for SSD device on SDS in + order to add it to an acceleration pool. + - The I(check_mode) is not supported. +''' + +EXAMPLES = r''' +- name: Add a device + dellemc.powerflex.device: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + current_pathname: "/dev/sdb" + sds_name: "node1" + media_type: "HDD" + device_name: "device2" + storage_pool_name: "pool1" + protection_domain_name: "domain1" + external_acceleration_type: "ReadAndWrite" + state: "present" +- name: Get device details using device_id + dellemc.powerflex.device: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + device_id: "d7fe088900000000" + state: "present" +- name: Get device details using (current_pathname, sds_name) + dellemc.powerflex.device: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + current_pathname: "/dev/sdb" + sds_name: "node0" + state: "present" +- name: Get device details using (current_pathname, sds_id) + dellemc.powerflex.device: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + current_pathname: "/dev/sdb" + sds_id: "5717d71800000000" + state: "present" +- name: Remove a device using device_id + dellemc.powerflex.device: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + device_id: "76eb7e2f00010000" + state: "absent" +- name: Remove a device using (current_pathname, sds_id) + dellemc.powerflex.device: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + current_pathname: "/dev/sdb" + sds_name: "node1" + state: "absent" +''' + +RETURN = r''' +changed: + description: Whether or not the resource has changed. + returned: always + type: bool + sample: 'false' +device_details: + description: Details of the device. + returned: When device exists + type: dict + contains: + accelerationPoolId: + description: Acceleration pool ID. + type: str + accelerationPoolName: + description: Acceleration pool name. + type: str + accelerationProps: + description: Indicates acceleration props. + type: str + aggregatedState: + description: Indicates aggregated state. + type: str + ataSecurityActive: + description: Indicates ATA security active state. + type: bool + autoDetectMediaType: + description: Indicates auto detection of media type. + type: str + cacheLookAheadActive: + description: Indicates cache look ahead active state. + type: bool + capacity: + description: Device capacity. + type: int + capacityLimitInKb: + description: Device capacity limit in KB. + type: int + deviceCurrentPathName: + description: Device current path name. + type: str + deviceOriginalPathName: + description: Device original path name. + type: str + deviceState: + description: Indicates device state. + type: str + deviceType: + description: Indicates device type. + type: str + errorState: + description: Indicates error state. + type: str + externalAccelerationType: + description: Indicates external acceleration type. + type: str + fglNvdimmMetadataAmortizationX100: + description: Indicates FGL NVDIMM meta data amortization value. + type: int + fglNvdimmWriteCacheSize: + description: Indicates FGL NVDIMM write cache size. + type: int + firmwareVersion: + description: Indicates firmware version. + type: str + id: + description: Device ID. + type: str + ledSetting: + description: Indicates LED setting. + type: str + links: + description: Device links. + type: list + contains: + href: + description: Device instance URL. + type: str + rel: + description: Relationship of device with different + entities. + type: str + logicalSectorSizeInBytes: + description: Logical sector size in bytes. + type: int + longSuccessfulIos: + description: Indicates long successful IOs. + type: list + maxCapacityInKb: + description: Maximum device capacity limit in KB. + type: int + mediaFailing: + description: Indicates media failing. + type: bool + mediaType: + description: Indicates media type. + type: str + modelName: + description: Indicates model name. + type: str + name: + description: Device name. + type: str + persistentChecksumState: + description: Indicates persistent checksum state. + type: str + physicalSectorSizeInBytes: + description: Physical sector size in bytes. + type: int + protectionDomainId: + description: Protection domain ID. + type: str + protectionDomainName: + description: Protection domain name. + type: str + raidControllerSerialNumber: + description: RAID controller serial number. + type: str + rfcacheErrorDeviceDoesNotExist: + description: Indicates RF cache error device does not exist. + type: bool + rfcacheProps: + description: RF cache props. + type: str + sdsId: + description: SDS ID. + type: str + sdsName: + description: SDS name. + type: str + serialNumber: + description: Indicates Serial number. + type: str + spSdsId: + description: Indicates SPs SDS ID. + type: str + ssdEndOfLifeState: + description: Indicates SSD end of life state. + type: str + storagePoolId: + description: Storage Pool ID. + type: str + storagePoolName: + description: Storage Pool name. + type: str + storageProps: + description: Storage props. + type: list + temperatureState: + description: Indicates temperature state. + type: str + vendorName: + description: Indicates vendor name. + type: str + writeCacheActive: + description: Indicates write cache active. + type: bool + sample: { + "accelerationPoolId": null, + "accelerationProps": null, + "aggregatedState": "NeverFailed", + "ataSecurityActive": false, + "autoDetectMediaType": "SSD", + "cacheLookAheadActive": false, + "capacity": 0, + "capacityLimitInKb": 365772800, + "deviceCurrentPathName": "/dev/sdb", + "deviceOriginalPathName": "/dev/sdb", + "deviceState": "Normal", + "deviceType": "Unknown", + "errorState": "None", + "externalAccelerationType": "None", + "fglNvdimmMetadataAmortizationX100": 150, + "fglNvdimmWriteCacheSize": 16, + "firmwareVersion": null, + "id": "b6efa59900000000", + "ledSetting": "Off", + "links": [ + { + "href": "/api/instances/Device::b6efa59900000000", + "rel": "self" + }, + { + "href": "/api/instances/Device::b6efa59900000000/relationships + /Statistics", + "rel": "/api/Device/relationship/Statistics" + }, + { + "href": "/api/instances/Sds::8f3bb0ce00000000", + "rel": "/api/parent/relationship/sdsId" + }, + { + "href": "/api/instances/StoragePool::e0d8f6c900000000", + "rel": "/api/parent/relationship/storagePoolId" + }, + { + "href": "/api/instances/SpSds::fedf6f2000000000", + "rel": "/api/parent/relationship/spSdsId" + } + ], + "logicalSectorSizeInBytes": 0, + "longSuccessfulIos": { + "longWindow": null, + "mediumWindow": null, + "shortWindow": null + }, + "maxCapacityInKb": 365772800, + "mediaFailing": false, + "mediaType": "HDD", + "modelName": null, + "name": "device230", + "persistentChecksumState": "Protected", + "physicalSectorSizeInBytes": 0, + "protectionDomainId": "9300c1f900000000", + "protectionDomainName": "domain1", + "raidControllerSerialNumber": null, + "rfcacheErrorDeviceDoesNotExist": false, + "rfcacheProps": null, + "sdsId": "8f3bb0ce00000000", + "sdsName": "node1", + "serialNumber": null, + "slotNumber": null, + "spSdsId": "fedf6f2000000000", + "ssdEndOfLifeState": "NeverFailed", + "storagePoolId": "e0d8f6c900000000", + "storagePoolName": "pool1", + "storageProps": { + "destFglAccDeviceId": null, + "destFglNvdimmSizeMb": 0, + "fglAccDeviceId": null, + "fglNvdimmSizeMb": 0 + }, + "temperatureState": "NeverFailed", + "vendorName": null, + "writeCacheActive": false + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.dellemc.powerflex.plugins.module_utils.storage.dell\ + import utils + +LOG = utils.get_logger('device') + + +class PowerFlexDevice(object): + """Class with device 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_device_parameters()) + + mut_ex_args = [['sds_name', 'sds_id'], + ['device_name', 'device_id'], + ['protection_domain_name', + 'protection_domain_id'], + ['storage_pool_name', 'storage_pool_id'], + ['acceleration_pool_name', 'acceleration_pool_id'], + ['acceleration_pool_id', 'storage_pool_id'], + ['acceleration_pool_name', 'storage_pool_name'], + ['device_id', 'sds_name'], + ['device_id', 'sds_id'], + ['device_id', 'current_pathname']] + + # initialize the Ansible module + self.module = AnsibleModule( + argument_spec=self.module_params, + supports_check_mode=False, + mutually_exclusive=mut_ex_args) + + 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_device_details(self, current_pathname=None, sds_id=None, + device_name=None, device_id=None): + """Get device details + :param current_pathname: Device path name + :type current_pathname: str + :param sds_id: ID of the SDS + :type sds_id: str + :param device_name: Name of the device + :type device_name: str + :param device_id: ID of the device + :type device_id: str + :return: Details of device if it exist + :rtype: dict + """ + + try: + if current_pathname and sds_id: + device_details = self.powerflex_conn.device.get( + filter_fields={'deviceCurrentPathName': current_pathname, + 'sdsId': sds_id}) + elif device_name and sds_id: + device_details = self.powerflex_conn.device.get( + filter_fields={'name': device_name, + 'sdsId': sds_id}) + else: + device_details = self.powerflex_conn.device.get( + filter_fields={'id': device_id}) + + if len(device_details) == 0: + msg = "Device not found" + LOG.info(msg) + return None + + return device_details[0] + + except Exception as e: + error_msg = "Failed to get the device with error '%s'" % str(e) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def get_sds(self, sds_name=None, sds_id=None): + """Get SDS details + :param sds_name: Name of the SDS + :param sds_id: ID of the SDS + :return: SDS details + :rtype: dict + """ + name_or_id = sds_id if sds_id else sds_name + try: + sds_details = None + if sds_id: + sds_details = self.powerflex_conn.sds.get( + filter_fields={'id': sds_id}) + + if sds_name: + sds_details = self.powerflex_conn.sds.get( + filter_fields={'name': sds_name}) + + if not sds_details: + error_msg = "Unable to find the SDS with '%s'. Please " \ + "enter a valid SDS name/id." % name_or_id + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + return sds_details[0] + + except Exception as e: + error_msg = "Failed to get the SDS '%s' with error '%s'" \ + % (name_or_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def get_protection_domain(self, protection_domain_name=None, + protection_domain_id=None): + """Get protection domain details + :param protection_domain_name: Name of the protection domain + :param protection_domain_id: ID of the protection domain + :return: Protection domain details + :rtype: dict + """ + name_or_id = protection_domain_id if protection_domain_id \ + else protection_domain_name + try: + pd_details = None + if protection_domain_id: + pd_details = self.powerflex_conn.protection_domain.get( + filter_fields={'id': protection_domain_id}) + + if protection_domain_name: + pd_details = self.powerflex_conn.protection_domain.get( + filter_fields={'name': protection_domain_name}) + + if not pd_details: + error_msg = "Unable to find the protection domain with " \ + "'%s'. Please enter a valid protection domain " \ + "name/id." % name_or_id + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + return pd_details[0] + + except Exception as e: + error_msg = "Failed to get the protection domain '%s' with " \ + "error '%s'" % (name_or_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def get_storage_pool(self, storage_pool_name=None, + storage_pool_id=None, + protection_domain_id=None): + """Get storage pool details + :param storage_pool_name: Name of the storage pool + :param storage_pool_id: ID of the storage pool + :param protection_domain_id: ID of the protection domain + :return: Storage pool details + :rtype: dict + """ + name_or_id = storage_pool_id if storage_pool_id else storage_pool_name + try: + storage_pool_details = None + if storage_pool_id: + storage_pool_details = self.powerflex_conn.storage_pool.get( + filter_fields={'id': storage_pool_id}) + + if storage_pool_name: + storage_pool_details = self.powerflex_conn.storage_pool.get( + filter_fields={'name': storage_pool_name, + 'protectionDomainId': protection_domain_id} + ) + + if not storage_pool_details: + error_msg = "Unable to find the storage pool with " \ + "'%s'. Please enter a valid storage pool " \ + "name/id." % name_or_id + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + return storage_pool_details[0] + + except Exception as e: + error_msg = "Failed to get the storage_pool '%s' with " \ + "error '%s'" % (name_or_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def get_acceleration_pool(self, acceleration_pool_name=None, + acceleration_pool_id=None, + protection_domain_id=None): + """Get acceleration pool details + :param acceleration_pool_name: Name of the acceleration pool + :param acceleration_pool_id: ID of the acceleration pool + :param protection_domain_id: ID of the protection domain + :return: Acceleration pool details + :rtype: dict + """ + name_or_id = acceleration_pool_id \ + if acceleration_pool_id else acceleration_pool_name + try: + acceleration_pool_details = None + if acceleration_pool_id: + acceleration_pool_details = self.powerflex_conn.\ + acceleration_pool.get(filter_fields={ + 'id': acceleration_pool_id}) + + if acceleration_pool_name: + acceleration_pool_details = self.powerflex_conn.\ + acceleration_pool.get(filter_fields={ + 'name': acceleration_pool_name, + 'protectionDomainId': protection_domain_id}) + + if not acceleration_pool_details: + error_msg = "Unable to find the acceleration pool with " \ + "'%s'. Please enter a valid acceleration pool " \ + "name/id." % name_or_id + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + return acceleration_pool_details[0] + + except Exception as e: + error_msg = "Failed to get the acceleration pool '%s' with " \ + "error '%s'" % (name_or_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def add_device(self, device_name, current_pathname, sds_id, + storage_pool_id, media_type, acceleration_pool_id, + external_acceleration_type): + """Add device + :param device_name: Device name + :type device_name: str + :param current_pathname: Current pathname of device + :type current_pathname: str + :param sds_id: SDS ID + :type sds_id: str + :param storage_pool_id: Storage Pool ID + :type storage_pool_id: str + :param media_type: Media type of device + :type media_type: str + :param acceleration_pool_id: Acceleration pool ID + :type acceleration_pool_id: str + :param external_acceleration_type: External acceleration type + :type external_acceleration_type: str + return: Boolean indicating if add device operation is successful + """ + try: + if device_name is None or len(device_name.strip()) == 0: + error_msg = "Please provide valid device_name value for " \ + "adding a device." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + if current_pathname is None or len(current_pathname.strip()) == 0: + error_msg = "Current pathname of device is a mandatory " \ + "parameter for adding a device. Please enter a " \ + "valid value." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + if sds_id is None or len(sds_id.strip()) == 0: + error_msg = "Please provide valid sds_id value " \ + "for adding a device." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + if storage_pool_id is None and acceleration_pool_id is None: + error_msg = "Please provide either storage pool name/ID " \ + "or acceleration pool name/ID for adding a " \ + "device." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + add_params = ("current_pathname: %s, " + "sds_id: %s, " + "acceleration_pool_id: %s," + "external_acceleration_type: %s," + "media_type: %s," + "device_name: %s," + "storage_pool_id: %s," + % (current_pathname, sds_id, + acceleration_pool_id, + external_acceleration_type, + media_type, + device_name, + storage_pool_id)) + LOG.info("Adding device with params: %s", add_params) + + self.powerflex_conn.device.create( + current_pathname=current_pathname, + sds_id=sds_id, + acceleration_pool_id=acceleration_pool_id, + external_acceleration_type=external_acceleration_type, + media_type=media_type, + name=device_name, + storage_pool_id=storage_pool_id) + return True + except Exception as e: + error_msg = "Adding device %s operation failed with " \ + "error '%s'" % (device_name, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def remove_device(self, device_id): + """Remove device + :param device_id: Device ID + :type device_id: str + return: Boolean indicating if remove device operation is + successful + """ + try: + LOG.info("Device to be removed: %s", device_id) + self.powerflex_conn.device.delete(device_id=device_id) + return True + except Exception as e: + error_msg = "Remove device '%s' operation failed with " \ + "error '%s'" % (device_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def validate_input_parameters(self, device_name=None, device_id=None, + current_pathname=None, sds_name=None, + sds_id=None): + """Validate the input parameters""" + + # Unique ways to identify a device: + # (current_pathname , sds_id) + # (current_pathname , sds_name) + # (device_name , sds_name) + # (device_name , sds_id) + # device_id. + + if current_pathname: + if (sds_name is None or len(sds_name.strip()) == 0) \ + and (sds_id is None or len(sds_id.strip()) == 0): + error_msg = "sds_name or sds_id is mandatory along with " \ + "current_pathname. Please enter a valid value." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + elif current_pathname is not None \ + and len(current_pathname.strip()) == 0: + error_msg = "Please enter a valid value for current_pathname." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + if device_name: + if (sds_name is None or len(sds_name.strip()) == 0) \ + and (sds_id is None or len(sds_id.strip()) == 0): + error_msg = "sds_name or sds_id is mandatory along with " \ + "device_name. Please enter a valid value." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + elif device_name is not None and len(device_name.strip()) == 0: + error_msg = "Please enter a valid value for device_name." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + if sds_name: + if (current_pathname is None + or len(current_pathname.strip()) == 0) \ + and (device_name is None + or len(device_name.strip()) == 0): + error_msg = "current_pathname or device_name is mandatory " \ + "along with sds_name. Please enter a valid value." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + elif sds_name is not None and len(sds_name.strip()) == 0: + error_msg = "Please enter a valid value for sds_name." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + if sds_id: + if (current_pathname is None + or len(current_pathname.strip()) == 0) \ + and (device_name is None + or len(device_name.strip()) == 0): + error_msg = "current_pathname or device_name is mandatory " \ + "along with sds_id. Please enter a valid value." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + elif sds_id is not None and len(sds_id.strip()) == 0: + error_msg = "Please enter a valid value for sds_id." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + if device_id is not None and len(device_id.strip()) == 0: + error_msg = "Please provide valid device_id value to identify " \ + "a device." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + if current_pathname is None and device_name is None \ + and device_id is None: + error_msg = "Please specify a valid parameter combination to " \ + "identify a device." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def validate_add_parameters(self, device_id=None, + external_acceleration_type=None, + storage_pool_id=None, + storage_pool_name=None, + acceleration_pool_id=None, + acceleration_pool_name=None): + """Validate the add device parameters""" + + if device_id: + error_msg = "Addition of device is allowed using " \ + "device_name only, device_id given." + LOG.info(error_msg) + self.module.fail_json(msg=error_msg) + if external_acceleration_type and storage_pool_id is None \ + and storage_pool_name is None \ + and acceleration_pool_id is None \ + and acceleration_pool_name is None: + error_msg = "Storage Pool ID/name or Acceleration Pool " \ + "ID/name is mandatory along with " \ + "external_acceleration_type." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def perform_module_operation(self): + """ + Perform different actions on device based on parameters passed in + the playbook + """ + current_pathname = self.module.params['current_pathname'] + device_name = self.module.params['device_name'] + device_id = self.module.params['device_id'] + sds_name = self.module.params['sds_name'] + sds_id = self.module.params['sds_id'] + storage_pool_name = self.module.params['storage_pool_name'] + storage_pool_id = self.module.params['storage_pool_id'] + acceleration_pool_id = self.module.params['acceleration_pool_id'] + acceleration_pool_name = self.module.params['acceleration_pool_name'] + protection_domain_name = self.module.params['protection_domain_name'] + protection_domain_id = self.module.params['protection_domain_id'] + external_acceleration_type = self.module.params[ + 'external_acceleration_type'] + media_type = self.module.params['media_type'] + state = self.module.params['state'] + + # result is a dictionary to contain end state and device details + changed = False + result = dict( + changed=False, + device_details={} + ) + + # validate input parameters + self.validate_input_parameters(device_name, device_id, + current_pathname, sds_name, sds_id) + + # get SDS ID from name + if sds_name: + sds_details = self.get_sds(sds_name) + if sds_details: + sds_id = sds_details['id'] + msg = "Fetched the SDS details with id '%s', name '%s'" \ + % (sds_id, sds_name) + LOG.info(msg) + + # get device details + device_details = self.get_device_details(current_pathname, + sds_id, device_name, + device_id) + + if device_details: + device_id = device_details['id'] + msg = "Fetched the device details %s" % (str(device_details)) + LOG.info(msg) + + # add operation + add_changed = False + if state == 'present' and not device_details: + # get Protection Domain ID from name + # it is needed to uniquely identify a storage pool or acceleration + # pool using name + if protection_domain_name \ + and (storage_pool_name or acceleration_pool_name): + pd_details = self.get_protection_domain( + protection_domain_name) + if pd_details: + protection_domain_id = pd_details['id'] + msg = "Fetched the protection domain details with id " \ + "'%s', name '%s'" % (protection_domain_id, + protection_domain_name) + LOG.info(msg) + + # get storage pool ID from name + if storage_pool_name: + if protection_domain_id: + storage_pool_details = self.get_storage_pool( + storage_pool_name=storage_pool_name, + protection_domain_id=protection_domain_id) + if storage_pool_details: + storage_pool_id = storage_pool_details['id'] + msg = "Fetched the storage pool details with id '%s', " \ + "name '%s'" % (storage_pool_id, storage_pool_name) + LOG.info(msg) + else: + error_msg = "Protection domain name/id is required to " \ + "uniquely identify a storage pool, only " \ + "storage_pool_name is given." + LOG.info(error_msg) + self.module.fail_json(msg=error_msg) + + # get acceleration pool ID from name + if acceleration_pool_name: + if protection_domain_id: + acceleration_pool_details = self.get_acceleration_pool( + acceleration_pool_name=acceleration_pool_name, + protection_domain_id=protection_domain_id) + if acceleration_pool_details: + acceleration_pool_id = acceleration_pool_details['id'] + msg = "Fetched the acceleration pool details with id " \ + "'%s', name '%s'" % (acceleration_pool_id, + acceleration_pool_name) + LOG.info(msg) + else: + error_msg = "Protection domain name/id is required to " \ + "uniquely identify a acceleration pool, " \ + "only acceleration_pool_name is given." + LOG.info(error_msg) + self.module.fail_json(msg=error_msg) + + # validate input parameters + self.validate_add_parameters(device_id, + external_acceleration_type, + storage_pool_id, + storage_pool_name, + acceleration_pool_id, + acceleration_pool_name) + add_changed = self.add_device(device_name, current_pathname, + sds_id, storage_pool_id, media_type, + acceleration_pool_id, + external_acceleration_type) + if add_changed: + device_details = self.get_device_details( + device_name=device_name, sds_id=sds_id) + device_id = device_details['id'] + msg = "Device created successfully, fetched device details " \ + "%s" % (str(device_details)) + LOG.info(msg) + + # remove operation + remove_changed = False + if state == 'absent' and device_details: + remove_changed = self.remove_device(device_id) + + if add_changed or remove_changed: + changed = True + + # modify operation + if device_details and state == 'present': + modify_dict = to_modify(device_details, media_type, + external_acceleration_type) + if modify_dict: + error_msg = "Modification of device attributes is " \ + "currently not supported by Ansible modules." + LOG.info(error_msg) + self.module.fail_json(msg=error_msg) + + # Returning the updated device details + if state == 'present': + device_details = self.show_output(device_id) + result['device_details'] = device_details + result['changed'] = changed + self.module.exit_json(**result) + + def show_output(self, device_id): + """Show device details + :param device_id: ID of the device + :type device_id: str + :return: Details of device + :rtype: dict + """ + + try: + device_details = self.powerflex_conn.device.get( + filter_fields={'id': device_id}) + + if len(device_details) == 0: + msg = "Device with identifier '%s' not found" % device_id + LOG.error(msg) + return None + + # Append SDS name + if 'sdsId' in device_details[0] and device_details[0]['sdsId']: + sds_details = self.get_sds(sds_id=device_details[0]['sdsId']) + device_details[0]['sdsName'] = sds_details['name'] + + # Append storage pool name and its protection domain name and ID + if 'storagePoolId' in device_details[0] \ + and device_details[0]['storagePoolId']: + sp_details = self.get_storage_pool( + storage_pool_id=device_details[0]['storagePoolId']) + device_details[0]['storagePoolName'] = sp_details['name'] + pd_id = sp_details['protectionDomainId'] + device_details[0]['protectionDomainId'] = pd_id + pd_details = self.get_protection_domain( + protection_domain_id=pd_id) + device_details[0]['protectionDomainName'] = pd_details['name'] + + # Append acceleration pool name and its protection domain name + # and ID + if 'accelerationPoolId' in device_details[0] \ + and device_details[0]['accelerationPoolId']: + ap_details = self.get_acceleration_pool( + acceleration_pool_id=device_details[0][ + 'accelerationPoolId']) + device_details[0]['accelerationPoolName'] = ap_details['name'] + pd_id = ap_details['protectionDomainId'] + device_details[0]['protectionDomainId'] = pd_id + pd_details = self.get_protection_domain( + protection_domain_id=pd_id) + device_details[0]['protectionDomainName'] = pd_details['name'] + + return device_details[0] + + except Exception as e: + error_msg = "Failed to get the device '%s' with error '%s'"\ + % (device_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + +def to_modify(device_details, media_type, external_acceleration_type): + """Identify device attributes to be modified""" + + modify_dict = {} + + if media_type is not None and \ + device_details['mediaType'] != media_type: + modify_dict['mediaType'] = media_type + + if external_acceleration_type is not None and \ + device_details['externalAccelerationType'] \ + != external_acceleration_type: + modify_dict['externalAccelerationType'] \ + = external_acceleration_type + + if len(modify_dict) != 0: + LOG.info("Attributes to be modified: %s", modify_dict) + return modify_dict + + +def get_powerflex_device_parameters(): + """This method provide parameter required for the device module on + PowerFlex""" + return dict( + current_pathname=dict(), + device_name=dict(), + device_id=dict(), + sds_name=dict(), + sds_id=dict(), + storage_pool_name=dict(), + storage_pool_id=dict(), + acceleration_pool_id=dict(), + acceleration_pool_name=dict(), + protection_domain_name=dict(), + protection_domain_id=dict(), + external_acceleration_type=dict(choices=['Invalid', 'None', 'Read', + 'Write', 'ReadAndWrite']), + media_type=dict(choices=['HDD', 'SSD', 'NVDIMM']), + state=dict(required=True, type='str', choices=['present', 'absent']) + ) + + +def main(): + """ Create PowerFlex device object and perform actions on it + based on user input from playbook""" + obj = PowerFlexDevice() + obj.perform_module_operation() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/powerflex/plugins/modules/info.py b/ansible_collections/dellemc/powerflex/plugins/modules/info.py new file mode 100644 index 000000000..ff1401d63 --- /dev/null +++ b/ansible_collections/dellemc/powerflex/plugins/modules/info.py @@ -0,0 +1,1578 @@ +#!/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 Gathering information about Dell Technologies (Dell) PowerFlex""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: info + +version_added: '1.0.0' + +short_description: Gathering information about Dell PowerFlex + +description: +- Gathering information about Dell PowerFlex storage system includes + getting the api details, list of volumes, SDSs, SDCs, storage pools, + protection domains, snapshot policies, and devices. + +extends_documentation_fragment: + - dellemc.powerflex.powerflex + +author: +- Arindam Datta (@dattaarindam) <ansible.team@dell.com> + +options: + gather_subset: + description: + - List of string variables to specify the Powerflex storage system + entities for which information is required. + - Volumes - C(vol). + - Storage pools - C(storage_pool). + - Protection domains - C(protection_domain). + - SDCs - C(sdc). + - SDSs - C(sds). + - Snapshot policies - C(snapshot_policy). + - Devices - C(device). + - Replication consistency groups - C(rcg). + - Replication pairs - C(replication_pair). + choices: [vol, storage_pool, protection_domain, sdc, sds, + snapshot_policy, device, rcg, replication_pair] + type: list + elements: str + filters: + description: + - List of filters to support filtered output for storage entities. + - Each filter is a list of I(filter_key), I(filter_operator), I(filter_value). + - Supports passing of multiple filters. + type: list + elements: dict + suboptions: + filter_key: + description: + - Name identifier of the filter. + type: str + required: true + filter_operator: + description: + - Operation to be performed on filter key. + type: str + choices: [equal] + required: true + filter_value: + description: + - Value of the filter key. + type: str + required: true +notes: + - The I(check_mode) is supported. +''' + +EXAMPLES = r''' +- name: Get detailed list of PowerFlex entities + dellemc.powerflex.info: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + gather_subset: + - vol + - storage_pool + - protection_domain + - sdc + - sds + - snapshot_policy + - device + - rcg + - replication_pair + +- name: Get a subset list of PowerFlex volumes + dellemc.powerflex.info: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + gather_subset: + - vol + filters: + - filter_key: "name" + filter_operator: "equal" + filter_value: "ansible_test" +''' + +RETURN = r''' +changed: + description: Whether or not the resource has changed. + returned: always + type: bool + sample: 'false' +Array_Details: + description: System entities of PowerFlex storage array. + returned: always + type: dict + contains: + addressSpaceUsage: + description: Address space usage. + type: str + authenticationMethod: + description: Authentication method. + type: str + capacityAlertCriticalThresholdPercent: + description: Capacity alert critical threshold percentage. + type: int + capacityAlertHighThresholdPercent: + description: Capacity alert high threshold percentage. + type: int + capacityTimeLeftInDays: + description: Capacity time left in days. + type: str + cliPasswordAllowed: + description: CLI password allowed. + type: bool + daysInstalled: + description: Days installed. + type: int + defragmentationEnabled: + description: Defragmentation enabled. + type: bool + enterpriseFeaturesEnabled: + description: Enterprise features enabled. + type: bool + id: + description: The ID of the system. + type: str + installId: + description: installation Id. + type: str + isInitialLicense: + description: Initial license. + type: bool + lastUpgradeTime: + description: Last upgrade time. + type: int + managementClientSecureCommunicationEnabled: + description: Management client secure communication enabled. + type: bool + maxCapacityInGb: + description: Maximum capacity in GB. + type: dict + mdmCluster: + description: MDM cluster details. + type: dict + mdmExternalPort: + description: MDM external port. + type: int + mdmManagementPort: + description: MDM management port. + type: int + mdmSecurityPolicy: + description: MDM security policy. + type: str + showGuid: + description: Show guid. + type: bool + swid: + description: SWID. + type: str + systemVersionName: + description: System version and name. + type: str + tlsVersion: + description: TLS version. + type: str + upgradeState: + description: Upgrade state. + type: str + sample: { + "addressSpaceUsage": "Normal", + "authenticationMethod": "Native", + "capacityAlertCriticalThresholdPercent": 90, + "capacityAlertHighThresholdPercent": 80, + "capacityTimeLeftInDays": "24", + "cliPasswordAllowed": true, + "daysInstalled": 66, + "defragmentationEnabled": true, + "enterpriseFeaturesEnabled": true, + "id": "4a54a8ba6df0690f", + "installId": "38622771228e56db", + "isInitialLicense": true, + "lastUpgradeTime": 0, + "managementClientSecureCommunicationEnabled": true, + "maxCapacityInGb": "Unlimited", + "mdmCluster": { + "clusterMode": "ThreeNodes", + "clusterState": "ClusteredNormal", + "goodNodesNum": 3, + "goodReplicasNum": 2, + "id": "5356091375512217871", + "master": { + "id": "6101582c2ca8db00", + "ips": [ + "10.47.xxx.xxx" + ], + "managementIPs": [ + "10.47.xxx.xxx" + ], + "name": "node0", + "opensslVersion": "OpenSSL 1.0.2k-fips 26 Jan 2017", + "port": 9011, + "role": "Manager", + "status": "Normal", + "versionInfo": "R3_6.0.0", + "virtualInterfaces": [ + "ens160" + ] + }, + "slaves": [ + { + "id": "23fb724015661901", + "ips": [ + "10.47.xxx.xxx" + ], + "managementIPs": [ + "10.47.xxx.xxx" + ], + "opensslVersion": "OpenSSL 1.0.2k-fips 26 Jan 2017", + "port": 9011, + "role": "Manager", + "status": "Normal", + "versionInfo": "R3_6.0.0", + "virtualInterfaces": [ + "ens160" + ] + } + ], + "tieBreakers": [ + { + "id": "6ef27eb20d0c1202", + "ips": [ + "10.47.xxx.xxx" + ], + "managementIPs": [ + "10.47.xxx.xxx" + ], + "opensslVersion": "N/A", + "port": 9011, + "role": "TieBreaker", + "status": "Normal", + "versionInfo": "R3_6.0.0" + } + ] + }, + "mdmExternalPort": 7611, + "mdmManagementPort": 6611, + "mdmSecurityPolicy": "None", + "showGuid": true, + "swid": "", + "systemVersionName": "DellEMC PowerFlex Version: R3_6.0.354", + "tlsVersion": "TLSv1.2", + "upgradeState": "NoUpgrade" + } +API_Version: + description: API version of PowerFlex API Gateway. + returned: always + type: str + sample: "3.5" +Protection_Domains: + description: Details of all protection domains. + returned: always + type: list + contains: + id: + description: protection domain id. + type: str + name: + description: protection domain name. + type: str + sample: [ + { + "id": "9300e90900000001", + "name": "domain2" + }, + { + "id": "9300c1f900000000", + "name": "domain1" + } + ] +SDCs: + description: Details of storage data clients. + returned: always + type: list + contains: + id: + description: storage data client id. + type: str + name: + description: storage data client name. + type: str + sample: [ + { + "id": "07335d3d00000006", + "name": "LGLAP203" + }, + { + "id": "07335d3c00000005", + "name": "LGLAP178" + }, + { + "id": "0733844a00000003" + } + ] +SDSs: + description: Details of storage data servers. + returned: always + type: list + contains: + id: + description: storage data server id. + type: str + name: + description: storage data server name. + type: str + sample: [ + { + "id": "8f3bb0cc00000002", + "name": "node0" + }, + { + "id": "8f3bb0ce00000000", + "name": "node1" + }, + { + "id": "8f3bb15300000001", + "name": "node22" + } + ] +Snapshot_Policies: + description: Details of snapshot policies. + returned: always + type: list + contains: + id: + description: snapshot policy id. + type: str + name: + description: snapshot policy name. + type: str + sample: [ + { + "id": "2b380c5c00000000", + "name": "sample_snap_policy" + }, + { + "id": "2b380c5d00000001", + "name": "sample_snap_policy_1" + } + ] +Storage_Pools: + description: Details of storage pools. + returned: always + type: list + contains: + mediaType: + description: Type of devices in the storage pool. + type: str + useRfcache: + description: Enable/Disable RFcache on a specific storage pool. + type: bool + useRmcache: + description: Enable/Disable RMcache on a specific storage pool. + type: bool + id: + description: ID of the storage pool under protection domain. + type: str + name: + description: Name of the storage pool under protection domain. + type: str + protectionDomainId: + description: ID of the protection domain in which pool resides. + type: str + protectionDomainName: + description: Name of the protection domain in which pool resides. + type: str + statistics: + description: Statistics details of the storage pool. + type: dict + contains: + capacityInUseInKb: + description: Total capacity of the storage pool. + type: str + unusedCapacityInKb: + description: Unused capacity of the storage pool. + type: str + deviceIds: + description: Device Ids of the storage pool. + type: list + sample: [ + { + "addressSpaceUsage": "Normal", + "addressSpaceUsageType": "DeviceCapacityLimit", + "backgroundScannerBWLimitKBps": 3072, + "backgroundScannerMode": "DataComparison", + "bgScannerCompareErrorAction": "ReportAndFix", + "bgScannerReadErrorAction": "ReportAndFix", + "capacityAlertCriticalThreshold": 90, + "capacityAlertHighThreshold": 80, + "capacityUsageState": "Normal", + "capacityUsageType": "NetCapacity", + "checksumEnabled": false, + "compressionMethod": "Invalid", + "dataLayout": "MediumGranularity", + "externalAccelerationType": "None", + "fglAccpId": null, + "fglExtraCapacity": null, + "fglMaxCompressionRatio": null, + "fglMetadataSizeXx100": null, + "fglNvdimmMetadataAmortizationX100": null, + "fglNvdimmWriteCacheSizeInMb": null, + "fglOverProvisioningFactor": null, + "fglPerfProfile": null, + "fglWriteAtomicitySize": null, + "fragmentationEnabled": true, + "id": "e0d8f6c900000000", + "links": [ + { + "href": "/api/instances/StoragePool::e0d8f6c900000000", + "rel": "self" + }, + { + "href": "/api/instances/StoragePool::e0d8f6c900000000 + /relationships/Statistics", + "rel": "/api/StoragePool/relationship/Statistics" + }, + { + "href": "/api/instances/StoragePool::e0d8f6c900000000 + /relationships/SpSds", + "rel": "/api/StoragePool/relationship/SpSds" + }, + { + "href": "/api/instances/StoragePool::e0d8f6c900000000 + /relationships/Volume", + "rel": "/api/StoragePool/relationship/Volume" + }, + { + "href": "/api/instances/StoragePool::e0d8f6c900000000 + /relationships/Device", + "rel": "/api/StoragePool/relationship/Device" + }, + { + "href": "/api/instances/StoragePool::e0d8f6c900000000 + /relationships/VTree", + "rel": "/api/StoragePool/relationship/VTree" + }, + { + "href": "/api/instances/ProtectionDomain::9300c1f900000000", + "rel": "/api/parent/relationship/protectionDomainId" + } + ], + "statistics": { + "BackgroundScannedInMB": 3466920, + "activeBckRebuildCapacityInKb": 0, + "activeEnterProtectedMaintenanceModeCapacityInKb": 0, + "aggregateCompressionLevel": "Uncompressed", + "atRestCapacityInKb": 1248256, + "backgroundScanCompareErrorCount": 0, + "backgroundScanFixedCompareErrorCount": 0, + "bckRebuildReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "bckRebuildWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "capacityAvailableForVolumeAllocationInKb": 369098752, + "capacityInUseInKb": 2496512, + "capacityInUseNoOverheadInKb": 2496512, + "capacityLimitInKb": 845783040, + "compressedDataCompressionRatio": 0.0, + "compressionRatio": 1.0, + "currentFglMigrationSizeInKb": 0, + "deviceIds": [ + ], + "enterProtectedMaintenanceModeCapacityInKb": 0, + "enterProtectedMaintenanceModeReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "enterProtectedMaintenanceModeWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "exitProtectedMaintenanceModeReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "exitProtectedMaintenanceModeWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "exposedCapacityInKb": 0, + "failedCapacityInKb": 0, + "fwdRebuildReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "fwdRebuildWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "inMaintenanceCapacityInKb": 0, + "inMaintenanceVacInKb": 0, + "inUseVacInKb": 184549376, + "inaccessibleCapacityInKb": 0, + "logWrittenBlocksInKb": 0, + "maxCapacityInKb": 845783040, + "migratingVolumeIds": [ + ], + "migratingVtreeIds": [ + ], + "movingCapacityInKb": 0, + "netCapacityInUseInKb": 1248256, + "normRebuildCapacityInKb": 0, + "normRebuildReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "normRebuildWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "numOfDeviceAtFaultRebuilds": 0, + "numOfDevices": 3, + "numOfIncomingVtreeMigrations": 0, + "numOfVolumes": 8, + "numOfVolumesInDeletion": 0, + "numOfVtrees": 8, + "overallUsageRatio": 73.92289, + "pendingBckRebuildCapacityInKb": 0, + "pendingEnterProtectedMaintenanceModeCapacityInKb": 0, + "pendingExitProtectedMaintenanceModeCapacityInKb": 0, + "pendingFwdRebuildCapacityInKb": 0, + "pendingMovingCapacityInKb": 0, + "pendingMovingInBckRebuildJobs": 0, + "persistentChecksumBuilderProgress": 100.0, + "persistentChecksumCapacityInKb": 414720, + "primaryReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "primaryReadFromDevBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "primaryReadFromRmcacheBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "primaryVacInKb": 92274688, + "primaryWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "protectedCapacityInKb": 2496512, + "protectedVacInKb": 184549376, + "provisionedAddressesInKb": 2496512, + "rebalanceCapacityInKb": 0, + "rebalanceReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "rebalanceWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "rfacheReadHit": 0, + "rfacheWriteHit": 0, + "rfcacheAvgReadTime": 0, + "rfcacheAvgWriteTime": 0, + "rfcacheIoErrors": 0, + "rfcacheIosOutstanding": 0, + "rfcacheIosSkipped": 0, + "rfcacheReadMiss": 0, + "rmPendingAllocatedInKb": 0, + "rmPendingThickInKb": 0, + "rplJournalCapAllowed": 0, + "rplTotalJournalCap": 0, + "rplUsedJournalCap": 0, + "secondaryReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "secondaryReadFromDevBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "secondaryReadFromRmcacheBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "secondaryVacInKb": 92274688, + "secondaryWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "semiProtectedCapacityInKb": 0, + "semiProtectedVacInKb": 0, + "snapCapacityInUseInKb": 0, + "snapCapacityInUseOccupiedInKb": 0, + "snapshotCapacityInKb": 0, + "spSdsIds": [ + "abdfe71b00030001", + "abdce71d00040001", + "abdde71e00050001" + ], + "spareCapacityInKb": 84578304, + "targetOtherLatency": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "targetReadLatency": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "targetWriteLatency": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "tempCapacityInKb": 0, + "tempCapacityVacInKb": 0, + "thickCapacityInUseInKb": 0, + "thinAndSnapshotRatio": 73.92289, + "thinCapacityAllocatedInKm": 184549376, + "thinCapacityInUseInKb": 0, + "thinUserDataCapacityInKb": 2496512, + "totalFglMigrationSizeInKb": 0, + "totalReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "totalWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "trimmedUserDataCapacityInKb": 0, + "unreachableUnusedCapacityInKb": 0, + "unusedCapacityInKb": 758708224, + "userDataCapacityInKb": 2496512, + "userDataCapacityNoTrimInKb": 2496512, + "userDataReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataSdcReadLatency": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataSdcTrimLatency": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataSdcWriteLatency": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataTrimBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "volMigrationReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "volMigrationWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "volumeAddressSpaceInKb": 922XXXXX, + "volumeAllocationLimitInKb": 3707XXXXX, + "volumeIds": [ + "456afc7900XXXXXXXX" + ], + "vtreeAddresSpaceInKb": 92274688, + "vtreeIds": [ + "32b1681bXXXXXXXX", + ] + }, + "mediaType": "HDD", + "name": "pool1", + "numOfParallelRebuildRebalanceJobsPerDevice": 2, + "persistentChecksumBuilderLimitKb": 3072, + "persistentChecksumEnabled": true, + "persistentChecksumState": "Protected", + "persistentChecksumValidateOnRead": false, + "protectedMaintenanceModeIoPriorityAppBwPerDeviceThresholdInKbps": null, + "protectedMaintenanceModeIoPriorityAppIopsPerDeviceThreshold": null, + "protectedMaintenanceModeIoPriorityBwLimitPerDeviceInKbps": 10240, + "protectedMaintenanceModeIoPriorityNumOfConcurrentIosPerDevice": 1, + "protectedMaintenanceModeIoPriorityPolicy": "limitNumOfConcurrentIos", + "protectedMaintenanceModeIoPriorityQuietPeriodInMsec": null, + "protectionDomainId": "9300c1f900000000", + "protectionDomainName": "domain1", + "rebalanceEnabled": true, + "rebalanceIoPriorityAppBwPerDeviceThresholdInKbps": null, + "rebalanceIoPriorityAppIopsPerDeviceThreshold": null, + "rebalanceIoPriorityBwLimitPerDeviceInKbps": 10240, + "rebalanceIoPriorityNumOfConcurrentIosPerDevice": 1, + "rebalanceIoPriorityPolicy": "favorAppIos", + "rebalanceIoPriorityQuietPeriodInMsec": null, + "rebuildEnabled": true, + "rebuildIoPriorityAppBwPerDeviceThresholdInKbps": null, + "rebuildIoPriorityAppIopsPerDeviceThreshold": null, + "rebuildIoPriorityBwLimitPerDeviceInKbps": 10240, + "rebuildIoPriorityNumOfConcurrentIosPerDevice": 1, + "rebuildIoPriorityPolicy": "limitNumOfConcurrentIos", + "rebuildIoPriorityQuietPeriodInMsec": null, + "replicationCapacityMaxRatio": 32, + "rmcacheWriteHandlingMode": "Cached", + "sparePercentage": 10, + "useRfcache": false, + "useRmcache": false, + "vtreeMigrationIoPriorityAppBwPerDeviceThresholdInKbps": null, + "vtreeMigrationIoPriorityAppIopsPerDeviceThreshold": null, + "vtreeMigrationIoPriorityBwLimitPerDeviceInKbps": 10240, + "vtreeMigrationIoPriorityNumOfConcurrentIosPerDevice": 1, + "vtreeMigrationIoPriorityPolicy": "favorAppIos", + "vtreeMigrationIoPriorityQuietPeriodInMsec": null, + "zeroPaddingEnabled": true + } + ] +Volumes: + description: Details of volumes. + returned: always + type: list + contains: + id: + description: The ID of the volume. + 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 volume. + 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 volume. + type: str + sizeInKb: + description: Size of the volume in Kb. + type: int + sizeInGb: + description: Size of the volume in Gb. + type: int + storagePoolId: + description: ID of the storage pool in which volume resides. + type: str + storagePoolName: + description: Name of the storage pool in which volume resides. + type: str + protectionDomainId: + description: ID of the protection domain in which volume resides. + type: str + protectionDomainName: + description: Name of the protection domain in which volume resides. + type: str + snapshotPolicyId: + description: ID of the snapshot policy associated with volume. + type: str + snapshotPolicyName: + description: Name of the snapshot policy associated with volume. + type: str + snapshotsList: + description: List of snapshots associated with the volume. + type: str + "statistics": + description: Statistics details of the storage pool. + type: dict + contains: + "numOfChildVolumes": + description: Number of child volumes. + type: int + "numOfMappedSdcs": + description: Number of mapped Sdcs of the volume. + type: int + sample: [ + { + "accessModeLimit": "ReadWrite", + "ancestorVolumeId": null, + "autoSnapshotGroupId": null, + "compressionMethod": "Invalid", + "consistencyGroupId": null, + "creationTime": 1661234220, + "dataLayout": "MediumGranularity", + "id": "456afd7XXXXXXX", + "lockedAutoSnapshot": false, + "lockedAutoSnapshotMarkedForRemoval": false, + "managedBy": "ScaleIO", + "mappedSdcInfo": [ + { + "accessMode": "ReadWrite", + "isDirectBufferMapping": false, + "limitBwInMbps": 0, + "limitIops": 0, + "sdcId": "c42425cbXXXXX", + "sdcIp": "10.XXX.XX.XX", + "sdcName": null + } + ], + "name": "vol-1", + "notGenuineSnapshot": false, + "originalExpiryTime": 0, + "pairIds": null, + "replicationJournalVolume": false, + "replicationTimeStamp": 0, + "retentionLevels": [ + ], + "secureSnapshotExpTime": 0, + "sizeInKb": 8388608, + "snplIdOfAutoSnapshot": null, + "snplIdOfSourceVolume": null, + "statistics": { + "childVolumeIds": [ + ], + "descendantVolumeIds": [ + ], + "initiatorSdcId": null, + "mappedSdcIds": [ + "c42425XXXXXX" + ], + "numOfChildVolumes": 0, + "numOfDescendantVolumes": 0, + "numOfMappedSdcs": 1, + "registrationKey": null, + "registrationKeys": [ + ], + "replicationJournalVolume": false, + "replicationState": "UnmarkedForReplication", + "reservationType": "NotReserved", + "rplTotalJournalCap": 0, + "rplUsedJournalCap": 0, + "userDataReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataSdcReadLatency": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataSdcTrimLatency": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataSdcWriteLatency": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataTrimBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + } + }, + "storagePoolId": "7630a248XXXXXXX", + "timeStampIsAccurate": false, + "useRmcache": false, + "volumeReplicationState": "UnmarkedForReplication", + "volumeType": "ThinProvisioned", + "vtreeId": "32b168bXXXXXX" + } + ] +Devices: + description: Details of devices. + returned: always + type: list + contains: + id: + description: device id. + type: str + name: + description: device name. + type: str + sample: [ + { + "id": "b6efa59900000000", + "name": "device230" + }, + { + "id": "b6efa5fa00020000", + "name": "device_node0" + }, + { + "id": "b7f3a60900010000", + "name": "device22" + } + ] +Replication_Consistency_Groups: + description: Details of rcgs. + returned: always + type: list + contains: + id: + description: The ID of the replication consistency group. + type: str + name: + description: The name of the replication consistency group. + type: str + protectionDomainId: + description: The Protection Domain ID of the replication consistency group. + type: str + peerMdmId: + description: The ID of the peer MDM of the replication consistency group. + type: str + remoteId: + description: The ID of the remote replication consistency group. + type: str + remoteMdmId: + description: The ID of the remote MDM of the replication consistency group. + type: str + currConsistMode: + description: The current consistency mode of the replication consistency group. + type: str + freezeState: + description: The freeze state of the replication consistency group. + type: str + lifetimeState: + description: The Lifetime state of the replication consistency group. + type: str + pauseMode: + description: The Lifetime state of the replication consistency group. + type: str + snapCreationInProgress: + description: Whether the process of snapshot creation of the replication consistency group is in progress or not. + type: bool + lastSnapGroupId: + description: ID of the last snapshot of the replication consistency group. + type: str + lastSnapCreationRc: + description: The return code of the last snapshot of the replication consistency group. + type: int + targetVolumeAccessMode: + description: The access mode of the target volume of the replication consistency group. + type: str + remoteProtectionDomainId: + description: The ID of the remote Protection Domain. + type: str + remoteProtectionDomainName: + description: The Name of the remote Protection Domain. + type: str + failoverType: + description: The type of failover of the replication consistency group. + type: str + failoverState: + description: The state of failover of the replication consistency group. + type: str + activeLocal: + description: Whether the local replication consistency group is active. + type: bool + activeRemote: + description: Whether the remote replication consistency group is active + type: bool + abstractState: + description: The abstract state of the replication consistency group. + type: str + localActivityState: + description: The state of activity of the local replication consistency group. + type: str + remoteActivityState: + description: The state of activity of the remote replication consistency group.. + type: str + inactiveReason: + description: The reason for the inactivity of the replication consistency group. + type: int + rpoInSeconds: + description: The RPO value of the replication consistency group in seconds. + type: int + replicationDirection: + description: The direction of the replication of the replication consistency group. + type: str + disasterRecoveryState: + description: The state of disaster recovery of the local replication consistency group. + type: str + remoteDisasterRecoveryState: + description: The state of disaster recovery of the remote replication consistency group. + type: str + error: + description: The error code of the replication consistency group. + type: int + type: + description: The type of the replication consistency group. + type: str + sample: { + "protectionDomainId": "b969400500000000", + "peerMdmId": "6c3d94f600000000", + "remoteId": "2130961a00000000", + "remoteMdmId": "0e7a082862fedf0f", + "currConsistMode": "Consistent", + "freezeState": "Unfrozen", + "lifetimeState": "Normal", + "pauseMode": "None", + "snapCreationInProgress": false, + "lastSnapGroupId": "e58280b300000001", + "lastSnapCreationRc": "SUCCESS", + "targetVolumeAccessMode": "NoAccess", + "remoteProtectionDomainId": "4eeb304600000000", + "remoteProtectionDomainName": "domain1", + "failoverType": "None", + "failoverState": "None", + "activeLocal": true, + "activeRemote": true, + "abstractState": "Ok", + "localActivityState": "Active", + "remoteActivityState": "Active", + "inactiveReason": 11, + "rpoInSeconds": 30, + "replicationDirection": "LocalToRemote", + "disasterRecoveryState": "None", + "remoteDisasterRecoveryState": "None", + "error": 65, + "name": "test_rcg", + "type": "User", + "id": "aadc17d500000000" + } +Replication_pairs: + description: Details of the replication pairs. + returned: Always + type: list + contains: + id: + description: The ID of the replication pair. + type: str + name: + description: The name of the replication pair. + type: str + remoteId: + description: The ID of the remote replication pair. + type: str + localVolumeId: + description: The ID of the local volume. + type: str + replicationConsistencyGroupId: + description: The ID of the replication consistency group. + type: str + copyType: + description: The copy type of the replication pair. + type: str + initialCopyState: + description: The inital copy state of the replication pair. + type: str + localActivityState: + description: The state of activity of the local replication pair. + type: str + remoteActivityState: + description: The state of activity of the remote replication pair. + type: str + sample: { + "copyType": "OnlineCopy", + "id": "23aa0bc900000001", + "initialCopyPriority": -1, + "initialCopyState": "Done", + "lifetimeState": "Normal", + "localActivityState": "RplEnabled", + "localVolumeId": "e2bc1fab00000008", + "name": null, + "peerSystemName": null, + "remoteActivityState": "RplEnabled", + "remoteCapacityInMB": 8192, + "remoteId": "a058446700000001", + "remoteVolumeId": "1cda7af20000000d", + "remoteVolumeName": "vol", + "replicationConsistencyGroupId": "e2ce036b00000002", + "userRequestedPauseTransmitInitCopy": false + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.dellemc.powerflex.plugins.module_utils.storage.dell \ + import utils + +LOG = utils.get_logger('info') + + +class PowerFlexInfo(object): + """Class with Info operations""" + + filter_mapping = {'equal': 'eq.'} + + 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_info_parameters()) + + self.filter_keys = sorted( + [k for k in self.module_params['filters']['options'].keys() + if 'filter' in k]) + + """ initialize the ansible module """ + self.module = AnsibleModule(argument_spec=self.module_params, + supports_check_mode=True) + + 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') + LOG.info('The check_mode flag %s', self.module.check_mode) + + except Exception as e: + LOG.error(str(e)) + self.module.fail_json(msg=str(e)) + + def get_api_details(self): + """ Get api details of the array """ + try: + LOG.info('Getting API details ') + api_version = self.powerflex_conn.system.api_version() + return api_version + + except Exception as e: + msg = 'Get API details from Powerflex array failed with' \ + ' error %s' % (str(e)) + LOG.error(msg) + self.module.fail_json(msg=msg) + + def get_array_details(self): + """ Get system details of a powerflex array """ + + try: + LOG.info('Getting array details ') + entity_list = ['addressSpaceUsage', 'authenticationMethod', + 'capacityAlertCriticalThresholdPercent', + 'capacityAlertHighThresholdPercent', + 'capacityTimeLeftInDays', 'cliPasswordAllowed', + 'daysInstalled', 'defragmentationEnabled', + 'enterpriseFeaturesEnabled', 'id', 'installId', + 'isInitialLicense', 'lastUpgradeTime', + 'managementClientSecureCommunicationEnabled', + 'maxCapacityInGb', 'mdmCluster', + 'mdmExternalPort', 'mdmManagementPort', + 'mdmSecurityPolicy', 'showGuid', 'swid', + 'systemVersionName', 'tlsVersion', 'upgradeState'] + + sys_list = self.powerflex_conn.system.get() + sys_details_list = [] + for sys in sys_list: + sys_details = {} + for entity in entity_list: + if entity in sys.keys(): + sys_details.update({entity: sys[entity]}) + if sys_details: + sys_details_list.append(sys_details) + + return sys_details_list + + except Exception as e: + msg = 'Get array details from Powerflex array failed with' \ + ' error %s' % (str(e)) + LOG.error(msg) + self.module.fail_json(msg=msg) + + def get_sdc_list(self, filter_dict=None): + """ Get the list of sdcs on a given PowerFlex storage system """ + + try: + LOG.info('Getting SDC list ') + if filter_dict: + sdc = self.powerflex_conn.sdc.get(filter_fields=filter_dict) + else: + sdc = self.powerflex_conn.sdc.get() + return result_list(sdc) + + except Exception as e: + msg = 'Get SDC list from powerflex array failed with' \ + ' error %s' % (str(e)) + LOG.error(msg) + self.module.fail_json(msg=msg) + + def get_sds_list(self, filter_dict=None): + """ Get the list of sdses on a given PowerFlex storage system """ + + try: + LOG.info('Getting SDS list ') + if filter_dict: + sds = self.powerflex_conn.sds.get(filter_fields=filter_dict) + else: + sds = self.powerflex_conn.sds.get() + return result_list(sds) + + except Exception as e: + msg = 'Get sds list from powerflex array failed with' \ + ' error %s' % (str(e)) + LOG.error(msg) + self.module.fail_json(msg=msg) + + def get_pd_list(self, filter_dict=None): + """ Get the list of Protection Domains on a given PowerFlex + storage system """ + + try: + LOG.info('Getting protection domain list ') + + if filter_dict: + pd = self.powerflex_conn.protection_domain.get(filter_fields=filter_dict) + else: + pd = self.powerflex_conn.protection_domain.get() + return result_list(pd) + + except Exception as e: + msg = 'Get protection domain list from powerflex array failed ' \ + 'with error %s' % (str(e)) + LOG.error(msg) + self.module.fail_json(msg=msg) + + def get_storage_pool_list(self, filter_dict=None): + """ Get the list of storage pools on a given PowerFlex storage + system """ + + try: + LOG.info('Getting storage pool list ') + if filter_dict: + pool = self.powerflex_conn.storage_pool.get(filter_fields=filter_dict) + else: + pool = self.powerflex_conn.storage_pool.get() + + if pool: + statistics_map = self.powerflex_conn.utility.get_statistics_for_all_storagepools() + list_of_pool_ids_in_statistics = statistics_map.keys() + for item in pool: + item['statistics'] = statistics_map[item['id']] if item['id'] in list_of_pool_ids_in_statistics else {} + return result_list(pool) + + except Exception as e: + msg = 'Get storage pool list from powerflex array failed with' \ + ' error %s' % (str(e)) + LOG.error(msg) + self.module.fail_json(msg=msg) + + def get_replication_consistency_group_list(self, filter_dict=None): + """ Get the list of replication consistency group on a given PowerFlex storage + system """ + + try: + LOG.info('Getting replication consistency group list ') + if filter_dict: + rcgs = self.powerflex_conn.replication_consistency_group.get(filter_fields=filter_dict) + else: + rcgs = self.powerflex_conn.replication_consistency_group.get() + if rcgs: + api_version = self.powerflex_conn.system.get()[0]['mdmCluster']['master']['versionInfo'] + statistics_map = \ + self.powerflex_conn.replication_consistency_group.get_all_statistics(utils.is_version_less_than_3_6(api_version)) + list_of_rcg_ids_in_statistics = statistics_map.keys() + for rcg in rcgs: + rcg.pop('links', None) + rcg['statistics'] = statistics_map[rcg['id']] if rcg['id'] in list_of_rcg_ids_in_statistics else {} + return result_list(rcgs) + + except Exception as e: + msg = 'Get replication consistency group list from powerflex array failed with' \ + ' error %s' % (str(e)) + LOG.error(msg) + self.module.fail_json(msg=msg) + + def get_replication_pair_list(self, filter_dict=None): + """ Get the list of replication pairs on a given PowerFlex storage + system """ + + try: + LOG.info('Getting replication pair list ') + if filter_dict: + pairs = self.powerflex_conn.replication_pair.get(filter_fields=filter_dict) + else: + pairs = self.powerflex_conn.replication_pair.get() + if pairs: + for pair in pairs: + pair.pop('links', None) + local_volume = self.powerflex_conn.volume.get(filter_fields={'id': pair['localVolumeId']}) + if local_volume: + pair['localVolumeName'] = local_volume[0]['name'] + pair['replicationConsistencyGroupName'] = \ + self.powerflex_conn.replication_consistency_group.get(filter_fields={'id': pair['replicationConsistencyGroupId']})[0]['name'] + pair['statistics'] = self.powerflex_conn.replication_pair.get_statistics(pair['id']) + return pairs + + except Exception as e: + msg = 'Get replication pair list from powerflex array failed with' \ + ' error %s' % (str(e)) + LOG.error(msg) + self.module.fail_json(msg=msg) + + def get_volumes_list(self, filter_dict=None): + """ Get the list of volumes on a given PowerFlex storage + system """ + + try: + LOG.info('Getting volumes list ') + if filter_dict: + volumes = self.powerflex_conn.volume.get(filter_fields=filter_dict) + else: + volumes = self.powerflex_conn.volume.get() + + if volumes: + statistics_map = self.powerflex_conn.utility.get_statistics_for_all_volumes() + list_of_vol_ids_in_statistics = statistics_map.keys() + for item in volumes: + item['statistics'] = statistics_map[item['id']] if item['id'] in list_of_vol_ids_in_statistics else {} + return result_list(volumes) + + except Exception as e: + msg = 'Get volumes list from powerflex array failed with' \ + ' error %s' % (str(e)) + LOG.error(msg) + self.module.fail_json(msg=msg) + + def get_snapshot_policy_list(self, filter_dict=None): + """ Get the list of snapshot schedules on a given PowerFlex storage + system """ + + try: + LOG.info('Getting snapshot schedules list ') + if filter_dict: + snapshot_schedules = \ + self.powerflex_conn.snapshot_policy.get( + filter_fields=filter_dict) + else: + snapshot_schedules = \ + self.powerflex_conn.snapshot_policy.get() + + return result_list(snapshot_schedules) + + except Exception as e: + msg = 'Get snapshot schedules list from powerflex array failed ' \ + 'with error %s' % (str(e)) + LOG.error(msg) + self.module.fail_json(msg=msg) + + def get_devices_list(self, filter_dict=None): + """ Get the list of devices on a given PowerFlex storage + system """ + + try: + LOG.info('Getting device list ') + if filter_dict: + devices = self.powerflex_conn.device.get(filter_fields=filter_dict) + else: + devices = self.powerflex_conn.device.get() + + return result_list(devices) + + except Exception as e: + msg = 'Get device list from powerflex array failed ' \ + 'with error %s' % (str(e)) + LOG.error(msg) + self.module.fail_json(msg=msg) + + def validate_filter(self, filter_dict): + """ Validate given filter_dict """ + + is_invalid_filter = self.filter_keys != sorted(list(filter_dict)) + if is_invalid_filter: + msg = "Filter should have all keys: '{0}'".format( + ", ".join(self.filter_keys)) + LOG.error(msg) + self.module.fail_json(msg=msg) + + is_invalid_filter = [filter_dict[i] is None for i in filter_dict] + if True in is_invalid_filter: + msg = "Filter keys: '{0}' cannot be None".format(self.filter_keys) + LOG.error(msg) + self.module.fail_json(msg=msg) + + def get_filters(self, filters): + """Get the filters to be applied""" + + filter_dict = {} + for item in filters: + self.validate_filter(item) + f_op = item['filter_operator'] + if self.filter_mapping.get(f_op): + f_key = item['filter_key'] + f_val = item['filter_value'] + if f_key in filter_dict: + # multiple filters on same key + if isinstance(filter_dict[f_key], list): + # prev_val is list, so append new f_val + filter_dict[f_key].append(f_val) + else: + # prev_val is not list, + # so create list with prev_val & f_val + filter_dict[f_key] = [filter_dict[f_key], f_val] + else: + filter_dict[f_key] = f_val + else: + msg = "Given filter operator '{0}' is not supported." \ + "supported operators are : '{1}'".format( + f_op, + list(self.filter_mapping.keys())) + LOG.error(msg) + self.module.fail_json(msg=msg) + return filter_dict + + def perform_module_operation(self): + """ Perform different actions on info based on user input + in the playbook """ + + filters = self.module.params['filters'] + filter_dict = {} + if filters: + filter_dict = self.get_filters(filters) + LOG.info('filters: %s', filter_dict) + + api_version = self.get_api_details() + array_details = self.get_array_details() + sdc = [] + sds = [] + storage_pool = [] + vol = [] + snapshot_policy = [] + protection_domain = [] + device = [] + rcgs = [] + replication_pair = [] + + subset = self.module.params['gather_subset'] + if subset is not None: + if 'sdc' in subset: + sdc = self.get_sdc_list(filter_dict=filter_dict) + if 'sds' in subset: + sds = self.get_sds_list(filter_dict=filter_dict) + if 'protection_domain' in subset: + protection_domain = self.get_pd_list(filter_dict=filter_dict) + if 'storage_pool' in subset: + storage_pool = self.get_storage_pool_list(filter_dict=filter_dict) + if 'vol' in subset: + vol = self.get_volumes_list(filter_dict=filter_dict) + if 'snapshot_policy' in subset: + snapshot_policy = self.get_snapshot_policy_list(filter_dict=filter_dict) + if 'device' in subset: + device = self.get_devices_list(filter_dict=filter_dict) + if 'rcg' in subset: + rcgs = self.get_replication_consistency_group_list(filter_dict=filter_dict) + if 'replication_pair' in subset: + replication_pair = self.get_replication_pair_list(filter_dict=filter_dict) + + self.module.exit_json( + Array_Details=array_details, + API_Version=api_version, + SDCs=sdc, + SDSs=sds, + Storage_Pools=storage_pool, + Volumes=vol, + Snapshot_Policies=snapshot_policy, + Protection_Domains=protection_domain, + Devices=device, + Replication_Consistency_Groups=rcgs, + Replication_Pairs=replication_pair + ) + + +def result_list(entity): + """ Get the name and id associated with the PowerFlex entities """ + result = [] + if entity: + LOG.info('Successfully listed.') + for item in entity: + if item['name']: + result.append(item) + else: + result.append({"id": item['id']}) + return result + else: + return None + + +def get_powerflex_info_parameters(): + """This method provides parameters required for the ansible + info module on powerflex""" + return dict( + gather_subset=dict(type='list', required=False, elements='str', + choices=['vol', 'storage_pool', + 'protection_domain', 'sdc', 'sds', + 'snapshot_policy', 'device', 'rcg', 'replication_pair']), + filters=dict(type='list', required=False, elements='dict', + options=dict(filter_key=dict(type='str', required=True, no_log=False), + filter_operator=dict( + type='str', required=True, + choices=['equal']), + filter_value=dict(type='str', required=True) + ))) + + +def main(): + """ Create PowerFlex info object and perform action on it + based on user input from playbook""" + obj = PowerFlexInfo() + obj.perform_module_operation() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/powerflex/plugins/modules/mdm_cluster.py b/ansible_collections/dellemc/powerflex/plugins/modules/mdm_cluster.py new file mode 100644 index 000000000..084666bc3 --- /dev/null +++ b/ansible_collections/dellemc/powerflex/plugins/modules/mdm_cluster.py @@ -0,0 +1,1339 @@ +#!/usr/bin/python + +# Copyright: (c) 2022, Dell Technologies +# Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt) + +""" Ansible module for managing MDM Cluster on PowerFlex""" + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +DOCUMENTATION = r''' +module: mdm_cluster +version_added: '1.3.0' +short_description: Manage MDM cluster on Dell PowerFlex +description: +- Managing MDM cluster and MDMs on PowerFlex storage system includes + adding/removing standby MDM, modify MDM name and virtual interface. +- It also includes getting details of MDM cluster, modify MDM cluster + ownership, cluster mode, and performance profile. +author: +- Bhavneet Sharma (@sharmb5) <ansible.team@dell.com> +extends_documentation_fragment: + - dellemc.powerflex.powerflex +options: + mdm_name: + description: + - The name of the MDM. It is unique across the PowerFlex array. + - Mutually exclusive with I(mdm_id). + - If mdm_name passed in add standby operation, then same name will be + assigned to the new standby mdm. + type: str + mdm_id: + description: + - The ID of the MDM. + - Mutually exclusive with I(mdm_name). + type: str + mdm_new_name: + description: + - To rename the MDM. + type: str + standby_mdm: + description: + - Specifies add standby MDM parameters. + type: dict + suboptions: + mdm_ips: + description: + - List of MDM IPs that will be assigned to new MDM. It can contain + IPv4 addresses. + required: true + type: list + elements: str + role: + description: + - Role of new MDM. + required: true + choices: ['Manager', 'TieBreaker'] + type: str + management_ips: + description: + - List of management IPs to manage MDM. It can contain IPv4 + addresses. + type: list + elements: str + port: + description: + - Specifies the port of new MDM. + type: int + allow_multiple_ips: + description: + - Allow the added node to have different number of IPs from the + primary node. + type: bool + virtual_interfaces: + description: + - List of NIC interfaces that will be used for virtual IP addresses. + type: list + elements: str + is_primary: + description: + - Set I(is_primary) as C(true) to change MDM cluster ownership from the current + master MDM to different MDM. + - Set I(is_primary) as C(false), will return MDM cluster details. + - New owner MDM must be an MDM with a manager role. + type: bool + cluster_mode: + description: + - Mode of the cluster. + choices: ['OneNode', 'ThreeNodes', 'FiveNodes'] + type: str + mdm: + description: + - Specifies parameters to add/remove MDMs to/from the MDM cluster. + type: list + elements: dict + suboptions: + mdm_id: + description: + - ID of MDM that will be added/removed to/from the cluster. + type: str + mdm_name: + description: + - Name of MDM that will be added/removed to/from the cluster. + type: str + mdm_type: + description: + - Type of the MDM. + - Either I(mdm_id) or I(mdm_name) must be passed with mdm_type. + required: true + choices: ['Secondary', 'TieBreaker'] + type: str + mdm_state: + description: + - Mapping state of MDM. + choices: ['present-in-cluster', 'absent-in-cluster'] + type: str + virtual_ip_interfaces: + description: + - List of interfaces to be used for virtual IPs. + - The order of interfaces must be matched with virtual IPs assigned to the + cluster. + - Interfaces of the primary and secondary type MDMs are allowed to modify. + - The I(virtual_ip_interfaces) is mutually exclusive with I(clear_interfaces). + type: list + elements: str + clear_interfaces: + description: + - Clear all virtual IP interfaces. + - The I(clear_interfaces) is mutually exclusive with I(virtual_ip_interfaces). + type: bool + performance_profile: + description: + - Apply performance profile to cluster MDMs. + choices: ['Compact', 'HighPerformance'] + type: str + state: + description: + - State of the MDM cluster. + choices: ['present', 'absent'] + required: true + type: str +notes: + - Parameters I(mdm_name) or I(mdm_id) are mandatory for rename and modify virtual IP + interfaces. + - Parameters I(mdm_name) or I(mdm_id) are not required while modifying performance + profile. + - For change MDM cluster ownership operation, only changed as True will be + returned and for idempotency case MDM cluster details will be returned. + - Reinstall all SDC after changing ownership to some newly added MDM. + - To add manager standby MDM, MDM package must be installed with manager + role. + - The I(check_mode) is supported. +''' + +EXAMPLES = r''' +- name: Add a standby MDM + dellemc.powerflex.mdm_cluster: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + mdm_name: "mdm_1" + standby_mdm: + mdm_ips: + - "10.x.x.x" + role: "TieBreaker" + management_ips: + - "10.x.y.z" + state: "present" + +- name: Remove a standby MDM + dellemc.powerflex.mdm_cluster: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + mdm_name: "mdm_1" + state: "absent" + +- name: Switch cluster mode from 3 node to 5 node MDM cluster + dellemc.powerflex.mdm_cluster: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + cluster_mode: "FiveNodes" + mdm: + - mdm_id: "5f091a8a013f1100" + mdm_type: "Secondary" + - mdm_name: "mdm_1" + mdm_type: "TieBreaker" + sdc_state: "present-in-cluster" + state: "present" + +- name: Switch cluster mode from 5 node to 3 node MDM cluster + dellemc.powerflex.mdm_cluster: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + cluster_mode: "ThreeNodes" + mdm: + - mdm_id: "5f091a8a013f1100" + mdm_type: "Secondary" + - mdm_name: "mdm_1" + mdm_type: "TieBreaker" + sdc_state: "absent-in-cluster" + state: "present" + +- name: Get the details of the MDM cluster + dellemc.powerflex.mdm_cluster: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + state: "present" + +- name: Change ownership of MDM cluster + dellemc.powerflex.mdm_cluster: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + mdm_name: "mdm_2" + is_primary: True + state: "present" + +- name: Modify performance profile + dellemc.powerflex.mdm_cluster: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + performance_profile: "HighPerformance" + state: "present" + +- name: Rename the MDM + dellemc.powerflex.mdm_cluster: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + mdm_name: "mdm_1" + mdm_new_name: "new_mdm_1" + state: "present" + +- name: Modify virtual IP interface of the MDM + dellemc.powerflex.mdm_cluster: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + mdm_name: "mdm_1" + virtual_ip_interface: + - "ens224" + state: "present" + +- name: Clear virtual IP interface of the MDM + dellemc.powerflex.mdm_cluster: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + mdm_name: "mdm_1" + clear_interfaces: True + state: "present" +''' + +RETURN = r''' +changed: + description: Whether or not the resource has changed. + returned: always + type: bool + sample: 'false' +mdm_cluster_details: + description: Details of the MDM cluster. + returned: When MDM cluster exists + type: dict + contains: + id: + description: The ID of the MDM cluster. + type: str + name: + description: Name of MDM cluster. + type: str + clusterMode: + description: Mode of the MDM cluster. + type: str + master: + description: The details of the master MDM. + type: dict + contains: + id: + description: ID of the MDM. + type: str + name: + description: Name of the MDM. + type: str + port: + description: Port of the MDM. + type: str + ips: + description: List of IPs for master MDM. + type: list + managementIPs: + description: List of management IPs for master MDM. + type: list + role: + description: Role of MDM. + type: str + status: + description: Status of MDM. + type: str + versionInfo: + description: Version of MDM. + type: str + virtualInterfaces: + description: List of virtual interfaces + type: list + opensslVersion: + description: OpenSSL version. + type: str + slaves: + description: The list of the secondary MDMs. + type: list + elements: dict + contains: + id: + description: ID of the MDM. + type: str + name: + description: Name of the MDM. + type: str + port: + description: Port of the MDM. + type: str + ips: + description: List of IPs for secondary MDM. + type: list + managementIPs: + description: List of management IPs for secondary MDM. + type: list + role: + description: Role of MDM. + type: str + status: + description: Status of MDM. + type: str + versionInfo: + description: Version of MDM. + type: str + virtualInterfaces: + description: List of virtual interfaces + type: list + opensslVersion: + description: OpenSSL version. + type: str + tieBreakers: + description: The list of the TieBreaker MDMs. + type: list + elements: dict + contains: + id: + description: ID of the MDM. + type: str + name: + description: Name of the MDM. + type: str + port: + description: Port of the MDM. + type: str + ips: + description: List of IPs for tie-breaker MDM. + type: list + managementIPs: + description: List of management IPs for tie-breaker MDM. + type: list + role: + description: Role of MDM. + type: str + status: + description: Status of MDM. + type: str + versionInfo: + description: Version of MDM. + type: str + opensslVersion: + description: OpenSSL version. + type: str + standbyMDMs: + description: The list of the standby MDMs. + type: list + elements: dict + contains: + id: + description: ID of the MDM. + type: str + name: + description: Name of the MDM. + type: str + port: + description: Port of the MDM. + type: str + ips: + description: List of IPs for MDM. + type: list + managementIPs: + description: List of management IPs for MDM. + type: list + role: + description: Role of MDM. + type: str + status: + description: Status of MDM. + type: str + versionInfo: + description: Version of MDM. + type: str + virtualInterfaces: + description: List of virtual interfaces. + type: list + opensslVersion: + description: OpenSSL version. + type: str + clusterState: + description: State of the MDM cluster. + type: str + goodNodesNum: + description: Number of Nodes in MDM cluster. + type: int + goodReplicasNum: + description: Number of nodes for Replication. + type: int + virtualIps: + description: List of virtual IPs. + type: list + sample: { + "clusterState": "ClusteredNormal", + "clusterMode": "ThreeNodes", + "goodNodesNum": 3, + "master": { + "virtualInterfaces": [ + "ens1" + ], + "managementIPs": [ + "10.x.y.z" + ], + "ips": [ + "10.x.y.z" + ], + "versionInfo": "R3_6.0.0", + "opensslVersion": "OpenSSL 1.0.2k-fips 26 Jan 2017", + "role": "Manager", + "status": "Normal", + "name": "sample_mdm", + "id": "5908d328581d1400", + "port": 9011 + }, + "perfProfile": "HighPerformance", + "slaves": [ + { + "virtualInterfaces": [ + "ens1" + ], + "managementIPs": [ + "10.x.x.z" + ], + "ips": [ + "10.x.x.z" + ], + "versionInfo": "R3_6.0.0", + "opensslVersion": "OpenSSL 1.0.2k-fips 26 Jan 2017", + "role": "Manager", + "status": "Normal", + "name": "sample_mdm1", + "id": "5908d328581d1401", + "port": 9011 + } + ], + "tieBreakers": [ + { + "virtualInterfaces": [], + "managementIPs": [], + "ips": [ + "10.x.y.y" + ], + "versionInfo": "R3_6.0.0", + "opensslVersion": "N/A", + "role": "TieBreaker", + "status": "Normal", + "id": "5908d328581d1402", + "port": 9011 + } + ], + "standbyMDMs": [ + { + "virtualInterfaces": [], + "managementIPs": [ + "10.x.z.z" + ], + "ips": [ + "10.x.z.z" + ], + "versionInfo": "R3_6.0.0", + "opensslVersion": "N/A", + "role": "TieBreaker", + "status": "Normal", + "id": "5908d328581d1403", + "port": 9011 + } + ], + "goodReplicasNum": 2, + "id": "cdd883cf00000002" + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.dellemc.powerflex.plugins.module_utils.storage.dell \ + import utils +import copy + +LOG = utils.get_logger('mdm_cluster') + + +class PowerFlexMdmCluster(object): + """Class with MDM cluster 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_mdm_cluster_parameters()) + + mut_ex_args = [['mdm_name', 'mdm_id'], + ['virtual_ip_interfaces', 'clear_interfaces']] + + required_together_args = [['cluster_mode', 'mdm', 'mdm_state']] + + # initialize the Ansible module + self.module = AnsibleModule( + argument_spec=self.module_params, + supports_check_mode=True, + mutually_exclusive=mut_ex_args, + required_together=required_together_args) + + utils.ensure_required_libs(self.module) + + self.not_exist_msg = "MDM {0} does not exists in MDM cluster." + self.exist_msg = "MDM already exists in the MDM cluster" + try: + self.powerflex_conn = utils.get_powerflex_gateway_host_connection( + self.module.params) + LOG.info("Got the PowerFlex system connection object instance") + LOG.info('Check Mode Flag %s', self.module.check_mode) + except Exception as e: + LOG.error(str(e)) + self.module.fail_json(msg=str(e)) + + def set_mdm_virtual_interface(self, mdm_id=None, mdm_name=None, + virtual_ip_interfaces=None, + clear_interfaces=None, + mdm_cluster_details=None): + """Modify the MDM virtual IP interface. + :param mdm_id: ID of MDM + :param mdm_name: Name of MDM + :param virtual_ip_interfaces: List of virtual IP interfaces + :param clear_interfaces: clear virtual IP interfaces of MDM. + :param mdm_cluster_details: Details of MDM cluster + :return: True if modification of virtual interface or clear operation + successful + """ + + name_or_id = mdm_id if mdm_id else mdm_name + if mdm_name is None and mdm_id is None: + err_msg = "Please provide mdm_name/mdm_id to modify virtual IP" \ + " interfaces the MDM." + LOG.error(err_msg) + self.module.fail_json(msg=err_msg) + mdm_details = self.\ + is_mdm_name_id_exists(mdm_name=mdm_name, mdm_id=mdm_id, + cluster_details=mdm_cluster_details) + if mdm_details is None: + err_msg = self.not_exist_msg.format(name_or_id) + self.module.fail_json(msg=err_msg) + + mdm_id = mdm_details['id'] + modify_list = [] + modify_list, clear = is_modify_mdm_virtual_interface( + virtual_ip_interfaces, clear_interfaces, mdm_details) + + if modify_list is None and not clear: + LOG.info("No change required in MDM virtual IP interfaces.") + return False + + try: + log_msg = "Modifying MDM virtual interfaces to %s " \ + "or %s" % (str(modify_list), clear) + LOG.debug(log_msg) + if not self.module.check_mode: + self.powerflex_conn.system.modify_virtual_ip_interface( + mdm_id=mdm_id, virtual_ip_interfaces=modify_list, + clear_interfaces=clear) + return True + except Exception as e: + error_msg = "Failed to modify the virtual IP interfaces of MDM " \ + "{0} with error {1}".format(name_or_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def set_performance_profile(self, performance_profile=None, + cluster_details=None): + """ Set the performance profile of Cluster MDMs + :param performance_profile: Specifies the performance profile of MDMs + :param cluster_details: Details of MDM cluster + :return: True if updated successfully + """ + + if self.module.params['state'] == 'present' and performance_profile: + if cluster_details['perfProfile'] != performance_profile: + try: + if not self.module.check_mode: + self.powerflex_conn.system.\ + set_cluster_mdm_performance_profile(performance_profile=performance_profile) + return True + except Exception as e: + error_msg = "Failed to update performance profile to {0} " \ + "with error {1}.".format(performance_profile, + str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + return False + return False + + def rename_mdm(self, mdm_name=None, mdm_id=None, mdm_new_name=None, + cluster_details=None): + """Rename the MDM + :param mdm_name: Name of the MDM. + :param mdm_id: ID of the MDM. + :param mdm_new_name: New name of the MDM. + :param cluster_details: Details of the MDM cluster. + :return: True if successfully renamed. + """ + + name_or_id = mdm_id if mdm_id else mdm_name + if mdm_name is None and mdm_id is None: + err_msg = "Please provide mdm_name/mdm_id to rename the MDM." + self.module.fail_json(msg=err_msg) + mdm_details = self.\ + is_mdm_name_id_exists(mdm_name=mdm_name, mdm_id=mdm_id, + cluster_details=cluster_details) + if mdm_details is None: + err_msg = self.not_exist_msg.format(name_or_id) + self.module.fail_json(msg=err_msg) + + mdm_id = mdm_details['id'] + try: + if ('name' in mdm_details and + mdm_new_name != mdm_details['name']) or \ + 'name' not in mdm_details: + log_msg = "Modifying the MDM name from %s to " \ + "%s." % (mdm_name, mdm_new_name) + LOG.info(log_msg) + if not self.module.check_mode: + self.powerflex_conn.system.rename_mdm( + mdm_id=mdm_id, mdm_new_name=mdm_new_name) + return True + except Exception as e: + error_msg = "Failed to rename the MDM {0} with error {1}.".\ + format(name_or_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def is_none_name_id_in_switch_cluster_mode(self, mdm): + """ Check whether mdm dict have mdm_name and mdm_id or not""" + + for node in mdm: + if node['mdm_id'] and node['mdm_name']: + msg = "parameters are mutually exclusive: mdm_name|mdm_id" + self.module.fail_json(msg=msg) + + def change_cluster_mode(self, cluster_mode, mdm, cluster_details): + """change the MDM cluster mode. + :param cluster_mode: specifies the mode of MDM cluster + :param mdm: A dict containing parameters to change MDM cluster mode + :param cluster_details: Details of MDM cluster + :return: True if mode changed successfully + """ + + self.is_none_name_id_in_switch_cluster_mode(mdm=mdm) + + if cluster_mode == cluster_details['clusterMode']: + LOG.info("MDM cluster is already in required mode.") + return False + + add_secondary = [] + add_tb = [] + remove_secondary = [] + remove_tb = [] + if self.module.params['state'] == 'present' and \ + self.module.params['mdm_state'] == 'present-in-cluster': + add_secondary, add_tb = self.cluster_expand_list(mdm, cluster_details) + elif self.module.params['state'] == 'present' and \ + self.module.params['mdm_state'] == 'absent-in-cluster': + remove_secondary, remove_tb = self.\ + cluster_reduce_list(mdm, cluster_details) + try: + if not self.module.check_mode: + self.powerflex_conn.system.switch_cluster_mode( + cluster_mode=cluster_mode, add_secondary=add_secondary, + remove_secondary=remove_secondary, add_tb=add_tb, + remove_tb=remove_tb) + return True + except Exception as e: + err_msg = "Failed to change the MDM cluster mode with error " \ + "{0}".format(str(e)) + LOG.error(err_msg) + self.module.fail_json(msg=err_msg) + + def gather_secondarys_ids(self, mdm, cluster_details): + """ Prepare a list of secondary MDMs for switch cluster mode + operation""" + + secondarys = [] + + for node in mdm: + name_or_id = node['mdm_name'] if node['mdm_name'] else \ + node['mdm_id'] + + if node['mdm_type'] == 'Secondary' and node['mdm_id'] is not None: + mdm_details = self. \ + is_mdm_name_id_exists(mdm_id=node['mdm_id'], + cluster_details=cluster_details) + if mdm_details is None: + err_msg = self.not_exist_msg.format(name_or_id) + self.module.fail_json(msg=err_msg) + secondarys.append(node['mdm_id']) + + elif node['mdm_type'] == 'Secondary' and node['mdm_name'] is not None: + mdm_details = self. \ + is_mdm_name_id_exists(mdm_name=node['mdm_name'], + cluster_details=cluster_details) + if mdm_details is None: + err_msg = self.not_exist_msg.format(name_or_id) + self.module.fail_json(msg=err_msg) + else: + secondarys.append(mdm_details['id']) + return secondarys + + def cluster_expand_list(self, mdm, cluster_details): + """Whether MDM cluster expansion is required or not. + """ + add_secondary = [] + add_tb = [] + + if 'standbyMDMs' not in cluster_details: + err_msg = "No Standby MDMs found. To expand cluster size, " \ + "first add standby MDMs." + LOG.error(err_msg) + self.module.fail_json(msg=err_msg) + + add_secondary = self.gather_secondarys_ids(mdm, cluster_details) + for node in mdm: + name_or_id = node['mdm_name'] if node['mdm_name'] else \ + node['mdm_id'] + + if node['mdm_type'] == 'TieBreaker' and \ + node['mdm_id'] is not None: + add_tb.append(node['mdm_id']) + + elif node['mdm_type'] == 'TieBreaker' and \ + node['mdm_name'] is not None: + mdm_details = self. \ + is_mdm_name_id_exists(mdm_name=node['mdm_name'], + cluster_details=cluster_details) + + if mdm_details is None: + err_msg = self.not_exist_msg.format(name_or_id) + self.module.fail_json(msg=err_msg) + else: + add_tb.append(mdm_details['id']) + + log_msg = "expand List are: %s, %s" % (add_secondary, add_tb) + LOG.debug(log_msg) + return add_secondary, add_tb + + def cluster_reduce_list(self, mdm, cluster_details): + """Whether MDM cluster reduction is required or not. + """ + remove_secondary = [] + remove_tb = [] + + remove_secondary = self.gather_secondarys_ids(mdm, cluster_details) + for node in mdm: + name_or_id = node['mdm_name'] if node['mdm_name'] else \ + node['mdm_id'] + + if node['mdm_type'] == 'TieBreaker' and \ + node['mdm_id'] is not None: + mdm_details = self. \ + is_mdm_name_id_exists(mdm_id=node['mdm_id'], + cluster_details=cluster_details) + if mdm_details is None: + err_msg = self.not_exist_msg.format(name_or_id) + self.module.fail_json(msg=err_msg) + remove_tb.append(mdm_details['id']) + + elif node['mdm_type'] == 'TieBreaker' and \ + node['mdm_name'] is not None: + mdm_details = self.\ + is_mdm_name_id_exists(mdm_name=node['mdm_name'], + cluster_details=cluster_details) + if mdm_details is None: + err_msg = self.not_exist_msg.format(name_or_id) + self.module.fail_json(msg=err_msg) + else: + remove_tb.append(mdm_details['id']) + + log_msg = "Reduce List are: %s, %s." % (remove_secondary, remove_tb) + LOG.debug(log_msg) + return remove_secondary, remove_tb + + def perform_add_standby(self, mdm_name, standby_payload): + """ Perform SDK call to add a standby MDM + + :param mdm_name: Name of new standby MDM + :param standby_payload: Parameters dict to add a standby MDM + :return: True if standby MDM added successfully + """ + try: + if not self.module.check_mode: + self.powerflex_conn.system.add_standby_mdm( + mdm_ips=standby_payload['mdm_ips'], + role=standby_payload['role'], + management_ips=standby_payload['management_ips'], + mdm_name=mdm_name, port=standby_payload['port'], + allow_multiple_ips=standby_payload['allow_multiple_ips'], + virtual_interface=standby_payload['virtual_interfaces']) + return True + except Exception as e: + err_msg = "Failed to Add a standby MDM with error {0}.".format( + str(e)) + LOG.error(err_msg) + self.module.fail_json(msg=err_msg) + + def is_id_new_name_in_add_mdm(self): + """ Check whether mdm_id or mdm_new_name present in Add standby MDM""" + + if self.module.params['mdm_id'] or self.module.params['mdm_new_name']: + err_msg = "Parameters mdm_id/mdm_new_name are not allowed while" \ + " adding a standby MDM. Please try with valid " \ + "parameters to add a standby MDM." + LOG.error(err_msg) + self.module.fail_json(msg=err_msg) + + def add_standby_mdm(self, mdm_name, standby_mdm, cluster_details): + """ Adding a standby MDM""" + + if self.module.params['state'] == 'present' and \ + standby_mdm is not None and \ + (self.check_mdm_exists(standby_mdm['mdm_ips'], + cluster_details)): + self.is_id_new_name_in_add_mdm() + mdm_details = self.\ + is_mdm_name_id_exists(mdm_name=mdm_name, + cluster_details=cluster_details) + if mdm_details: + LOG.info("Standby MDM %s exits in the system", mdm_name) + return False, cluster_details + + standby_payload = prepare_standby_payload(standby_mdm) + standby_add = self.perform_add_standby(mdm_name, standby_payload) + + if standby_add: + cluster_details = self.get_mdm_cluster_details() + msg = "Fetched the MDM cluster details {0} after adding a " \ + "standby MDM".format(str(cluster_details)) + LOG.info(msg) + return True, cluster_details + return False, cluster_details + + def remove_standby_mdm(self, mdm_name, mdm_id, cluster_details): + """ Remove the Standby MDM + :param mdm_id: ID of MDM that will become owner of MDM cluster + :param mdm_name: Name of MDM that will become owner of MDM cluster + :param cluster_details: Details of MDM cluster + :return: True if MDM removed successful + """ + + name_or_id = mdm_id if mdm_id else mdm_name + if mdm_id is None and mdm_name is None: + err_msg = "Either mdm_name or mdm_id is required while removing" \ + " the standby MDM." + LOG.error(err_msg) + self.module.fail_json(msg=err_msg) + mdm_details = self. \ + is_mdm_name_id_exists(mdm_name=mdm_name, mdm_id=mdm_id, + cluster_details=cluster_details) + if mdm_details is None: + LOG.info("MDM %s not exists in MDM cluster.", name_or_id) + return False + mdm_id = mdm_details['id'] + + try: + if not self.module.check_mode: + self.powerflex_conn.system.remove_standby_mdm(mdm_id=mdm_id) + return True + except Exception as e: + error_msg = "Failed to remove the standby MDM {0} from the MDM " \ + "cluster with error {1}".format(name_or_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def change_ownership(self, mdm_id=None, mdm_name=None, + cluster_details=None): + """ Change the ownership of MDM cluster. + :param mdm_id: ID of MDM that will become owner of MDM cluster + :param mdm_name: Name of MDM that will become owner of MDM cluster + :param cluster_details: Details of MDM cluster + :return: True if Owner of MDM cluster change successful + """ + + name_or_id = mdm_id if mdm_id else mdm_name + if mdm_id is None and mdm_name is None: + err_msg = "Either mdm_name or mdm_id is required while changing" \ + " ownership of MDM cluster." + LOG.error(err_msg) + self.module.fail_json(msg=err_msg) + + mdm_details = self.\ + is_mdm_name_id_exists(mdm_name=mdm_name, mdm_id=mdm_id, + cluster_details=cluster_details) + if mdm_details is None: + err_msg = self.not_exist_msg.format(name_or_id) + self.module.fail_json(msg=err_msg) + + mdm_id = mdm_details['id'] + + if mdm_details['id'] == cluster_details['master']['id']: + LOG.info("MDM %s is already Owner of MDM cluster.", name_or_id) + return False + else: + try: + if not self.module.check_mode: + self.powerflex_conn.system.\ + change_mdm_ownership(mdm_id=mdm_id) + return True + except Exception as e: + error_msg = "Failed to update the Owner of MDM cluster to " \ + "MDM {0} with error {1}".format(name_or_id, + str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def find_mdm_in_secondarys(self, mdm_name=None, mdm_id=None, + cluster_details=None, name_or_id=None): + """Whether MDM exists with mdm_name or id in secondary MDMs""" + + if 'slaves' in cluster_details: + for mdm in cluster_details['slaves']: + if ('name' in mdm and mdm_name == mdm['name']) or \ + mdm_id == mdm['id']: + LOG.info("MDM %s found in Secondarys MDM.", name_or_id) + return mdm + + def find_mdm_in_tb(self, mdm_name=None, mdm_id=None, + cluster_details=None, name_or_id=None): + """Whether MDM exists with mdm_name or id in tie-breaker MDMs""" + + if 'tieBreakers' in cluster_details: + for mdm in cluster_details['tieBreakers']: + if ('name' in mdm and mdm_name == mdm['name']) or \ + mdm_id == mdm['id']: + LOG.info("MDM %s found in tieBreakers MDM.", name_or_id) + return mdm + + def find_mdm_in_standby(self, mdm_name=None, mdm_id=None, + cluster_details=None, name_or_id=None): + """Whether MDM exists with mdm_name or id in standby MDMs""" + + if 'standbyMDMs' in cluster_details: + for mdm in cluster_details['standbyMDMs']: + if ('name' in mdm and mdm_name == mdm['name']) or \ + mdm_id == mdm['id']: + LOG.info("MDM %s found in standby MDM.", name_or_id) + return mdm + + def is_mdm_name_id_exists(self, mdm_id=None, mdm_name=None, + cluster_details=None): + """Whether MDM exists with mdm_name or id """ + + name_or_id = mdm_id if mdm_id else mdm_name + # check in master MDM + if ('name' in cluster_details['master'] and mdm_name == cluster_details['master']['name']) \ + or mdm_id == cluster_details['master']['id']: + LOG.info("MDM %s is master MDM.", name_or_id) + return cluster_details['master'] + + # check in secondary MDMs + secondary_mdm = [] + secondary_mdm = self.\ + find_mdm_in_secondarys(mdm_name=mdm_name, mdm_id=mdm_id, + cluster_details=cluster_details, + name_or_id=name_or_id) + if secondary_mdm is not None: + return secondary_mdm + + # check in tie-breaker MDMs + tb_mdm = [] + tb_mdm = self.find_mdm_in_tb(mdm_name=mdm_name, mdm_id=mdm_id, + cluster_details=cluster_details, + name_or_id=name_or_id) + if tb_mdm is not None: + return tb_mdm + + # check in standby MDMs + standby_mdm = self.find_mdm_in_standby(mdm_name=mdm_name, + mdm_id=mdm_id, + cluster_details=cluster_details, + name_or_id=name_or_id) + if standby_mdm is not None: + return standby_mdm + + LOG.info("MDM %s does not exists in MDM Cluster.", name_or_id) + return None + + def get_mdm_cluster_details(self): + """Get MDM cluster details + :return: Details of MDM Cluster if existed. + """ + + try: + mdm_cluster_details = self.powerflex_conn.system.\ + get_mdm_cluster_details() + + if len(mdm_cluster_details) == 0: + msg = "MDM cluster not found" + LOG.error(msg) + self.module.fail_json(msg=msg) + + # Append Performance profile + resp = self.get_system_details() + if resp is not None: + mdm_cluster_details['perfProfile'] = resp['perfProfile'] + + return mdm_cluster_details + + except Exception as e: + error_msg = "Failed to get the MDM cluster with error {0}." + error_msg = error_msg.format(str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def check_ip_in_secondarys(self, standby_ip, cluster_details): + """whether standby IPs present in secondary MDMs""" + + for secondary_mdm in cluster_details['slaves']: + current_secondary_ips = secondary_mdm['ips'] + for ips in standby_ip: + if ips in current_secondary_ips: + LOG.info(self.exist_msg) + return False + return True + + def check_ip_in_tbs(self, standby_ip, cluster_details): + """whether standby IPs present in tie-breaker MDMs""" + + for tb_mdm in cluster_details['tieBreakers']: + current_tb_ips = tb_mdm['ips'] + for ips in standby_ip: + if ips in current_tb_ips: + LOG.info(self.exist_msg) + return False + return True + + def check_ip_in_standby(self, standby_ip, cluster_details): + """whether standby IPs present in standby MDMs""" + + if 'standbyMDMs' in cluster_details: + for stb_mdm in cluster_details['tieBreakers']: + current_stb_ips = stb_mdm['ips'] + for ips in standby_ip: + if ips in current_stb_ips: + LOG.info(self.exist_msg) + return False + return True + + def check_mdm_exists(self, standby_ip=None, cluster_details=None): + """Check whether standby MDM exists in MDM Cluster""" + + # check in master node + current_master_ips = cluster_details['master']['ips'] + for ips in standby_ip: + if ips in current_master_ips: + LOG.info(self.exist_msg) + return False + + # check in secondary nodes + in_secondary = self.check_ip_in_secondarys(standby_ip=standby_ip, + cluster_details=cluster_details) + if not in_secondary: + return False + + # check in tie-breaker nodes + in_tbs = self.check_ip_in_tbs(standby_ip=standby_ip, + cluster_details=cluster_details) + if not in_tbs: + return False + + # check in Standby nodes + in_standby = self.check_ip_in_standby(standby_ip=standby_ip, + cluster_details=cluster_details) + if not in_standby: + return False + + LOG.info("New Standby MDM does not exists in MDM cluster") + return True + + def get_system_details(self): + """Get system details + :return: Details of PowerFlex system + """ + + 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] + 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 validate_parameters(self): + """Validate the input parameters""" + + name_params = ['mdm_name', 'mdm_id', 'mdm_new_name'] + msg = "Please provide the valid {0}" + + for n_item in name_params: + if self.module.params[n_item] is not None and \ + (len(self.module.params[n_item].strip()) or + self.module.params[n_item].count(" ") > 0) == 0: + err_msg = msg.format(n_item) + self.module.fail_json(msg=err_msg) + + def perform_module_operation(self): + """ + Perform different actions on MDM cluster based on parameters passed in + the playbook + """ + mdm_name = self.module.params['mdm_name'] + mdm_id = self.module.params['mdm_id'] + mdm_new_name = self.module.params['mdm_new_name'] + standby_mdm = copy.deepcopy(self.module.params['standby_mdm']) + is_primary = self.module.params['is_primary'] + cluster_mode = self.module.params['cluster_mode'] + mdm = copy.deepcopy(self.module.params['mdm']) + mdm_state = self.module.params['mdm_state'] + virtual_ip_interfaces = self.module.params['virtual_ip_interfaces'] + clear_interfaces = self.module.params['clear_interfaces'] + performance_profile = self.module.params['performance_profile'] + state = self.module.params['state'] + + # result is a dictionary to contain end state and MDM cluster details + changed = False + result = dict( + changed=False, + mdm_cluster_details={} + ) + self.validate_parameters() + + mdm_cluster_details = self.get_mdm_cluster_details() + msg = "Fetched the MDM cluster details {0}".\ + format(str(mdm_cluster_details)) + LOG.info(msg) + + standby_changed = False + performance_changed = False + renamed_changed = False + interface_changed = False + remove_changed = False + mode_changed = False + owner_changed = False + + # Add standby MDM + standby_changed, mdm_cluster_details = self.\ + add_standby_mdm(mdm_name, standby_mdm, mdm_cluster_details) + + # Update performance profile + performance_changed = self.\ + set_performance_profile(performance_profile, mdm_cluster_details) + + # Rename MDM + if state == 'present' and mdm_new_name: + renamed_changed = self.rename_mdm(mdm_name, mdm_id, mdm_new_name, + mdm_cluster_details) + + # Change MDM virtual IP interfaces + if state == 'present' and (virtual_ip_interfaces or clear_interfaces): + interface_changed = self.\ + set_mdm_virtual_interface(mdm_id, mdm_name, + virtual_ip_interfaces, + clear_interfaces, + mdm_cluster_details) + # change cluster mode + if state == 'present' and cluster_mode and mdm and mdm_state: + mode_changed = self.change_cluster_mode(cluster_mode, mdm, + mdm_cluster_details) + + # Remove standby MDM + if state == 'absent': + remove_changed = self.remove_standby_mdm(mdm_name, mdm_id, + mdm_cluster_details) + + # change ownership of MDM cluster + if state == 'present' and is_primary: + owner_changed = self.change_ownership(mdm_id, mdm_name, + mdm_cluster_details) + + # Setting Changed Flag + changed = update_change_flag(standby_changed, performance_changed, + renamed_changed, interface_changed, + mode_changed, remove_changed, + owner_changed) + + # Returning the updated MDM cluster details + # Checking whether owner of MDM cluster has changed + if owner_changed: + mdm_cluster_details = {} + else: + mdm_cluster_details = self.get_mdm_cluster_details() + + result['mdm_cluster_details'] = mdm_cluster_details + result['changed'] = changed + self.module.exit_json(**result) + + +def update_change_flag(standby_changed, performance_changed, renamed_changed, + interface_changed, mode_changed, remove_changed, + owner_changed): + """ Update the changed flag based on the operation performed in the task""" + + if standby_changed or performance_changed or renamed_changed or \ + interface_changed or mode_changed or remove_changed or \ + owner_changed: + return True + return False + + +def prepare_standby_payload(standby_mdm): + """prepare the payload for add standby MDM""" + payload_dict = {} + for mdm_keys in standby_mdm: + if standby_mdm[mdm_keys]: + payload_dict[mdm_keys] = standby_mdm[mdm_keys] + else: + payload_dict[mdm_keys] = None + return payload_dict + + +def is_modify_mdm_virtual_interface(virtual_ip_interfaces, clear_interfaces, + mdm_details): + """Check if modification in MDM virtual IP interface required.""" + + modify_list = [] + clear = False + existing_interfaces = mdm_details['virtualInterfaces'] + + # Idempotency check for virtual IP interface + if clear_interfaces is None and \ + len(existing_interfaces) == len(virtual_ip_interfaces) and \ + set(existing_interfaces) == set(virtual_ip_interfaces): + LOG.info("No changes required for virtual IP interface.") + return None, False + + # Idempotency check for clear_interfaces + if clear_interfaces and len(mdm_details['virtualInterfaces']) == 0: + LOG.info("No change required for clear interface.") + return None, False + + # clearing all virtual IP interfaces of MDM + elif clear_interfaces and len(mdm_details['virtualInterfaces']) != 0 and \ + virtual_ip_interfaces is None: + LOG.info("Clear all interfaces of the MDM.") + clear = True + return None, clear + + if virtual_ip_interfaces and clear_interfaces is None: + for interface in virtual_ip_interfaces: + modify_list.append(interface) + return modify_list, clear + + +def get_powerflex_mdm_cluster_parameters(): + """This method provide parameter required for the MDM cluster + module on PowerFlex""" + return dict( + mdm_name=dict(), mdm_id=dict(), mdm_new_name=dict(), + virtual_ip_interfaces=dict(type='list', elements='str'), + clear_interfaces=dict(type='bool'), is_primary=dict(type='bool'), + standby_mdm=dict(type='dict', options=dict( + mdm_ips=dict(type='list', elements='str', required=True), + role=dict(required=True, choices=['Manager', 'TieBreaker']), + management_ips=dict(type='list', elements='str'), + port=dict(type='int'), allow_multiple_ips=dict(type='bool'), + virtual_interfaces=dict(type='list', elements='str'))), + cluster_mode=dict(choices=['OneNode', 'ThreeNodes', 'FiveNodes']), + mdm=dict(type='list', elements='dict', + options=dict(mdm_id=dict(), mdm_name=dict(), + mdm_type=dict(required=True, + choices=['Secondary', 'TieBreaker']))), + mdm_state=dict(choices=['present-in-cluster', 'absent-in-cluster']), + performance_profile=dict(choices=['Compact', 'HighPerformance']), + state=dict(required=True, type='str', choices=['present', 'absent']) + ) + + +def main(): + """ Perform actions on MDM cluster based on user input from playbook""" + obj = PowerFlexMdmCluster() + obj.perform_module_operation() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/powerflex/plugins/modules/protection_domain.py b/ansible_collections/dellemc/powerflex/plugins/modules/protection_domain.py new file mode 100644 index 000000000..5ffdc6b63 --- /dev/null +++ b/ansible_collections/dellemc/powerflex/plugins/modules/protection_domain.py @@ -0,0 +1,1122 @@ +#!/usr/bin/python + +# Copyright: (c) 2022, Dell Technologies +# Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt) + +""" Ansible module for managing Protection Domain on Dell Technologies (Dell) PowerFlex""" +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +DOCUMENTATION = r''' +module: protection_domain +version_added: '1.2.0' +short_description: Manage Protection Domain on Dell PowerFlex +description: +- Managing Protection Domain on PowerFlex storage system includes creating, + modifying attributes, deleting and getting details of Protection Domain. +author: +- Bhavneet Sharma (@sharmb5) <ansible.team@dell.com> +extends_documentation_fragment: + - dellemc.powerflex.powerflex +options: + protection_domain_name: + description: + - The name of the protection domain. + - Mandatory for create operation. + - It is unique across the PowerFlex array. + - Mutually exclusive with I(protection_domain_id). + type: str + protection_domain_id: + description: + - The ID of the protection domain. + - Except for create operation, all other operations can be performed + using protection_domain_id. + - Mutually exclusive with I(protection_domain_name). + type: str + protection_domain_new_name: + description: + - Used to rename the protection domain. + type: str + is_active: + description: + - Used to activate or deactivate the protection domain. + type: bool + network_limits: + description: + - Network bandwidth limit used by all SDS in protection domain. + type: dict + suboptions: + rebuild_limit: + description: + - Limit the network bandwidth for rebuild. + type: int + rebalance_limit: + description: + - Limit the network bandwidth for rebalance. + type: int + vtree_migration_limit: + description: + - Limit the network bandwidth for vtree migration. + type: int + overall_limit: + description: + - Limit the overall network bandwidth. + type: int + bandwidth_unit: + description: + - Unit for network bandwidth limits. + type: str + choices: ['KBps', 'MBps', 'GBps'] + default: 'KBps' + rf_cache_limits: + description: + - Used to set the RFcache parameters of the protection domain. + type: dict + suboptions: + is_enabled: + description: + - Used to enable or disable RFcache in the protection domain. + type: bool + page_size: + description: + - Used to set the cache page size in KB. + type: int + max_io_limit: + description: + - Used to set cache maximum I/O limit in KB. + type: int + pass_through_mode: + description: + - Used to set the cache mode. + choices: ['None', 'Read', 'Write', 'ReadAndWrite', 'WriteMiss'] + type: str + state: + description: + - State of the protection domain. + required: true + type: str + choices: ['present', 'absent'] +notes: + - The protection domain can only be deleted if all its related objects have + been dissociated from the protection domain. + - If the protection domain set to inactive, then no operation can be + performed on protection domain. + - The I(check_mode) is not supported. +''' + +EXAMPLES = r''' +- name: Create protection domain + dellemc.powerflex.protection_domain: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + protection_domain_name: "domain1" + state: "present" + +- name: Create protection domain with all parameters + dellemc.powerflex.protection_domain: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + protection_domain_name: "domain1" + is_active: true + network_limits: + rebuild_limit: 10 + rebalance_limit: 17 + vtree_migration_limit: 14 + overall_limit: 20 + bandwidth_unit: "MBps" + rf_cache_limits: + is_enabled: true + page_size: 16 + max_io_limit: 128 + pass_through_mode: "Read" + state: "present" + +- name: Get protection domain details using name + dellemc.powerflex.protection_domain: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + protection_domain_name: "domain1" + state: "present" + +- name: Get protection domain details using ID + dellemc.powerflex.protection_domain: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + protection_domain_id: "5718253c00000004" + state: "present" + +- name: Modify protection domain attributes + dellemc.powerflex.protection_domain: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + protection_domain_name: "domain1" + protection_domain_new_name: "domain1_new" + network_limits: + rebuild_limit: 14 + rebalance_limit: 20 + overall_limit: 25 + bandwidth_unit: "MBps" + rf_cache_limits: + page_size: 64 + pass_through_mode: "WriteMiss" + state: "present" + +- name: Delete protection domain using name + dellemc.powerflex.protection_domain: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + protection_domain_name: "domain1_new" + state: "absent" +''' + +RETURN = r''' +changed: + description: Whether or not the resource has changed. + returned: always + type: bool + sample: 'false' +protection_domain_details: + description: Details of the protection domain. + returned: When protection domain exists + type: dict + contains: + fglDefaultMetadataCacheSize: + description: FGL metadata cache size. + type: int + fglDefaultNumConcurrentWrites: + description: FGL concurrent writes. + type: str + fglMetadataCacheEnabled: + description: Whether FGL cache enabled. + type: bool + id: + description: Protection domain ID. + type: str + links: + description: Protection domain links. + type: list + contains: + href: + description: Protection domain instance URL. + type: str + rel: + description: Protection domain's relationship with + different entities. + type: str + mdmSdsNetworkDisconnectionsCounterParameters: + description: MDM's SDS counter parameter. + type: dict + contains: + longWindow: + description: Long window for Counter Parameters. + type: int + mediumWindow: + description: Medium window for Counter Parameters. + type: int + shortWindow: + description: Short window for Counter Parameters. + type: int + name: + description: Name of the protection domain. + type: str + overallIoNetworkThrottlingEnabled: + description: Whether overall network throttling enabled. + type: bool + overallIoNetworkThrottlingInKbps: + description: Overall network throttling in KBps. + type: int + protectedMaintenanceModeNetworkThrottlingEnabled: + description: Whether protected maintenance mode network throttling + enabled. + type: bool + protectedMaintenanceModeNetworkThrottlingInKbps: + description: Protected maintenance mode network throttling in + KBps. + type: int + protectionDomainState: + description: State of protection domain. + type: int + rebalanceNetworkThrottlingEnabled: + description: Whether rebalance network throttling enabled. + type: int + rebalanceNetworkThrottlingInKbps: + description: Rebalance network throttling in KBps. + type: int + rebuildNetworkThrottlingEnabled: + description: Whether rebuild network throttling enabled. + type: int + rebuildNetworkThrottlingInKbps: + description: Rebuild network throttling in KBps. + type: int + rfcacheAccpId: + description: Id of RF cache acceleration pool. + type: str + rfcacheEnabled: + description: Whether RF cache is enabled or not. + type: bool + rfcacheMaxIoSizeKb: + description: RF cache maximum I/O size in KB. + type: int + rfcacheOpertionalMode: + description: RF cache operational mode. + type: str + rfcachePageSizeKb: + description: RF cache page size in KB. + type: bool + sdrSdsConnectivityInfo: + description: Connectivity info of SDR and SDS. + type: dict + contains: + clientServerConnStatus: + description: Connectivity status of client and server. + type: str + disconnectedClientId: + description: Disconnected client ID. + type: str + disconnectedClientName: + description: Disconnected client name. + type: str + disconnectedServerId: + description: Disconnected server ID. + type: str + disconnectedServerIp: + description: Disconnected server IP. + type: str + disconnectedServerName: + description: Disconnected server name. + type: str + sdsSdsNetworkDisconnectionsCounterParameters: + description: Counter parameter for SDS-SDS network. + type: dict + contains: + longWindow: + description: Long window for Counter Parameters. + type: int + mediumWindow: + description: Medium window for Counter Parameters. + type: int + shortWindow: + description: Short window for Counter Parameters. + type: int + storagePool: + description: List of storage pools. + type: list + systemId: + description: ID of system. + type: str + vtreeMigrationNetworkThrottlingEnabled: + description: Whether V-Tree migration network throttling enabled. + type: bool + vtreeMigrationNetworkThrottlingInKbps: + description: V-Tree migration network throttling in KBps. + type: int + sample: { + "fglDefaultMetadataCacheSize": 0, + "fglDefaultNumConcurrentWrites": 1000, + "fglMetadataCacheEnabled": false, + "id": "7bd6457000000000", + "links": [ + { + "href": "/api/instances/ProtectionDomain::7bd6457000000000", + "rel": "self" + }, + { + "href": "/api/instances/ProtectionDomain::7bd6457000000000/ + relationships/Statistics", + "rel": "/api/ProtectionDomain/relationship/Statistics" + }, + { + "href": "/api/instances/ProtectionDomain::7bd6457000000000/ + relationships/Sdr", + "rel": "/api/ProtectionDomain/relationship/Sdr" + }, + { + "href": "/api/instances/ProtectionDomain::7bd6457000000000/ + relationships/AccelerationPool", + "rel": "/api/ProtectionDomain/relationship/AccelerationPool" + }, + { + "href": "/api/instances/ProtectionDomain::7bd6457000000000/ + relationships/StoragePool", + "rel": "/api/ProtectionDomain/relationship/StoragePool" + }, + { + "href": "/api/instances/ProtectionDomain::7bd6457000000000/ + relationships/Sds", + "rel": "/api/ProtectionDomain/relationship/Sds" + }, + { + "href": "/api/instances/ProtectionDomain::7bd6457000000000/ + relationships/ReplicationConsistencyGroup", + "rel": "/api/ProtectionDomain/relationship/ + ReplicationConsistencyGroup" + }, + { + "href": "/api/instances/ProtectionDomain::7bd6457000000000/ + relationships/FaultSet", + "rel": "/api/ProtectionDomain/relationship/FaultSet" + }, + { + "href": "/api/instances/System::0989ce79058f150f", + "rel": "/api/parent/relationship/systemId" + } + ], + "mdmSdsNetworkDisconnectionsCounterParameters": { + "longWindow": { + "threshold": 700, + "windowSizeInSec": 86400 + }, + "mediumWindow": { + "threshold": 500, + "windowSizeInSec": 3600 + }, + "shortWindow": { + "threshold": 300, + "windowSizeInSec": 60 + } + }, + "name": "domain1", + "overallIoNetworkThrottlingEnabled": false, + "overallIoNetworkThrottlingInKbps": null, + "protectedMaintenanceModeNetworkThrottlingEnabled": false, + "protectedMaintenanceModeNetworkThrottlingInKbps": null, + "protectionDomainState": "Active", + "rebalanceNetworkThrottlingEnabled": false, + "rebalanceNetworkThrottlingInKbps": null, + "rebuildNetworkThrottlingEnabled": false, + "rebuildNetworkThrottlingInKbps": null, + "rfcacheAccpId": null, + "rfcacheEnabled": true, + "rfcacheMaxIoSizeKb": 128, + "rfcacheOpertionalMode": "WriteMiss", + "rfcachePageSizeKb": 64, + "sdrSdsConnectivityInfo": { + "clientServerConnStatus": "CLIENT_SERVER_CONN_STATUS_ALL + _CONNECTED", + "disconnectedClientId": null, + "disconnectedClientName": null, + "disconnectedServerId": null, + "disconnectedServerIp": null, + "disconnectedServerName": null + }, + "sdsConfigurationFailureCounterParameters": { + "longWindow": { + "threshold": 700, + "windowSizeInSec": 86400 + }, + "mediumWindow": { + "threshold": 500, + "windowSizeInSec": 3600 + }, + "shortWindow": { + "threshold": 300, + "windowSizeInSec": 60 + } + }, + "sdsDecoupledCounterParameters": { + "longWindow": { + "threshold": 700, + "windowSizeInSec": 86400 + }, + "mediumWindow": { + "threshold": 500, + "windowSizeInSec": 3600 + }, + "shortWindow": { + "threshold": 300, + "windowSizeInSec": 60 + } + }, + "sdsReceiveBufferAllocationFailuresCounterParameters": { + "longWindow": { + "threshold": 2000000, + "windowSizeInSec": 86400 + }, + "mediumWindow": { + "threshold": 200000, + "windowSizeInSec": 3600 + }, + "shortWindow": { + "threshold": 20000, + "windowSizeInSec": 60 + } + }, + "sdsSdsNetworkDisconnectionsCounterParameters": { + "longWindow": { + "threshold": 700, + "windowSizeInSec": 86400 + }, + "mediumWindow": { + "threshold": 500, + "windowSizeInSec": 3600 + }, + "shortWindow": { + "threshold": 300, + "windowSizeInSec": 60 + } + }, + "storagePool": [ + { + "id": "8d1cba1700000000", + "name": "pool1" + } + ], + "systemId": "0989ce79058f150f", + "vtreeMigrationNetworkThrottlingEnabled": false, + "vtreeMigrationNetworkThrottlingInKbps": null + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.dellemc.powerflex.plugins.module_utils.storage.dell \ + import utils + +LOG = utils.get_logger('protection_domain') + + +class PowerFlexProtectionDomain(object): + """Class with protection domain 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_protection_domain_parameters()) + + mut_ex_args = [['protection_domain_name', 'protection_domain_id']] + + required_one_of_args = [['protection_domain_name', + 'protection_domain_id']] + + # initialize the Ansible module + self.module = AnsibleModule( + argument_spec=self.module_params, + supports_check_mode=False, + mutually_exclusive=mut_ex_args, + required_one_of=required_one_of_args) + + 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 validate_input_params(self): + """Validate the input parameters""" + + name_params = ['protection_domain_name', 'protection_domain_new_name', + 'protection_domain_id'] + msg = "Please provide the valid {0}" + + for n_item in name_params: + if self.module.params[n_item] is not None and (len( + self.module.params[n_item].strip()) or self. + module.params[n_item].count(" ") > 0) == 0: + err_msg = msg.format(n_item) + self.module.fail_json(msg=err_msg) + + def is_id_or_new_name_in_create(self): + """Checking if protection domain id or new names present in create """ + + if self.module.params['protection_domain_new_name'] or \ + self.module.params['protection_domain_id']: + error_msg = "protection_domain_new_name/protection_domain_id " \ + "are not supported during creation of protection " \ + "domain. Please try with protection_domain_name." + LOG.info(error_msg) + self.module.fail_json(msg=error_msg) + + def get_storage_pool(self, protection_domain_id): + """ + Get Storage pools details + :param protection_domain_id: Name of the protection domain + :type protection_domain_id: str + :return: list containing storage pools which are present in + protection domain + """ + + try: + sps_list = [] + resp = self.powerflex_conn.protection_domain. \ + get_storage_pools(protection_domain_id=protection_domain_id) + for items in resp: + sp_name_id = dict() + sp_name_id['id'] = items['id'] + sp_name_id['name'] = items['name'] + sps_list.append(sp_name_id) + return sps_list + + except Exception as e: + errmsg = "Failed to get the storage pools present in protection" \ + " domain %s with error %s" % (protection_domain_id, str(e)) + LOG.error(errmsg) + self.module.fail_json(msg=errmsg) + + def get_protection_domain(self, protection_domain_name=None, + protection_domain_id=None): + """ + Get protection domain details + :param protection_domain_name: Name of the protection domain + :param protection_domain_id: ID of the protection domain + :return: Protection domain details if exists + :rtype: dict + """ + + name_or_id = protection_domain_id if protection_domain_id \ + else protection_domain_name + + try: + if protection_domain_id: + pd_details = self.powerflex_conn.protection_domain.get( + filter_fields={'id': protection_domain_id}) + + else: + pd_details = self.powerflex_conn.protection_domain.get( + filter_fields={'name': protection_domain_name}) + + if len(pd_details) == 0: + error_msg = "Unable to find the protection domain with " \ + "'%s'." % name_or_id + LOG.info(error_msg) + return None + + # Append storage pool list present in protection domain + pd_details[0]['storagePool'] = self.get_storage_pool(pd_details + [0]['id']) + return pd_details[0] + + except Exception as e: + error_msg = "Failed to get the protection domain '%s' with " \ + "error '%s'" % (name_or_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def create_protection_domain(self, protection_domain_name): + """ + Create Protection Domain + :param protection_domain_name: Name of the protection domain + :type protection_domain_name: str + :return: Boolean indicating if create operation is successful + """ + # Creation of Protection domain + try: + LOG.info("Creating protection domain with name: %s ", + protection_domain_name) + self.powerflex_conn.protection_domain.\ + create(name=protection_domain_name) + return True + + except Exception as e: + error_msg = "Create protection domain '%s' operation failed" \ + " with error '%s'" % (protection_domain_name, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def perform_create_operation(self, state, pd_details, + protection_domain_name): + """performing creation of protection domain details""" + + if state == 'present' and not pd_details: + self.is_id_or_new_name_in_create() + create_change = self.\ + create_protection_domain(protection_domain_name) + if create_change: + pd_details = self. \ + get_protection_domain(protection_domain_name) + msg = "Protection domain created successfully, fetched" \ + " protection domain details {0}". \ + format(str(pd_details)) + LOG.info(msg) + return create_change, pd_details + + return False, pd_details + + def is_modify_required(self, pd_details, network_limits, rf_cache_limits, + protection_domain_new_name, is_active): + """Check if modification required""" + + if (self.module.params['state'] == 'present') and pd_details and \ + (network_limits is not None or rf_cache_limits is not None + or protection_domain_new_name is not None or is_active is + not None): + return True + + def modify_nw_limits(self, protection_domain_id, nw_modify_dict, + create_flag=False): + """ + Modify Protection domain attributes + :param protection_domain_id: ID of the protection domain + :type protection_domain_id: str + :param nw_modify_dict: Dictionary containing the attributes of + protection domain which are to be updated + :type nw_modify_dict: dict + :param create_flag: Flag to indicate whether modify operation is + followed by create operation or not + :type create_flag: bool + :return: Boolean indicating if the operation is successful + """ + try: + msg = "Dict containing network modify params {0}".\ + format(str(nw_modify_dict)) + LOG.info(msg) + if 'rebuild_limit' in nw_modify_dict or 'rebalance_limit' in \ + nw_modify_dict or 'vtree_migration_limit' in \ + nw_modify_dict or 'overall_limit' in nw_modify_dict: + self.powerflex_conn.protection_domain.network_limits( + protection_domain_id=protection_domain_id, + rebuild_limit=nw_modify_dict['rebuild_limit'], + rebalance_limit=nw_modify_dict['rebalance_limit'], + vtree_migration_limit=nw_modify_dict['vtree_migration_limit'], + overall_limit=nw_modify_dict['overall_limit']) + msg = "The Network limits are updated to {0}, {1}, {2}, " \ + "{3} successfully.". \ + format(nw_modify_dict['rebuild_limit'], + nw_modify_dict['rebalance_limit'], + nw_modify_dict['vtree_migration_limit'], + nw_modify_dict['overall_limit']) + LOG.info(msg) + return True + + except Exception as e: + if create_flag: + err_msg = "Create protection domain is successful," \ + " but failed to update the network limits" \ + " {0} with error {1}".format(protection_domain_id, + str(e)) + else: + err_msg = "Failed to update the network limits of " \ + "protection domain {0} with error {1}".\ + format(protection_domain_id, str(e)) + LOG.error(err_msg) + self.module.fail_json(msg=err_msg) + + def modify_rf_limits(self, protection_domain_id, rf_modify_dict, + create_flag): + """ + Modify Protection domain attributes + :param protection_domain_id: ID of the protection domain + :type protection_domain_id: str + :param rf_modify_dict: Dict containing the attributes of rf cache + which are to be updated + :type rf_modify_dict: dict + :param create_flag: Flag to indicate whether modify operation is + followed by create operation or not + :type create_flag: bool + :return: Boolean indicating if the operation is successful + """ + try: + msg = "Dict containing network modify params {0}". \ + format(str(rf_modify_dict)) + LOG.info(msg) + + if 'is_enabled' in rf_modify_dict and \ + rf_modify_dict['is_enabled'] is not None: + self.powerflex_conn.protection_domain.set_rfcache_enabled( + protection_domain_id, rf_modify_dict['is_enabled']) + msg = "The RFcache is enabled to '%s' successfully." \ + % rf_modify_dict['is_enabled'] + LOG.info(msg) + + if 'page_size' in rf_modify_dict or 'max_io_limit' in \ + rf_modify_dict or 'pass_through_mode' in rf_modify_dict: + self.powerflex_conn.protection_domain.rfcache_parameters( + protection_domain_id=protection_domain_id, + page_size=rf_modify_dict['page_size'], + max_io_limit=rf_modify_dict['max_io_limit'], + pass_through_mode=rf_modify_dict['pass_through_mode']) + msg = "The RFcache parameters are updated to {0}, {1},{2}.'" \ + .format(rf_modify_dict['page_size'], + rf_modify_dict['max_io_limit'], + rf_modify_dict['pass_through_mode']) + LOG.info(msg) + return True + + except Exception as e: + if create_flag: + err_msg = "Create protection domain is successful," \ + " but failed to update the rf cache limits" \ + " {0} with error {1}".format(protection_domain_id, + str(e)) + else: + err_msg = "Failed to update the rf cache limits of " \ + "protection domain {0} with error {1}". \ + format(protection_domain_id, str(e)) + LOG.error(err_msg) + self.module.fail_json(msg=err_msg) + + def modify_pd_attributes(self, protection_domain_id, modify_dict, + create_flag=False): + """ + Modify Protection domain attributes + :param protection_domain_id: ID of the protection domain + :type protection_domain_id: str + :param modify_dict: Dictionary containing the attributes of + protection domain which are to be updated + :type modify_dict: dict + :param create_flag: Flag to indicate whether modify operation is + followed by create operation or not + :type create_flag: bool + :return: Boolean indicating if the operation is successful + """ + try: + msg = "Dictionary containing attributes which need to be" \ + " updated are '%s'." % (str(modify_dict)) + LOG.info(msg) + + if 'protection_domain_new_name' in modify_dict: + self.powerflex_conn.protection_domain. \ + rename(protection_domain_id, + modify_dict['protection_domain_new_name']) + msg = "The name of the protection domain is updated to " \ + "'%s' successfully." % \ + modify_dict['protection_domain_new_name'] + LOG.info(msg) + + if 'is_active' in modify_dict and modify_dict['is_active']: + self.powerflex_conn.protection_domain. \ + activate(protection_domain_id, modify_dict['is_active']) + msg = "The protection domain is activated successfully, by " \ + "setting as is_active: '%s' " % \ + modify_dict['is_active'] + LOG.info(msg) + + if 'is_active' in modify_dict and not modify_dict['is_active']: + self.powerflex_conn.protection_domain. \ + inactivate(protection_domain_id, modify_dict['is_active']) + msg = "The protection domain is inactivated successfully, " \ + "by setting as is_active: '%s' " % \ + modify_dict['is_active'] + LOG.info(msg) + return True + + except Exception as e: + if create_flag: + err_msg = "Create protection domain is successful," \ + " but failed to update the protection domain" \ + " {0} with error {1}".format(protection_domain_id, + str(e)) + else: + err_msg = "Failed to update the protection domain {0}" \ + " with error {1}".format(protection_domain_id, + str(e)) + LOG.error(err_msg) + self.module.fail_json(msg=err_msg) + + def delete_protection_domain(self, protection_domain_id): + """ + Delete Protection Domain + :param protection_domain_id: ID of the protection domain + :type protection_domain_id: str + :return: Boolean indicating if delete operation is successful + """ + try: + self.powerflex_conn.protection_domain.delete(protection_domain_id) + LOG.info("Protection domain deleted successfully.") + return True + except Exception as e: + error_msg = "Delete protection domain '%s' operation failed" \ + " with error '%s'" % (protection_domain_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def perform_module_operation(self): + """ + Perform different actions on protection domain based on parameters + passed in the playbook + """ + protection_domain_name = self.module.params['protection_domain_name'] + protection_domain_id = self.module.params['protection_domain_id'] + protection_domain_new_name = self.module.params[ + 'protection_domain_new_name'] + is_active = self.module.params['is_active'] + network_limits = self.convert_limits_in_kbps( + self.module.params['network_limits']) + rf_cache_limits = self.module.params['rf_cache_limits'] + state = self.module.params['state'] + + # result is a dictionary to contain end state and protection domain + # details + changed = False + result = dict( + changed=False, + protection_domain_details=None + ) + + # Checking invalid value for id, name and rename + self.validate_input_params() + + # get Protection Domain details + pd_details = self.get_protection_domain(protection_domain_name, + protection_domain_id) + + if pd_details: + protection_domain_id = pd_details['id'] + msg = "Fetched the protection domain details with id '%s', name" \ + " '%s'" % (protection_domain_id, protection_domain_name) + LOG.info(msg) + + # create operation + create_changed = False + create_changed, pd_details = self.\ + perform_create_operation(state, pd_details, + protection_domain_name) + + # checking if basic protection domain parameters are modified or not + modify_dict = {} + nw_modify_dict = {} + rf_modify_dict = {} + + if self.is_modify_required(pd_details, network_limits, + rf_cache_limits, + protection_domain_new_name, is_active): + modify_dict = to_modify(pd_details, protection_domain_new_name, + is_active) + nw_modify_dict = to_nw_limit_modify(pd_details, network_limits) + rf_modify_dict = to_rf_limit_modify(pd_details, rf_cache_limits) + msg = "Parameters to be modified are as follows: %s %s, %s" \ + % (str(modify_dict), str(nw_modify_dict), + str(rf_modify_dict)) + LOG.info(msg) + + # modify operation + modify_changed = False + is_nw_limit = all(value is None for value in nw_modify_dict.values()) + is_rf_limit = all(value is None for value in rf_modify_dict.values()) + + if not is_nw_limit and state == 'present': + modify_changed = self.modify_nw_limits(pd_details['id'], + nw_modify_dict, + create_changed) + if not is_rf_limit and state == 'present': + modify_changed = self.modify_rf_limits(pd_details['id'], + rf_modify_dict, + create_changed) + if modify_dict and state == 'present': + modify_changed = self. \ + modify_pd_attributes(pd_details['id'], modify_dict, + create_changed) + if modify_changed: + pd_details = self.get_protection_domain( + protection_domain_id=pd_details['id']) + msg = "Protection domain details after modification:" \ + " '%s'" % str(pd_details) + LOG.info(msg) + + # delete operation + delete_changed = False + if state == 'absent' and pd_details: + delete_changed = self.\ + delete_protection_domain(pd_details['id']) + + if create_changed or modify_changed or delete_changed: + changed = True + + # Returning the updated Protection domain details + if state == 'present': + pd_details = self.get_protection_domain( + protection_domain_id=pd_details['id']) + result['protection_domain_details'] = pd_details + result['changed'] = changed + self.module.exit_json(**result) + + def convert_limits_in_kbps(self, network_limits): + """ + Convert the limits into KBps + + :param network_limits: dict containing all Network bandwidth limits + :rtype: converted network limits + """ + limit_params = ['rebuild_limit', 'rebalance_limit', + 'vtree_migration_limit', 'overall_limit'] + modified_limits = dict() + modified_limits['rebuild_limit'] = None + modified_limits['rebalance_limit'] = None + modified_limits['vtree_migration_limit'] = None + modified_limits['overall_limit'] = None + if network_limits is None: + return None + for limits in network_limits: + if network_limits[limits] is not None and limits in limit_params: + if network_limits['bandwidth_unit'] == "GBps": + modified_limits[limits] = \ + network_limits[limits] * 1024 * 1024 + elif network_limits['bandwidth_unit'] == "MBps": + modified_limits[limits] = network_limits[limits] * 1024 + else: + modified_limits[limits] = network_limits[limits] + + return modified_limits + + +def to_modify(pd_details, protection_domain_new_name, is_active): + """ + Check if modification required for rename and is_active for protection + domain + :param pd_details: Details of the protection domain + :type pd_details: dict + :param protection_domain_new_name: To rename protection domain + :type protection_domain_new_name: str + :param is_active: Whether to activate protection domain + :type is_active: bool + :return: Dictionary containing the attributes of protection domain + which are to be updated + :rtype: dict + """ + + modify_dict = dict() + if protection_domain_new_name is not None and \ + protection_domain_new_name != pd_details['name']: + modify_dict['protection_domain_new_name'] = \ + protection_domain_new_name + + if is_active is not None and \ + ((pd_details['protectionDomainState'] == 'Active' and + not is_active) or + (pd_details['protectionDomainState'] == 'Inactive' and + is_active)): + modify_dict['is_active'] = is_active + + return modify_dict + + +def to_nw_limit_modify(pd_details, network_limits): + """ + Check if modification required network bandwidth limit for protection + domain + :param pd_details: Details of the protection domain + :type pd_details: dict + :param network_limits: dict of Network bandwidth limit + :type network_limits: dict + :return: Dictionary containing the attributes of protection domain + which are to be updated + :rtype: dict + """ + + modify_dict = {} + if network_limits is not None: + modify_dict['rebuild_limit'] = None + modify_dict['rebalance_limit'] = None + modify_dict['vtree_migration_limit'] = None + modify_dict['overall_limit'] = None + + if network_limits['rebuild_limit'] is not None and \ + pd_details['rebuildNetworkThrottlingInKbps'] != network_limits['rebuild_limit']: + modify_dict['rebuild_limit'] = network_limits['rebuild_limit'] + + if network_limits['rebalance_limit'] is not None and \ + pd_details['rebalanceNetworkThrottlingInKbps'] \ + != network_limits['rebalance_limit']: + modify_dict['rebalance_limit'] = network_limits['rebalance_limit'] + + if network_limits['vtree_migration_limit'] is not None and \ + pd_details['vtreeMigrationNetworkThrottlingInKbps'] != \ + network_limits['vtree_migration_limit']: + modify_dict['vtree_migration_limit'] = network_limits['vtree_migration_limit'] + + if network_limits['overall_limit'] is not None and \ + pd_details['overallIoNetworkThrottlingInKbps'] != \ + network_limits['overall_limit']: + modify_dict['overall_limit'] = network_limits['overall_limit'] + + return modify_dict + + +def to_rf_limit_modify(pd_details, rf_cache_limits): + """ + Check if modification required for RF cache for protection domain + :param pd_details: Details of the protection domain + :type pd_details: dict + :param rf_cache_limits: dict for RF cache + :type rf_cache_limits: dict + :return: Dictionary containing the attributes of protection domain + which are to be updated + :rtype: dict + """ + modify_dict = {} + if rf_cache_limits is not None: + modify_dict['is_enabled'] = None + modify_dict['page_size'] = None + modify_dict['max_io_limit'] = None + modify_dict['pass_through_mode'] = None + + if rf_cache_limits['is_enabled'] is not None and pd_details['rfcacheEnabled'] != \ + rf_cache_limits['is_enabled']: + modify_dict['is_enabled'] = rf_cache_limits['is_enabled'] + + if rf_cache_limits['page_size'] is not None and pd_details['rfcachePageSizeKb'] != \ + rf_cache_limits['page_size']: + modify_dict['page_size'] = rf_cache_limits['page_size'] + + if rf_cache_limits['max_io_limit'] is not None and pd_details['rfcacheMaxIoSizeKb'] != \ + rf_cache_limits['max_io_limit']: + modify_dict['max_io_limit'] = rf_cache_limits['max_io_limit'] + + if rf_cache_limits['pass_through_mode'] is not None and \ + pd_details['rfcacheOpertionalMode'] != rf_cache_limits['pass_through_mode']: + modify_dict['pass_through_mode'] = rf_cache_limits['pass_through_mode'] + + return modify_dict + + +def get_powerflex_protection_domain_parameters(): + """This method provides parameters required for the protection domain + module on PowerFlex""" + return dict( + protection_domain_name=dict(), + protection_domain_new_name=dict(), + protection_domain_id=dict(), + is_active=dict(type='bool'), + network_limits=dict( + type='dict', options=dict( + rebuild_limit=dict(type='int'), + rebalance_limit=dict(type='int'), + vtree_migration_limit=dict(type='int'), + overall_limit=dict(type='int'), + bandwidth_unit=dict(choices=['KBps', 'MBps', 'GBps'], + default='KBps') + ) + ), + rf_cache_limits=dict( + type='dict', options=dict( + is_enabled=dict(type='bool'), + page_size=dict(type='int'), + max_io_limit=dict(type='int'), + pass_through_mode=dict(choices=['None', 'Read', 'Write', + 'ReadAndWrite', 'WriteMiss']) + ) + ), + state=dict(required=True, type='str', choices=['present', 'absent']) + ) + + +def main(): + """ Create PowerFlex protection domain object and perform actions on it + based on user input from playbook""" + obj = PowerFlexProtectionDomain() + obj.perform_module_operation() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/powerflex/plugins/modules/replication_consistency_group.py b/ansible_collections/dellemc/powerflex/plugins/modules/replication_consistency_group.py new file mode 100644 index 000000000..94ec651c3 --- /dev/null +++ b/ansible_collections/dellemc/powerflex/plugins/modules/replication_consistency_group.py @@ -0,0 +1,907 @@ +#!/usr/bin/python + +# Copyright: (c) 2022, Dell Technologies +# Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt) + +""" Ansible module for managing replication consistency groups on Dell Technologies (Dell) PowerFlex""" + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +DOCUMENTATION = r''' +module: replication_consistency_group +version_added: '1.5.0' +short_description: Manage replication consistency groups on Dell PowerFlex +description: +- Managing replication consistency groups on PowerFlex storage system includes + getting details, creating, modifying, creating snapshots, pause, resume, freeze, unfreeze, + activate, inactivate and deleting a replication consistency group. +author: +- Trisha Datta (@Trisha-Datta) <ansible.team@dell.com> +- Jennifer John (@Jennifer-John) <ansible.team@dell.com> +extends_documentation_fragment: + - dellemc.powerflex.powerflex +options: + rcg_name: + description: + - The name of the replication consistency group. + - It is unique across the PowerFlex array. + - Mutually exclusive with I(rcg_id). + type: str + rcg_id: + description: + - The ID of the replication consistency group. + - Mutually exclusive with I(rcg_name). + type: str + create_snapshot: + description: + - Whether to create the snapshot of the replication consistency group. + type: bool + rpo: + description: + - Desired RPO in seconds. + type: int + protection_domain_id: + description: + - Protection domain id. + - Mutually exclusive with I(protection_domain_name). + type: str + protection_domain_name: + description: + - Protection domain name. + - Mutually exclusive with I(protection_domain_id). + type: str + activity_mode: + description: + - Activity mode of RCG. + - This parameter is supported for version 3.6 and above. + choices: ['Active', 'Inactive'] + type: str + pause: + description: + - Pause or resume the RCG. + type: bool + freeze: + description: + - Freeze or unfreeze the RCG. + type: bool + pause_mode: + description: + - Pause mode. + - It is required if pause is set as True. + choices: ['StopDataTransfer', 'OnlyTrackChanges'] + type: str + target_volume_access_mode: + description: + - Target volume access mode. + choices: ['ReadOnly', 'NoAccess'] + type: str + is_consistent: + description: + - Consistency of RCG. + type: bool + new_rcg_name: + description: + - Name of RCG to rename to. + type: str + remote_peer: + description: + - Remote peer system. + type: dict + suboptions: + hostname: + required: true + description: + - IP or FQDN of the remote peer gateway host. + type: str + aliases: + - gateway_host + username: + type: str + required: true + description: + - The username of the remote peer gateway host. + password: + type: str + required: true + description: + - The password of the remote peer gateway host. + validate_certs: + type: bool + default: true + aliases: + - verifycert + description: + - Boolean variable to specify whether or not to validate SSL + certificate. + - C(true) - Indicates that the SSL certificate should be verified. + - C(false) - Indicates that the SSL certificate should not be verified. + port: + description: + - Port number through which communication happens with remote peer + gateway host. + type: int + default: 443 + timeout: + description: + - Time after which connection will get terminated. + - It is to be mentioned in seconds. + type: int + default: 120 + protection_domain_id: + description: + - Remote protection domain id. + - Mutually exclusive with I(protection_domain_name). + type: str + protection_domain_name: + description: + - Remote protection domain name. + - Mutually exclusive with I(protection_domain_id). + type: str + state: + description: + - State of the replication consistency group. + choices: ['present', 'absent'] + default: present + type: str +notes: +- The I(check_mode) is supported. +- Idempotency is not supported for create snapshot operation. +- There is a delay in reflection of final state of RCG after few update operations on RCG. +- In 3.6 and above, the replication consistency group will return back to consistent mode on changing to inconsistent mode + if consistence barrier arrives. Hence idempotency on setting to inconsistent mode will return changed as True. +''' + +EXAMPLES = r''' + +- name: Get RCG details + dellemc.powerflex.replication_consistency_group: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + rcg_name: "{{rcg_name}}" + +- name: Create a snapshot of the RCG + dellemc.powerflex.replication_consistency_group: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + rcg_id: "{{rcg_id}}" + create_snapshot: True + state: "present" + +- name: Create a replication consistency group + dellemc.powerflex.replication_consistency_group: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + rcg_name: "rcg_test" + rpo: 60 + protection_domain_name: "domain1" + activity_mode: "active" + remote_peer: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + protection_domain_name: "domain1" + +- name: Modify replication consistency group + dellemc.powerflex.replication_consistency_group: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + rcg_name: "rcg_test" + rpo: 60 + target_volume_access_mode: "ReadOnly" + activity_mode: "Inactive" + is_consistent: True + +- name: Rename replication consistency group + dellemc.powerflex.replication_consistency_group: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + rcg_name: "rcg_test" + new_rcg_name: "rcg_test_rename" + +- name: Pause replication consistency group + dellemc.powerflex.replication_consistency_group: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + rcg_name: "rcg_test" + pause: True + pause_mode: "StopDataTransfer" + +- name: Resume replication consistency group + dellemc.powerflex.replication_consistency_group: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + rcg_name: "rcg_test" + pause: False + +- name: Freeze replication consistency group + dellemc.powerflex.replication_consistency_group: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + rcg_name: "rcg_test" + freeze: True + +- name: UnFreeze replication consistency group + dellemc.powerflex.replication_consistency_group: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + rcg_name: "rcg_test" + freeze: False + +- name: Delete replication consistency group + dellemc.powerflex.replication_consistency_group: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + rcg_name: "rcg_test" + state: "absent" +''' + +RETURN = r''' +changed: + description: Whether or not the resource has changed. + returned: always + type: bool + sample: 'false' +replication_consistency_group_details: + description: Details of the replication consistency group. + returned: When replication consistency group exists + type: dict + contains: + id: + description: The ID of the replication consistency group. + type: str + name: + description: The name of the replication consistency group. + type: str + protectionDomainId: + description: The Protection Domain ID of the replication consistency group. + type: str + peerMdmId: + description: The ID of the peer MDM of the replication consistency group. + type: str + remoteId: + description: The ID of the remote replication consistency group. + type: str + remoteMdmId: + description: The ID of the remote MDM of the replication consistency group. + type: str + currConsistMode: + description: The current consistency mode of the replication consistency group. + type: str + freezeState: + description: The freeze state of the replication consistency group. + type: str + lifetimeState: + description: The Lifetime state of the replication consistency group. + type: str + pauseMode: + description: The Lifetime state of the replication consistency group. + type: str + snapCreationInProgress: + description: Whether the process of snapshot creation of the replication consistency group is in progress or not. + type: bool + lastSnapGroupId: + description: ID of the last snapshot of the replication consistency group. + type: str + lastSnapCreationRc: + description: The return code of the last snapshot of the replication consistency group. + type: int + targetVolumeAccessMode: + description: The access mode of the target volume of the replication consistency group. + type: str + remoteProtectionDomainId: + description: The ID of the remote Protection Domain. + type: str + remoteProtectionDomainName: + description: The Name of the remote Protection Domain. + type: str + failoverType: + description: The type of failover of the replication consistency group. + type: str + failoverState: + description: The state of failover of the replication consistency group. + type: str + activeLocal: + description: Whether the local replication consistency group is active. + type: bool + activeRemote: + description: Whether the remote replication consistency group is active + type: bool + abstractState: + description: The abstract state of the replication consistency group. + type: str + localActivityState: + description: The state of activity of the local replication consistency group. + type: str + remoteActivityState: + description: The state of activity of the remote replication consistency group.. + type: str + inactiveReason: + description: The reason for the inactivity of the replication consistency group. + type: int + rpoInSeconds: + description: The RPO value of the replication consistency group in seconds. + type: int + replicationDirection: + description: The direction of the replication of the replication consistency group. + type: str + disasterRecoveryState: + description: The state of disaster recovery of the local replication consistency group. + type: str + remoteDisasterRecoveryState: + description: The state of disaster recovery of the remote replication consistency group. + type: str + error: + description: The error code of the replication consistency group. + type: int + type: + description: The type of the replication consistency group. + type: str + sample: { + "protectionDomainId": "b969400500000000", + "peerMdmId": "6c3d94f600000000", + "remoteId": "2130961a00000000", + "remoteMdmId": "0e7a082862fedf0f", + "currConsistMode": "Consistent", + "freezeState": "Unfrozen", + "lifetimeState": "Normal", + "pauseMode": "None", + "snapCreationInProgress": false, + "lastSnapGroupId": "e58280b300000001", + "lastSnapCreationRc": "SUCCESS", + "targetVolumeAccessMode": "NoAccess", + "remoteProtectionDomainId": "4eeb304600000000", + "remoteProtectionDomainName": "domain1", + "failoverType": "None", + "failoverState": "None", + "activeLocal": true, + "activeRemote": true, + "abstractState": "Ok", + "localActivityState": "Active", + "remoteActivityState": "Active", + "inactiveReason": 11, + "rpoInSeconds": 30, + "replicationDirection": "LocalToRemote", + "disasterRecoveryState": "None", + "remoteDisasterRecoveryState": "None", + "error": 65, + "name": "test_rcg", + "type": "User", + "id": "aadc17d500000000" + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.dellemc.powerflex.plugins.module_utils.storage.dell \ + import utils + +LOG = utils.get_logger('replication_consistency_group') + + +class PowerFlexReplicationConsistencyGroup(object): + """Class with replication consistency group 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_replication_consistency_group_parameters()) + + mut_ex_args = [['rcg_name', 'rcg_id'], ['protection_domain_id', 'protection_domain_name']] + + required_one_of_args = [['rcg_name', 'rcg_id']] + + # initialize the Ansible module + self.module = AnsibleModule( + argument_spec=self.module_params, + supports_check_mode=True, + mutually_exclusive=mut_ex_args, + required_one_of=required_one_of_args) + + 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_rcg(self, rcg_name=None, rcg_id=None): + """Get rcg details + :param rcg_name: Name of the rcg + :param rcg_id: ID of the rcg + :return: RCG details + """ + name_or_id = rcg_id if rcg_id else rcg_name + try: + rcg_details = None + if rcg_id: + rcg_details = self.powerflex_conn.replication_consistency_group.get( + filter_fields={'id': rcg_id}) + + if rcg_name: + rcg_details = self.powerflex_conn.replication_consistency_group.get( + filter_fields={'name': rcg_name}) + + if rcg_details: + rcg_details[0]['statistics'] = \ + self.powerflex_conn.replication_consistency_group.get_statistics(rcg_details[0]['id']) + rcg_details[0].pop('links', None) + self.append_protection_domain_name(rcg_details[0]) + return rcg_details[0] + + except Exception as e: + errormsg = "Failed to get the replication consistency group {0} with" \ + " error {1}".format(name_or_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def create_rcg_snapshot(self, rcg_id): + """Create RCG snapshot + :param rcg_id: Unique identifier of the RCG. + :return: Boolean indicating if create snapshot operation is successful + """ + try: + if not self.module.check_mode: + self.powerflex_conn.replication_consistency_group.create_snapshot( + rcg_id=rcg_id) + return True + + except Exception as e: + errormsg = "Create RCG snapshot for RCG with id {0} operation failed with " \ + "error {1}".format(rcg_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def create_rcg(self, rcg_params): + """Create RCG""" + try: + resp = None + # Get remote system details + self.remote_powerflex_conn = utils.get_powerflex_gateway_host_connection( + self.module.params['remote_peer']) + LOG.info("Got the remote peer connection object instance") + protection_domain_id = rcg_params['protection_domain_id'] + if rcg_params['protection_domain_name']: + protection_domain_id = \ + self.get_protection_domain(self.powerflex_conn, rcg_params['protection_domain_name'])['id'] + + remote_protection_domain_id = rcg_params['remote_peer']['protection_domain_id'] + if rcg_params['remote_peer']['protection_domain_name']: + remote_protection_domain_id = \ + self.get_protection_domain(self.remote_powerflex_conn, + rcg_params['remote_peer']['protection_domain_name'])['id'] + + if not self.module.check_mode: + resp = self.powerflex_conn.replication_consistency_group.create( + rpo=rcg_params['rpo'], + protection_domain_id=protection_domain_id, + remote_protection_domain_id=remote_protection_domain_id, + destination_system_id=self.remote_powerflex_conn.system.get()[0]['id'], + name=rcg_params['rcg_name'], + activity_mode=rcg_params['activity_mode']) + return True, resp + + except Exception as e: + errormsg = "Create replication consistency group failed with error {0}".format(str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def modify_rpo(self, rcg_id, rpo): + """Modify rpo + :param rcg_id: Unique identifier of the RCG. + :param rpo: rpo value in seconds + :return: Boolean indicates if modify rpo is successful + """ + try: + if not self.module.check_mode: + self.powerflex_conn.replication_consistency_group.modify_rpo( + rcg_id, rpo) + return True + + except Exception as e: + errormsg = "Modify rpo for replication consistency group {0} failed with " \ + "error {1}".format(rcg_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def modify_target_volume_access_mode(self, rcg_id, target_volume_access_mode): + """Modify target volume access mode + :param rcg_id: Unique identifier of the RCG. + :param target_volume_access_mode: Target volume access mode. + :return: Boolean indicates if modify operation is successful + """ + try: + if not self.module.check_mode: + self.powerflex_conn.replication_consistency_group.modify_target_volume_access_mode( + rcg_id, target_volume_access_mode) + return True + + except Exception as e: + errormsg = "Modify target volume access mode for replication consistency group {0} failed with " \ + "error {1}".format(rcg_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def modify_activity_mode(self, rcg_id, rcg_details, activity_mode): + """Modify activity mode + :param rcg_id: Unique identifier of the RCG. + :param rcg_details: RCG details. + :param activity_mode: RCG activity mode. + :return: Boolean indicates if modify operation is successful + """ + try: + if activity_mode == 'Active' and rcg_details['localActivityState'].lower() == 'inactive': + if not self.module.check_mode: + self.powerflex_conn.replication_consistency_group.activate(rcg_id) + return True + elif activity_mode == 'Inactive' and rcg_details['localActivityState'].lower() == 'active': + if not self.module.check_mode: + rcg_details = self.powerflex_conn.replication_consistency_group.inactivate(rcg_id) + return True + except Exception as e: + errormsg = "Modify activity_mode for replication consistency group {0} failed with " \ + "error {1}".format(rcg_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def pause_or_resume_rcg(self, rcg_id, rcg_details, pause, pause_mode=None): + """Perform specified rcg action + :param rcg_id: Unique identifier of the RCG. + :param rcg_details: RCG details. + :param pause: Pause or resume RCG. + :param pause_mode: Specifies the pause mode if pause is True. + :return: Boolean indicates if rcg action is successful + """ + if pause and rcg_details['pauseMode'] == 'None': + if not pause_mode: + self.module.fail_json(msg="Specify pause_mode to perform pause on replication consistency group.") + return self.pause(rcg_id, pause_mode) + + if not pause and rcg_details['pauseMode'] != 'None': + return self.resume(rcg_id) + + def freeze_or_unfreeze_rcg(self, rcg_id, rcg_details, freeze): + """Perform specified rcg action + :param rcg_id: Unique identifier of the RCG. + :param rcg_details: RCG details. + :param freeze: Freeze or unfreeze RCG. + :return: Boolean indicates if rcg action is successful + """ + if freeze and rcg_details['freezeState'].lower() == 'unfrozen': + return self.freeze(rcg_id) + + if not freeze and rcg_details['freezeState'].lower() == 'frozen': + return self.unfreeze(rcg_id) + + def freeze(self, rcg_id): + try: + if not self.module.check_mode: + self.powerflex_conn.replication_consistency_group.freeze(rcg_id) + return True + except Exception as e: + errormsg = "Freeze replication consistency group {0} failed with error {1}".format(rcg_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def unfreeze(self, rcg_id): + try: + if not self.module.check_mode: + self.powerflex_conn.replication_consistency_group.unfreeze(rcg_id) + return True + except Exception as e: + errormsg = "Unfreeze replication consistency group {0} failed with error {1}".format(rcg_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def pause(self, rcg_id, pause_mode): + try: + if not self.module.check_mode: + self.powerflex_conn.replication_consistency_group.pause(rcg_id, pause_mode) + return True + except Exception as e: + errormsg = "Pause replication consistency group {0} failed with error {1}".format(rcg_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def resume(self, rcg_id): + try: + if not self.module.check_mode: + self.powerflex_conn.replication_consistency_group.resume(rcg_id) + return True + except Exception as e: + errormsg = "Resume replication consistency group {0} failed with error {1}".format(rcg_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def set_consistency(self, rcg_id, rcg_details, is_consistent): + """Set rcg to specified mode + :param rcg_id: Unique identifier of the RCG. + :param rcg_details: RCG details. + :param is_consistent: RCG consistency. + :return: Boolean indicates if set consistency is successful + """ + try: + if is_consistent and rcg_details['currConsistMode'].lower() not in ('consistent', 'consistentpending'): + if not self.module.check_mode: + self.powerflex_conn.replication_consistency_group.set_as_consistent(rcg_id) + return True + elif not is_consistent and rcg_details['currConsistMode'].lower() not in ('inconsistent', 'inconsistentpending'): + if not self.module.check_mode: + self.powerflex_conn.replication_consistency_group.set_as_inconsistent(rcg_id) + return True + except Exception as e: + errormsg = "Modifying consistency of replication consistency group failed with error {0}".format(str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def rename_rcg(self, rcg_id, rcg_details, new_name): + """Rename rcg + :param rcg_id: Unique identifier of the RCG. + :param rcg_details: RCG details + :param new_name: RCG name to rename to. + :return: Boolean indicates if rename is successful + """ + try: + if rcg_details['name'] != new_name: + if not self.module.check_mode: + self.powerflex_conn.replication_consistency_group.rename_rcg(rcg_id, new_name) + return True + except Exception as e: + errormsg = "Renaming replication consistency group to {0} failed with error {1}".format(new_name, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def delete_rcg(self, rcg_id): + """Delete RCG + :param rcg_id: Unique identifier of the RCG. + :return: Boolean indicates if delete rcg operation is successful + """ + try: + if not self.module.check_mode: + self.powerflex_conn.replication_consistency_group.delete( + rcg_id=rcg_id) + return True + + except Exception as e: + errormsg = "Delete replication consistency group {0} failed with " \ + "error {1}".format(rcg_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def get_protection_domain(self, conn, protection_domain_name=None, protection_domain_id=None): + """ + Get protection domain details + :param conn: local or remote connection + :param protection_domain_name: Name of the protection domain + :param protection_domain_id: ID of the protection domain + :return: Protection domain id if exists + :rtype: str + """ + name_or_id = protection_domain_id if protection_domain_id \ + else protection_domain_name + try: + pd_details = [] + if protection_domain_id: + pd_details = conn.protection_domain.get( + filter_fields={'id': protection_domain_id}) + + if protection_domain_name: + pd_details = conn.protection_domain.get( + filter_fields={'name': protection_domain_name}) + + if len(pd_details) == 0: + error_msg = "Unable to find the protection domain with " \ + "'%s'." % name_or_id + self.module.fail_json(msg=error_msg) + + return pd_details[0] + except Exception as e: + error_msg = "Failed to get the protection domain '%s' with " \ + "error '%s'" % (name_or_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def validate_create(self, rcg_params): + """Validate create RCG params""" + params = ['create_snapshot', 'new_rcg_name'] + for param in params: + if rcg_params[param] is not None: + self.module.fail_json(msg="%s cannot be specified while creating replication consistency group" % param) + if not rcg_params['rpo']: + self.module.fail_json(msg='Enter rpo to create replication consistency group') + if not rcg_params['remote_peer']: + self.module.fail_json(msg='Enter remote_peer to create replication consistency group') + if not rcg_params['protection_domain_id'] and not rcg_params['protection_domain_name']: + self.module.fail_json(msg='Enter protection_domain_name or protection_domain_id to create replication consistency group') + if (not rcg_params['remote_peer']['protection_domain_id'] and not rcg_params['remote_peer']['protection_domain_name']) or \ + (rcg_params['remote_peer']['protection_domain_id'] is not None and + rcg_params['remote_peer']['protection_domain_name'] is not None): + self.module.fail_json(msg='Enter remote protection_domain_name or protection_domain_id to create replication consistency group') + + def modify_rcg(self, rcg_id, rcg_details): + create_snapshot = self.module.params['create_snapshot'] + rpo = self.module.params['rpo'] + target_volume_access_mode = self.module.params['target_volume_access_mode'] + pause = self.module.params['pause'] + freeze = self.module.params['freeze'] + is_consistent = self.module.params['is_consistent'] + activity_mode = self.module.params['activity_mode'] + new_rcg_name = self.module.params['new_rcg_name'] + changed = False + + if create_snapshot is True: + changed = self.create_rcg_snapshot(rcg_id) + if rpo and rcg_details['rpoInSeconds'] and \ + rpo != rcg_details['rpoInSeconds']: + changed = self.modify_rpo(rcg_id, rpo) + if target_volume_access_mode and \ + rcg_details['targetVolumeAccessMode'] != target_volume_access_mode: + changed = \ + self.modify_target_volume_access_mode(rcg_id, target_volume_access_mode) + if activity_mode and \ + self.modify_activity_mode(rcg_id, rcg_details, activity_mode): + changed = True + rcg_details = self.get_rcg(rcg_id=rcg_details['id']) + if pause is not None and \ + self.pause_or_resume_rcg(rcg_id, rcg_details, pause, self.module.params['pause_mode']): + changed = True + if freeze is not None and \ + self.freeze_or_unfreeze_rcg(rcg_id, rcg_details, freeze): + changed = True + if is_consistent is not None and \ + self.set_consistency(rcg_id, rcg_details, is_consistent): + changed = True + if new_rcg_name and self.rename_rcg(rcg_id, rcg_details, new_rcg_name): + changed = True + + return changed + + def validate_input(self, rcg_params): + try: + api_version = self.powerflex_conn.system.get()[0]['mdmCluster']['master']['versionInfo'] + if rcg_params['activity_mode'] is not None and utils.is_version_less_than_3_6(api_version): + self.module.fail_json(msg='activity_mode is supported only from version 3.6 and above') + params = ['rcg_name', 'new_rcg_name'] + for param in params: + if rcg_params[param] and utils.is_invalid_name(rcg_params[param]): + self.module.fail_json(msg='Enter a valid %s' % param) + if rcg_params['pause_mode'] and rcg_params['pause'] is None: + self.module.fail_json(msg='Specify pause as True to pause replication consistency group') + except Exception as e: + error_msg = "Validating input parameters failed with " \ + "error '%s'" % (str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def append_protection_domain_name(self, rcg_details): + try: + # Append protection domain name + if 'protectionDomainId' in rcg_details \ + and rcg_details['protectionDomainId']: + pd_details = self.get_protection_domain( + conn=self.powerflex_conn, + protection_domain_id=rcg_details['protectionDomainId']) + rcg_details['protectionDomainName'] = pd_details['name'] + except Exception as e: + error_msg = "Updating replication consistency group details with protection domain name failed with " \ + "error '%s'" % (str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def perform_module_operation(self): + """ + Perform different actions on replication consistency group based on parameters passed in + the playbook + """ + self.validate_input(self.module.params) + rcg_name = self.module.params['rcg_name'] + new_rcg_name = self.module.params['new_rcg_name'] + rcg_id = self.module.params['rcg_id'] + state = self.module.params['state'] + + # result is a dictionary to contain end state and RCG details + changed = False + result = dict( + changed=False, + replication_consistency_group_details=[] + ) + # get RCG details + rcg_details = self.get_rcg(rcg_name, rcg_id) + if rcg_details: + result['replication_consistency_group_details'] = rcg_details + rcg_id = rcg_details['id'] + msg = "Fetched the RCG details {0}".format(str(rcg_details)) + LOG.info(msg) + + # perform create + if state == "present": + if not rcg_details: + self.validate_create(self.module.params) + changed, rcg_details = self.create_rcg(self.module.params) + if rcg_details: + rcg_id = rcg_details['id'] + + if rcg_details and self.modify_rcg(rcg_id, rcg_details): + changed = True + + if state == "absent" and rcg_details: + changed = self.delete_rcg(rcg_id=rcg_details['id']) + + # Returning the RCG details + if changed: + result['replication_consistency_group_details'] = \ + self.get_rcg(new_rcg_name or rcg_name, rcg_id) + result['changed'] = changed + self.module.exit_json(**result) + + +def get_powerflex_replication_consistency_group_parameters(): + """This method provide parameter required for the replication_consistency_group + module on PowerFlex""" + return dict( + rcg_name=dict(), rcg_id=dict(), + create_snapshot=dict(type='bool'), + rpo=dict(type='int'), protection_domain_id=dict(), + protection_domain_name=dict(), new_rcg_name=dict(), + activity_mode=dict(choices=['Active', 'Inactive']), + pause=dict(type='bool'), freeze=dict(type='bool'), + pause_mode=dict(choices=['StopDataTransfer', 'OnlyTrackChanges']), + target_volume_access_mode=dict(choices=['ReadOnly', 'NoAccess']), + is_consistent=dict(type='bool'), + remote_peer=dict(type='dict', + options=dict(hostname=dict(type='str', aliases=['gateway_host'], required=True), + username=dict(type='str', required=True), + password=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', aliases=['verifycert'], default=True), + port=dict(type='int', default=443), + timeout=dict(type='int', default=120), + protection_domain_id=dict(), + protection_domain_name=dict())), + state=dict(default='present', type='str', choices=['present', 'absent']) + ) + + +def main(): + """ Create PowerFlex Replication Consistency Group object and perform actions on it + based on user input from playbook""" + obj = PowerFlexReplicationConsistencyGroup() + obj.perform_module_operation() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/powerflex/plugins/modules/replication_pair.py b/ansible_collections/dellemc/powerflex/plugins/modules/replication_pair.py new file mode 100644 index 000000000..c95455023 --- /dev/null +++ b/ansible_collections/dellemc/powerflex/plugins/modules/replication_pair.py @@ -0,0 +1,695 @@ +#!/usr/bin/python + +# Copyright: (c) 2023, Dell Technologies +# Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt) + +""" Ansible module for managing replication pairs on Dell Technologies (Dell) PowerFlex""" + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +DOCUMENTATION = r''' +module: replication_pair +version_added: '1.6.0' +short_description: Manage replication pairs on Dell PowerFlex +description: +- Managing replication pairs on PowerFlex storage system includes + getting details, creating, pause, resume initial copy and deleting a replication pair. +author: +- Jennifer John (@Jennifer-John) <ansible.team@dell.com> +extends_documentation_fragment: + - dellemc.powerflex.powerflex +options: + pair_id: + description: + - The ID of the replication pair. + - Mutually exclusive with I(pair_name). + type: str + pair_name: + description: + - The name of the replication pair. + - Mutually exclusive with I(pair_id). + type: str + rcg_name: + description: + - The name of the replication consistency group. + - Mutually exclusive with I(rcg_id). + type: str + rcg_id: + description: + - The ID of the replication consistency group. + - Mutually exclusive with I(rcg_name). + type: str + pause: + description: + - Pause or resume the initial copy of replication pair. + type: bool + pairs: + description: + - List of replication pairs to add to rcg. + type: list + elements: dict + suboptions: + source_volume_id: + description: + - Source volume ID. + - Mutually exclusive with I(source_volume_name). + type: str + source_volume_name: + description: + - Source volume name. + - Mutually exclusive with I(source_volume_id). + type: str + target_volume_id: + description: + - Target volume ID. + - Mutually exclusive with I(target_volume_name). + type: str + target_volume_name: + description: + - Target volume name. + - If specified, I(remote_peer) details should also be specified. + - Mutually exclusive with I(target_volume_id). + type: str + copy_type: + description: + - Copy type. + choices: ['Identical', 'OnlineCopy', 'OnlineHashCopy', 'OfflineCopy'] + type: str + required: True + name: + description: + - Name of replication pair. + type: str + remote_peer: + description: + - Remote peer system. + type: dict + suboptions: + hostname: + required: true + description: + - IP or FQDN of the remote peer gateway host. + type: str + aliases: + - gateway_host + username: + type: str + required: true + description: + - The username of the remote peer gateway host. + password: + type: str + required: true + description: + - The password of the remote peer gateway host. + validate_certs: + type: bool + default: true + aliases: + - verifycert + description: + - Boolean variable to specify whether or not to validate SSL + certificate. + - C(true) - Indicates that the SSL certificate should be verified. + - C(false) - Indicates that the SSL certificate should not be verified. + port: + description: + - Port number through which communication happens with remote peer + gateway host. + type: int + default: 443 + timeout: + description: + - Time after which connection will get terminated. + - It is to be mentioned in seconds. + type: int + default: 120 + state: + description: + - State of the replication pair. + choices: ['present', 'absent'] + default: present + type: str +notes: +- The I(check_mode) is supported. +- In 4.0 the creation of replication pair fails when I(copy_type) is specified as C(OfflineCopy). +''' + +EXAMPLES = r''' + +- name: Get replication pair details + dellemc.powerflex.replication_pair: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + pair_id: "123" + +- name: Create a replication pair + dellemc.powerflex.replication_pair: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + rcg_name: "test_rcg" + pairs: + - source_volume_id: "002" + target_volume_id: "001" + copy_type: "OnlineCopy" + name: "pair1" + +- name: Create a replication pair with target volume name + dellemc.powerflex.replication_pair: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + rcg_name: "test_rcg" + pairs: + - source_volume_name: "src_vol" + target_volume_name: "dest_vol" + copy_type: "OnlineCopy" + name: "pair1" + remote_peer: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + +- name: Pause replication pair + dellemc.powerflex.replication_pair: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + pair_name: "pair1" + pause: True + +- name: Resume replication pair + dellemc.powerflex.replication_pair: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + pair_name: "pair1" + pause: False + +- name: Delete replication pair + dellemc.powerflex.replication_pair: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + pair_name: "pair1" + state: "absent" +''' + +RETURN = r''' +changed: + description: Whether or not the resource has changed. + returned: always + type: bool + sample: 'false' +replication_pair_details: + description: Details of the replication pair. + returned: When replication pair exists + type: dict + contains: + id: + description: The ID of the replication pair. + type: str + name: + description: The name of the replication pair. + type: str + remoteId: + description: The ID of the remote replication pair. + type: str + localVolumeId: + description: The ID of the local volume. + type: str + localVolumeName: + description: The name of the local volume. + type: str + replicationConsistencyGroupId: + description: The ID of the replication consistency group. + type: str + copyType: + description: The copy type of the replication pair. + type: str + initialCopyState: + description: The inital copy state of the replication pair. + type: str + localActivityState: + description: The state of activity of the local replication pair. + type: str + remoteActivityState: + description: The state of activity of the remote replication pair. + type: str + initialCopyPriority: + description: Initial copy priority. + type: int + lifetimeState: + description: Lifetime state of replication pair. + type: int + peerSystemName: + description: Peer system name. + type: int + remoteCapacityInMB: + description: Remote Capacity in MB. + type: int + userRequestedPauseTransmitInitCopy: + description: Value of user requested pause transmit initial copy. + type: int + remoteVolumeId: + description: Remote Volume ID. + type: int + remoteVolumeName: + description: Remote Volume Name. + type: int + sample: { + "copyType": "OnlineCopy", + "id": "23aa0bc900000001", + "initialCopyPriority": -1, + "initialCopyState": "Done", + "lifetimeState": "Normal", + "localActivityState": "RplEnabled", + "localVolumeId": "e2bc1fab00000008", + "localVolumeName": "vol1", + "name": null, + "peerSystemName": null, + "remoteActivityState": "RplEnabled", + "remoteCapacityInMB": 8192, + "remoteId": "a058446700000001", + "remoteVolumeId": "1cda7af20000000d", + "remoteVolumeName": "vol", + "replicationConsistencyGroupId": "e2ce036b00000002", + "userRequestedPauseTransmitInitCopy": false + } +rcg_replication_pairs: + description: Details of the replication pairs of rcg. + returned: When rcg exists + type: list + contains: + id: + description: The ID of the replication pair. + type: str + name: + description: The name of the replication pair. + type: str + remoteId: + description: The ID of the remote replication pair. + type: str + localVolumeId: + description: The ID of the local volume. + type: str + localVolumeName: + description: The name of the local volume. + type: str + replicationConsistencyGroupId: + description: The ID of the replication consistency group. + type: str + copyType: + description: The copy type of the replication pair. + type: str + initialCopyState: + description: The inital copy state of the replication pair. + type: str + localActivityState: + description: The state of activity of the local replication pair. + type: str + remoteActivityState: + description: The state of activity of the remote replication pair. + type: str + initialCopyPriority: + description: Initial copy priority. + type: int + lifetimeState: + description: Lifetime state of replication pair. + type: int + peerSystemName: + description: Peer system name. + type: int + remoteCapacityInMB: + description: Remote Capacity in MB. + type: int + userRequestedPauseTransmitInitCopy: + description: Value of user requested pause transmit initial copy. + type: int + remoteVolumeId: + description: Remote Volume ID. + type: int + remoteVolumeName: + description: Remote Volume Name. + type: int + sample: [{ + "copyType": "OnlineCopy", + "id": "23aa0bc900000001", + "initialCopyPriority": -1, + "initialCopyState": "Done", + "lifetimeState": "Normal", + "localActivityState": "RplEnabled", + "localVolumeId": "e2bc1fab00000008", + "localVolumeName": "vol1", + "name": null, + "peerSystemName": null, + "remoteActivityState": "RplEnabled", + "remoteCapacityInMB": 8192, + "remoteId": "a058446700000001", + "remoteVolumeId": "1cda7af20000000d", + "remoteVolumeName": "vol", + "replicationConsistencyGroupId": "e2ce036b00000002", + "userRequestedPauseTransmitInitCopy": false + }] +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.dellemc.powerflex.plugins.module_utils.storage.dell \ + import utils + +LOG = utils.get_logger('replication_pair') + + +class PowerFlexReplicationPair(object): + """Class with replication pair 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_replication_pair_parameters()) + + mut_ex_args = [['rcg_name', 'rcg_id'], ['pair_id', 'pair_name']] + + # initialize the Ansible module + self.module = AnsibleModule( + argument_spec=self.module_params, + supports_check_mode=True, + mutually_exclusive=mut_ex_args) + + 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_replication_pair(self, pair_name=None, pair_id=None): + """Get replication pair details + :param pair_name: Name of the replication pair + :param pair_id: ID of the replication pair + :return: Replication pair details + """ + name_or_id = pair_id if pair_id else pair_name + try: + pair_details = [] + if pair_id: + pair_details = self.powerflex_conn.replication_pair.get( + filter_fields={'id': pair_id}) + + if pair_name: + pair_details = self.powerflex_conn.replication_pair.get( + filter_fields={'name': pair_name}) + + if pair_details: + pair_details[0].pop('links', None) + pair_details[0]['localVolumeName'] = self.get_volume(pair_details[0]['localVolumeId'], filter_by_name=False)[0]['name'] + pair_details[0]['statistics'] = \ + self.powerflex_conn.replication_pair.get_statistics(pair_details[0]['id']) + return pair_details[0] + return pair_details + except Exception as e: + errormsg = "Failed to get the replication pair {0} with" \ + " error {1}".format(name_or_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def get_volume(self, vol_name_or_id, filter_by_name=True, is_remote=False): + """Get volume details + :param vol_name: ID or name of the volume + :param filter_by_name: If filter details by name or id + :param is_remote: Specifies if source or target volume + :return: Details of volume if exist. + """ + try: + volume_details = [] + filter_field = {'id': vol_name_or_id} + if filter_by_name: + filter_field = {'name': vol_name_or_id} + if is_remote: + self.remote_powerflex_conn = utils.get_powerflex_gateway_host_connection( + self.module.params['remote_peer']) + volume_details = self.remote_powerflex_conn.volume.get( + filter_fields=filter_field) + else: + volume_details = self.powerflex_conn.volume.get( + filter_fields=filter_field) + + if not volume_details: + vol_type = 'Target' if is_remote else 'Source' + self.module.fail_json("%s volume %s does not exist" % (vol_type, vol_name_or_id)) + return volume_details + except Exception as e: + errormsg = "Failed to retrieve volume {0}".format(str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def get_rcg(self, rcg_name=None, rcg_id=None): + """Get rcg details + :param rcg_name: Name of the rcg + :param rcg_id: ID of the rcg + :return: RCG details + """ + name_or_id = rcg_id if rcg_id else rcg_name + try: + rcg_details = {} + if rcg_id: + rcg_details = self.powerflex_conn.replication_consistency_group.get( + filter_fields={'id': rcg_id}) + + if rcg_name: + rcg_details = self.powerflex_conn.replication_consistency_group.get( + filter_fields={'name': rcg_name}) + + if not rcg_details: + self.module.fail_json("RCG %s does not exist" % rcg_name) + + return rcg_details[0] + except Exception as e: + errormsg = "Failed to get the replication consistency group {0} with" \ + " error {1}".format(name_or_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def get_rcg_replication_pairs(self, rcg_id): + """Get rcg replication pair details + :param rcg_id: ID of the rcg + :return: RCG replication pair details + """ + try: + rcg_pairs = self.powerflex_conn.replication_consistency_group.get_replication_pairs(rcg_id) + for rcg_pair in rcg_pairs: + rcg_pair.pop('links', None) + rcg_pair['localVolumeName'] = self.get_volume(rcg_pair['localVolumeId'], filter_by_name=False)[0]['name'] + rcg_pair['replicationConsistencyGroupName'] = self.get_rcg(rcg_id=rcg_pair['replicationConsistencyGroupId'])['name'] + return rcg_pairs + except Exception as e: + errormsg = "Failed to get the replication pairs for replication consistency group {0} with" \ + " error {1}".format(rcg_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def create_replication_pairs(self, rcg_id, rcg_pairs, input_pairs): + """Create replication pairs""" + try: + for pair in input_pairs: + if pair['source_volume_name'] is not None: + pair['source_volume_id'] = self.get_volume(pair['source_volume_name'])[0]['id'] + if pair['target_volume_name'] is not None: + pair['target_volume_id'] = self.get_volume(pair['target_volume_name'], is_remote=True)[0]['id'] + pairs = find_non_existing_pairs(rcg_pairs, input_pairs) + if not pairs: + return False + if not self.module.check_mode: + for pair in pairs: + self.powerflex_conn.replication_pair.add( + source_vol_id=pair['source_volume_id'], + dest_vol_id=pair['target_volume_id'], + rcg_id=rcg_id, + copy_type=pair['copy_type'], + name=pair['name']) + return True + except Exception as e: + errormsg = "Create replication pairs failed with error {0}".format(str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def pause(self, pair_id): + """Pause replication pair + :param pair_id: ID of the replication pair + :return: True if paused + """ + try: + if not self.module.check_mode: + self.powerflex_conn.replication_pair.pause(pair_id) + return True + except Exception as e: + errormsg = "Pause replication pair {0} failed with error {1}".format(pair_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def resume(self, pair_id): + """Resume replication pair + :param pair_id: ID of the replication pair + :return: True if resumed + """ + try: + if not self.module.check_mode: + self.powerflex_conn.replication_pair.resume(pair_id) + return True + except Exception as e: + errormsg = "Resume replication pair {0} failed with error {1}".format(pair_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def delete_pair(self, pair_id): + """Delete replication pair + :param pair_id: Replication pair id. + :return: Boolean indicates if delete pair operation is successful + """ + try: + if not self.module.check_mode: + self.powerflex_conn.replication_pair.remove(pair_id) + return True + + except Exception as e: + errormsg = "Delete replication pair {0} failed with " \ + "error {1}".format(pair_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def validate_input(self, params): + if params['pairs'] is not None: + self.validate_pairs(params) + if not params['rcg_id'] and not params['rcg_name']: + self.module.fail_json(msg="Specify either rcg_id or rcg_name to create replication pair") + self.validate_pause(params) + + def validate_pairs(self, params): + for pair in params['pairs']: + if pair['source_volume_id'] and pair['source_volume_name']: + self.module.fail_json(msg='Specify either source_volume_id or source_volume_name') + if pair['target_volume_id'] and pair['target_volume_name']: + self.module.fail_json(msg='Specify either target_volume_id or target_volume_name') + if pair['target_volume_name'] and params['remote_peer'] is None: + self.module.fail_json(msg='Specify remote_peer with target_volume_name') + + def validate_pause(self, params): + if params['pause'] is not None and (not params['pair_id'] and not params['pair_name']): + self.module.fail_json(msg='Specify either pair_id or pair_name to perform pause or resume of inital copy') + + def validate_pause_or_resume(self, pause, replication_pair_details, pair_id): + if not replication_pair_details: + self.module.fail_json(msg="Specify a valid pair_name or pair_id to perform pause or resume") + return self.perform_pause_or_resume(pause, replication_pair_details, pair_id) + + def perform_pause_or_resume(self, pause, replication_pair_details, pair_id): + changed = False + if pause and replication_pair_details['initialCopyState'] not in ('Paused', 'Done'): + changed = self.pause(pair_id) + elif not pause and replication_pair_details['initialCopyState'] == 'Paused': + changed = self.resume(pair_id) + return changed + + def perform_module_operation(self): + """ + Perform different actions on replication pair based on parameters passed in + the playbook + """ + self.validate_input(self.module.params) + rcg_name = self.module.params['rcg_name'] + rcg_id = self.module.params['rcg_id'] + pair_name = self.module.params['pair_name'] + pair_id = self.module.params['pair_id'] + pairs = self.module.params['pairs'] + pause = self.module.params['pause'] + state = self.module.params['state'] + + changed = False + result = dict( + changed=False, + replication_pair_details=[], + rcg_replication_pairs=[] + ) + + if pair_id or pair_name: + result['replication_pair_details'] = self.get_replication_pair(pair_name, pair_id) + if result['replication_pair_details']: + pair_id = result['replication_pair_details']['id'] + if pairs: + rcg_id = self.get_rcg(rcg_name, rcg_id)['id'] + result['rcg_replication_pairs'] = self.get_rcg_replication_pairs(rcg_id) + changed = self.create_replication_pairs(rcg_id, result['rcg_replication_pairs'], pairs) + if changed: + result['rcg_replication_pairs'] = self.get_rcg_replication_pairs(rcg_id) + if pause is not None: + changed = self.validate_pause_or_resume(pause, result['replication_pair_details'], pair_id) + if state == 'absent' and result['replication_pair_details']: + changed = self.delete_pair(pair_id) + if changed and (pair_id or pair_name): + result['replication_pair_details'] = self.get_replication_pair(pair_name, pair_id) + result['changed'] = changed + self.module.exit_json(**result) + + +def find_non_existing_pairs(rcg_pairs, input_pairs): + for pair in rcg_pairs: + for input_pair in list(input_pairs): + if input_pair['source_volume_id'] == pair['localVolumeId'] and \ + input_pair['target_volume_id'] == pair['remoteVolumeId']: + input_pairs.remove(input_pair) + return input_pairs + + +def get_powerflex_replication_pair_parameters(): + """This method provide parameter required for the replication_consistency_group + module on PowerFlex""" + return dict(pair_id=dict(), pair_name=dict(), pause=dict(type='bool'), + state=dict(choices=['absent', 'present'], default='present'), rcg_id=dict(), rcg_name=dict(), + remote_peer=dict(type='dict', + options=dict(hostname=dict(type='str', aliases=['gateway_host'], required=True), + username=dict(type='str', required=True), + password=dict(type='str', required=True, no_log=True), + validate_certs=dict(type='bool', aliases=['verifycert'], default=True), + port=dict(type='int', default=443), + timeout=dict(type='int', default=120))), + pairs=dict( + type='list', elements='dict', + options=dict(source_volume_name=dict(), + source_volume_id=dict(), + target_volume_name=dict(), + target_volume_id=dict(), + copy_type=dict(required=True, choices=['Identical', 'OnlineCopy', 'OnlineHashCopy', 'OfflineCopy']), + name=dict(),) + )) + + +def main(): + """ Create PowerFlex Replication Consistency Group object and perform actions on it + based on user input from playbook""" + obj = PowerFlexReplicationPair() + obj.perform_module_operation() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/powerflex/plugins/modules/sdc.py b/ansible_collections/dellemc/powerflex/plugins/modules/sdc.py new file mode 100644 index 000000000..a2f05a31b --- /dev/null +++ b/ansible_collections/dellemc/powerflex/plugins/modules/sdc.py @@ -0,0 +1,365 @@ +#!/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 SDCs on Dell Technologies (Dell) PowerFlex""" + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +DOCUMENTATION = r''' +module: sdc +version_added: '1.0.0' +short_description: Manage SDCs on Dell PowerFlex +description: +- Managing SDCs on PowerFlex storage system includes getting details of SDC + and renaming SDC. + +author: +- Akash Shendge (@shenda1) <ansible.team@dell.com> + +extends_documentation_fragment: + - dellemc.powerflex.powerflex + +options: + sdc_name: + description: + - Name of the SDC. + - Specify either I(sdc_name), I(sdc_id) or I(sdc_ip) for get/rename operation. + - 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) for get/rename operation. + - 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) for get/rename operation. + - Mutually exclusive with I(sdc_id) and I(sdc_name). + type: str + sdc_new_name: + description: + - New name of the SDC. Used to rename the SDC. + type: str + state: + description: + - State of the SDC. + choices: ['present', 'absent'] + required: true + type: str +notes: + - The I(check_mode) is not supported. +''' + +EXAMPLES = r''' +- name: Get SDC details using SDC ip + dellemc.powerflex.sdc: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + sdc_ip: "{{sdc_ip}}" + state: "present" + +- name: Rename SDC using SDC name + dellemc.powerflex.sdc: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + sdc_name: "centos_sdc" + sdc_new_name: "centos_sdc_renamed" + state: "present" +''' + +RETURN = r''' +changed: + description: Whether or not the resource has changed. + returned: always + type: bool + sample: 'false' + +sdc_details: + description: Details of the SDC. + returned: When SDC exists + type: dict + contains: + id: + description: The ID of the SDC. + type: str + name: + description: Name of the SDC. + type: str + sdcIp: + description: IP of the SDC. + type: str + osType: + description: OS type of the SDC. + type: str + mapped_volumes: + description: The details of the mapped volumes. + type: list + contains: + id: + description: The ID of the volume. + type: str + name: + description: The name of the volume. + type: str + volumeType: + description: Type of the volume. + type: str + sdcApproved: + description: Indicates whether an SDC has approved access to the + system. + type: bool + sample: { + "id": "07335d3d00000006", + "installedSoftwareVersionInfo": "R3_6.0.0", + "kernelBuildNumber": null, + "kernelVersion": "3.10.0", + "links": [ + { + "href": "/api/instances/Sdc::07335d3d00000006", + "rel": "self" + }, + { + "href": "/api/instances/Sdc::07335d3d00000006/relationships/ + Statistics", + "rel": "/api/Sdc/relationship/Statistics" + }, + { + "href": "/api/instances/Sdc::07335d3d00000006/relationships/ + Volume", + "rel": "/api/Sdc/relationship/Volume" + }, + { + "href": "/api/instances/System::4a54a8ba6df0690f", + "rel": "/api/parent/relationship/systemId" + } + ], + "mapped_volumes": [], + "mdmConnectionState": "Disconnected", + "memoryAllocationFailure": null, + "name": "LGLAP203", + "osType": "Linux", + "peerMdmId": null, + "perfProfile": "HighPerformance", + "sdcApproved": true, + "sdcApprovedIps": null, + "sdcGuid": "F8ECB844-23B8-4629-92BB-B6E49A1744CB", + "sdcIp": "N/A", + "sdcIps": null, + "sdcType": "AppSdc", + "sdrId": null, + "socketAllocationFailure": null, + "softwareVersionInfo": "R3_6.0.0", + "systemId": "4a54a8ba6df0690f", + "versionInfo": "R3_6.0.0" + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.dellemc.powerflex.plugins.module_utils.storage.dell\ + import utils + +LOG = utils.get_logger('sdc') + + +class PowerFlexSdc(object): + """Class with SDC 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_sdc_parameters()) + + mutually_exclusive = [['sdc_id', 'sdc_ip', 'sdc_name']] + + required_one_of = [['sdc_id', 'sdc_ip', 'sdc_name']] + + # 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) + + 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 rename_sdc(self, sdc_id, new_name): + """Rename SDC + :param sdc_id: The ID of the SDC + :param new_name: The new name of the SDC + :return: Boolean indicating if rename operation is successful + """ + + try: + self.powerflex_conn.sdc.rename(sdc_id=sdc_id, name=new_name) + return True + except Exception as e: + errormsg = "Failed to rename SDC %s with error %s" % (sdc_id, + str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def get_mapped_volumes(self, sdc_id): + """Get volumes mapped to SDC + :param sdc_id: The ID of the SDC + :return: List containing volume details mapped to SDC + """ + + try: + resp = self.powerflex_conn.sdc.get_mapped_volumes(sdc_id=sdc_id) + return resp + except Exception as e: + errormsg = "Failed to get the volumes mapped to SDC %s with " \ + "error %s" % (sdc_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def get_sdc(self, sdc_name=None, sdc_ip=None, sdc_id=None): + """Get the SDC Details + :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 dict containing SDC details + """ + + 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 %s" \ + % id_ip_name + LOG.error(error_msg) + return None + sdc_details[0]['mapped_volumes'] = self.get_mapped_volumes( + sdc_details[0]['id']) + return sdc_details[0] + 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 validate_parameters(self, sdc_name=None, sdc_id=None, sdc_ip=None): + """Validate the input parameters""" + + if all(param is None for param in [sdc_name, sdc_id, sdc_ip]): + self.module.fail_json(msg="Please provide sdc_name/sdc_id/sdc_ip " + "with valid input.") + + sdc_identifiers = ['sdc_name', 'sdc_id', 'sdc_ip'] + for param in sdc_identifiers: + 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) + + def perform_module_operation(self): + """ + Perform different actions on SDC based on parameters passed in + the playbook + """ + sdc_name = self.module.params['sdc_name'] + sdc_id = self.module.params['sdc_id'] + sdc_ip = self.module.params['sdc_ip'] + sdc_new_name = self.module.params['sdc_new_name'] + state = self.module.params['state'] + + # result is a dictionary to contain end state and SDC details + changed = False + result = dict( + changed=False, + sdc_details={} + ) + + self.validate_parameters(sdc_name, sdc_id, sdc_ip) + + sdc_details = self.get_sdc(sdc_name=sdc_name, sdc_id=sdc_id, + sdc_ip=sdc_ip) + if sdc_name: + id_ip_name = sdc_name + elif sdc_ip: + id_ip_name = sdc_ip + else: + id_ip_name = sdc_id + + if state == 'present' and not sdc_details: + error_msg = 'Could not find any SDC instance with ' \ + 'identifier %s.' % id_ip_name + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + if state == 'absent' and sdc_details: + error_msg = 'Removal of SDC is not allowed through Ansible ' \ + 'module.' + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + if state == 'present' and sdc_details and sdc_new_name is not None: + if len(sdc_new_name.strip()) == 0: + self.module.fail_json(msg="Please provide valid SDC name.") + + changed = self.rename_sdc(sdc_details['id'], sdc_new_name) + + if changed: + sdc_name = sdc_new_name + + if state == 'present': + result['sdc_details'] = self.get_sdc(sdc_name=sdc_name, + sdc_id=sdc_id, sdc_ip=sdc_ip) + result['changed'] = changed + self.module.exit_json(**result) + + +def get_powerflex_sdc_parameters(): + """This method provide parameter required for the Ansible SDC module on + PowerFlex""" + return dict( + sdc_id=dict(), + sdc_ip=dict(), + sdc_name=dict(), + sdc_new_name=dict(), + state=dict(required=True, type='str', choices=['present', 'absent']) + ) + + +def main(): + """ Create PowerFlex SDC object and perform actions on it + based on user input from playbook""" + obj = PowerFlexSdc() + obj.perform_module_operation() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/powerflex/plugins/modules/sds.py b/ansible_collections/dellemc/powerflex/plugins/modules/sds.py new file mode 100644 index 000000000..91c287769 --- /dev/null +++ b/ansible_collections/dellemc/powerflex/plugins/modules/sds.py @@ -0,0 +1,1160 @@ +#!/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 SDS on Dell Technologies (Dell) PowerFlex""" + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +DOCUMENTATION = r''' +module: sds +version_added: '1.1.0' +short_description: Manage SDS on Dell PowerFlex +description: +- Managing SDS on PowerFlex storage system includes + creating new SDS, getting details of SDS, adding/removing IP to/from SDS, + modifying attributes of SDS, and deleting SDS. +author: +- Rajshree Khare (@khareRajshree) <ansible.team@dell.com> +extends_documentation_fragment: + - dellemc.powerflex.powerflex +options: + sds_name: + description: + - The name of the SDS. + - Mandatory for create operation. + - It is unique across the PowerFlex array. + - Mutually exclusive with I(sds_id). + type: str + sds_id: + description: + - The ID of the SDS. + - Except create operation, all other operations can be performed + using I(sds_id). + - Mutually exclusive with I(sds_name). + type: str + protection_domain_name: + description: + - The name of the protection domain. + - Mutually exclusive with I(protection_domain_id). + type: str + protection_domain_id: + description: + - The ID of the protection domain. + - Mutually exclusive with I(protection_domain_name). + type: str + sds_ip_list: + description: + - Dictionary of IPs and their roles for the SDS. + - At least one IP-role is mandatory while creating a SDS. + - IP-roles can be updated as well. + type: list + elements: dict + suboptions: + ip: + description: + - IP address of the SDS. + type: str + required: true + role: + description: + - Role assigned to the SDS IP address. + choices: ['sdsOnly', 'sdcOnly', 'all'] + type: str + required: true + sds_ip_state: + description: + - State of IP with respect to the SDS. + choices: ['present-in-sds', 'absent-in-sds'] + type: str + rfcache_enabled: + description: + - Whether to enable the Read Flash cache. + type: bool + rmcache_enabled: + description: + - Whether to enable the Read RAM cache. + type: bool + rmcache_size: + description: + - Read RAM cache size (in MB). + - Minimum size is 128 MB. + - Maximum size is 3911 MB. + type: int + sds_new_name: + description: + - SDS new name. + type: str + performance_profile: + description: + - Performance profile to apply to the SDS. + - The HighPerformance profile configures a predefined set of parameters + for very high performance use cases. + - Default value by API is C(HighPerformance). + choices: ['Compact', 'HighPerformance'] + type: str + state: + description: + - State of the SDS. + choices: ['present', 'absent'] + required: true + type: str +notes: + - The maximum limit for the IPs that can be associated with an SDS is 8. + - There needs to be at least 1 IP for SDS communication and 1 for SDC + communication. + - If only 1 IP exists, it must be with role 'all'; else 1 IP + can be with role 'all'and other IPs with role 'sdcOnly'; or 1 IP must be + with role 'sdsOnly' and others with role 'sdcOnly'. + - There can be 1 or more IPs with role 'sdcOnly'. + - There must be only 1 IP with SDS role (either with role 'all' or + 'sdsOnly'). + - SDS can be created with RF cache disabled, but, be aware that the RF cache + is not always updated. In this case, the user should re-try the operation. + - The I(check_mode) is not supported. +''' + +EXAMPLES = r''' +- name: Create SDS + dellemc.powerflex.sds: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + sds_name: "node0" + protection_domain_name: "domain1" + sds_ip_list: + - ip: "198.10.xxx.xxx" + role: "all" + sds_ip_state: "present-in-sds" + state: "present" + +- name: Create SDS with all parameters + dellemc.powerflex.sds: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + sds_name: "node1" + protection_domain_name: "domain1" + sds_ip_list: + - ip: "198.10.xxx.xxx" + role: "sdcOnly" + sds_ip_state: "present-in-sds" + rmcache_enabled: true + rmcache_size: 128 + performance_profile: "HighPerformance" + state: "present" + +- name: Get SDS details using name + dellemc.powerflex.sds: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + sds_name: "node0" + state: "present" + +- name: Get SDS details using ID + dellemc.powerflex.sds: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + sds_id: "5718253c00000004" + state: "present" + +- name: Modify SDS attributes using name + dellemc.powerflex.sds: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + sds_name: "node0" + sds_new_name: "node0_new" + rfcache_enabled: true + rmcache_enabled: true + rmcache_size: 256 + performance_profile: "HighPerformance" + state: "present" + +- name: Modify SDS attributes using ID + dellemc.powerflex.sds: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + sds_id: "5718253c00000004" + sds_new_name: "node0_new" + rfcache_enabled: true + rmcache_enabled: true + rmcache_size: 256 + performance_profile: "HighPerformance" + state: "present" + +- name: Add IP and role to an SDS + dellemc.powerflex.sds: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + sds_name: "node0" + sds_ip_list: + - ip: "198.10.xxx.xxx" + role: "sdcOnly" + sds_ip_state: "present-in-sds" + state: "present" + +- name: Remove IP and role from an SDS + dellemc.powerflex.sds: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + sds_name: "node0" + sds_ip_list: + - ip: "198.10.xxx.xxx" + role: "sdcOnly" + sds_ip_state: "absent-in-sds" + state: "present" + +- name: Delete SDS using name + dellemc.powerflex.sds: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + sds_name: "node0" + state: "absent" + +- name: Delete SDS using ID + dellemc.powerflex.sds: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + sds_id: "5718253c00000004" + state: "absent" +''' + +RETURN = r''' +changed: + description: Whether or not the resource has changed. + returned: always + type: bool + sample: 'false' +sds_details: + description: Details of the SDS. + returned: When SDS exists + type: dict + contains: + authenticationError: + description: Indicates authentication error. + type: str + certificateInfo: + description: Information about certificate. + type: str + configuredDrlMode: + description: Configured DRL mode. + type: str + drlMode: + description: DRL mode. + type: str + faultSetId: + description: Fault set ID. + type: str + fglMetadataCacheSize: + description: FGL metadata cache size. + type: int + fglMetadataCacheState: + description: FGL metadata cache state. + type: str + fglNumConcurrentWrites: + description: FGL concurrent writes. + type: int + id: + description: SDS ID. + type: str + ipList: + description: SDS IP list. + type: list + contains: + ip: + description: IP present in the SDS. + type: str + role: + description: Role of the SDS IP. + type: str + lastUpgradeTime: + description: Last time SDS was upgraded. + type: str + links: + description: SDS links. + type: list + contains: + href: + description: SDS instance URL. + type: str + rel: + description: SDS's relationship with different entities. + type: str + maintenanceState: + description: Maintenance state. + type: str + maintenanceType: + description: Maintenance type. + type: str + mdmConnectionState: + description: MDM connection state. + type: str + membershipState: + description: Membership state. + type: str + name: + description: Name of the SDS. + type: str + numOfIoBuffers: + description: Number of IO buffers. + type: int + numRestarts: + description: Number of restarts. + type: int + onVmWare: + description: Presence on VMware. + type: bool + perfProfile: + description: Performance profile. + type: str + port: + description: SDS port. + type: int + protectionDomainId: + description: Protection Domain ID. + type: str + protectionDomainName: + description: Protection Domain Name. + type: str + raidControllers: + description: Number of RAID controllers. + type: int + rfcacheEnabled: + description: Whether RF cache is enabled or not. + type: bool + rfcacheErrorApiVersionMismatch: + description: RF cache error for API version mismatch. + type: bool + rfcacheErrorDeviceDoesNotExist: + description: RF cache error for device does not exist. + type: bool + rfcacheErrorInconsistentCacheConfiguration: + description: RF cache error for inconsistent cache configuration. + type: bool + rfcacheErrorInconsistentSourceConfiguration: + description: RF cache error for inconsistent source configuration. + type: bool + rfcacheErrorInvalidDriverPath: + description: RF cache error for invalid driver path. + type: bool + rfcacheErrorLowResources: + description: RF cache error for low resources. + type: bool + rmcacheEnabled: + description: Whether Read RAM cache is enabled or not. + type: bool + rmcacheFrozen: + description: RM cache frozen. + type: bool + rmcacheMemoryAllocationState: + description: RM cache memory allocation state. + type: bool + rmcacheSizeInKb: + description: RM cache size in KB. + type: int + rmcacheSizeInMb: + description: RM cache size in MB. + type: int + sdsConfigurationFailure: + description: SDS configuration failure. + type: str + sdsDecoupled: + description: SDS decoupled. + type: str + sdsReceiveBufferAllocationFailures: + description: SDS receive buffer allocation failures. + type: str + sdsState: + description: SDS state. + type: str + softwareVersionInfo: + description: SDS software version information. + type: str + sample: { + "authenticationError": "None", + "certificateInfo": null, + "configuredDrlMode": "Volatile", + "drlMode": "Volatile", + "faultSetId": null, + "fglMetadataCacheSize": 0, + "fglMetadataCacheState": "Disabled", + "fglNumConcurrentWrites": 1000, + "id": "8f3bb0cc00000002", + "ipList": [ + { + "ip": "10.47.xxx.xxx", + "role": "all" + } + ], + "lastUpgradeTime": 0, + "links": [ + { + "href": "/api/instances/Sds::8f3bb0cc00000002", + "rel": "self" + }, + { + "href": "/api/instances/Sds::8f3bb0cc00000002/relationships + /Statistics", + "rel": "/api/Sds/relationship/Statistics" + }, + { + "href": "/api/instances/Sds::8f3bb0cc00000002/relationships + /SpSds", + "rel": "/api/Sds/relationship/SpSds" + }, + { + "href": "/api/instances/Sds::8f3bb0cc00000002/relationships + /Device", + "rel": "/api/Sds/relationship/Device" + }, + { + "href": "/api/instances/ProtectionDomain::9300c1f900000000", + "rel": "/api/parent/relationship/protectionDomainId" + } + ], + "maintenanceState": "NoMaintenance", + "maintenanceType": "NoMaintenance", + "mdmConnectionState": "Connected", + "membershipState": "Joined", + "name": "node0", + "numOfIoBuffers": null, + "numRestarts": 2, + "onVmWare": true, + "perfProfile": "HighPerformance", + "port": 7072, + "protectionDomainId": "9300c1f900000000", + "protectionDomainName": "domain1", + "raidControllers": null, + "rfcacheEnabled": true, + "rfcacheErrorApiVersionMismatch": false, + "rfcacheErrorDeviceDoesNotExist": false, + "rfcacheErrorInconsistentCacheConfiguration": false, + "rfcacheErrorInconsistentSourceConfiguration": false, + "rfcacheErrorInvalidDriverPath": false, + "rfcacheErrorLowResources": false, + "rmcacheEnabled": true, + "rmcacheFrozen": false, + "rmcacheMemoryAllocationState": "AllocationPending", + "rmcacheSizeInKb": 131072, + "rmcacheSizeInMb": 128, + "sdsConfigurationFailure": null, + "sdsDecoupled": null, + "sdsReceiveBufferAllocationFailures": null, + "sdsState": "Normal", + "softwareVersionInfo": "R3_6.0.0" + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.dellemc.powerflex.plugins.module_utils.storage.dell\ + import utils +import copy + +LOG = utils.get_logger('sds') + + +class PowerFlexSDS(object): + """Class with SDS 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_sds_parameters()) + + mut_ex_args = [['sds_name', 'sds_id'], + ['protection_domain_name', 'protection_domain_id']] + + required_together_args = [['sds_ip_list', 'sds_ip_state']] + + required_one_of_args = [['sds_name', 'sds_id']] + + # initialize the Ansible module + self.module = AnsibleModule( + argument_spec=self.module_params, + supports_check_mode=False, + mutually_exclusive=mut_ex_args, + required_together=required_together_args, + required_one_of=required_one_of_args) + + 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 validate_rmcache_size_parameter(self, rmcache_enabled, rmcache_size): + """Validate the input parameters""" + + # RM cache size cannot be set only when RM cache is enabled + if rmcache_size is not None and rmcache_enabled is False: + error_msg = "RM cache size can be set only when RM cache " \ + "is enabled, please enable it along with RM " \ + "cache size." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def validate_ip_parameter(self, sds_ip_list): + """Validate the input parameters""" + + if sds_ip_list is None or len(sds_ip_list) == 0: + error_msg = "Provide valid values for " \ + "sds_ip_list as 'ip' and 'role' for Create/Modify " \ + "operations." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def get_sds_details(self, sds_name=None, sds_id=None): + """Get SDS details + :param sds_name: Name of the SDS + :type sds_name: str + :param sds_id: ID of the SDS + :type sds_id: str + :return: Details of SDS if it exist + :rtype: dict + """ + + id_or_name = sds_id if sds_id else sds_name + + try: + if sds_name: + sds_details = self.powerflex_conn.sds.get( + filter_fields={'name': sds_name}) + else: + sds_details = self.powerflex_conn.sds.get( + filter_fields={'id': sds_id}) + + if len(sds_details) == 0: + msg = "SDS with identifier '%s' not found" % id_or_name + LOG.info(msg) + return None + + return sds_details[0] + + except Exception as e: + error_msg = "Failed to get the SDS '%s' with error '%s'" \ + % (id_or_name, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def get_protection_domain(self, protection_domain_name=None, + protection_domain_id=None): + """Get protection domain details + :param protection_domain_name: Name of the protection domain + :param protection_domain_id: ID of the protection domain + :return: Protection domain details + :rtype: dict + """ + name_or_id = protection_domain_id if protection_domain_id \ + else protection_domain_name + try: + pd_details = None + if protection_domain_id: + pd_details = self.powerflex_conn.protection_domain.get( + filter_fields={'id': protection_domain_id}) + + if protection_domain_name: + pd_details = self.powerflex_conn.protection_domain.get( + filter_fields={'name': protection_domain_name}) + + if not pd_details: + error_msg = "Unable to find the protection domain with " \ + "'%s'. Please enter a valid protection domain " \ + "name/id." % name_or_id + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + return pd_details[0] + + except Exception as e: + error_msg = "Failed to get the protection domain '%s' with " \ + "error '%s'" % (name_or_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def restructure_ip_role_dict(self, sds_ip_list): + """Restructure IP role dict + :param sds_ip_list: List of one or more IP addresses and + their roles + :type sds_ip_list: list[dict] + :return: List of one or more IP addresses and their roles + :rtype: list[dict] + """ + new_sds_ip_list = [] + for item in sds_ip_list: + new_sds_ip_list.append({"SdsIp": item}) + return new_sds_ip_list + + def create_sds(self, protection_domain_id, sds_ip_list, sds_ip_state, + sds_name, rmcache_enabled=None, rmcache_size=None): + """Create SDS + :param protection_domain_id: ID of the Protection Domain + :type protection_domain_id: str + :param sds_ip_list: List of one or more IP addresses associated + with the SDS over which the data will be + transferred. + :type sds_ip_list: list[dict] + :param sds_ip_state: SDS IP state + :type sds_ip_state: str + :param sds_name: SDS name + :type sds_name: str + :param rmcache_enabled: Whether to enable the Read RAM cache + :type rmcache_enabled: bool + :param rmcache_size: Read RAM cache size (in MB) + :type rmcache_size: int + :return: Boolean indicating if create operation is successful + """ + try: + if sds_name is None or len(sds_name.strip()) == 0: + error_msg = "Please provide valid sds_name value for " \ + "creation of SDS." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + if protection_domain_id is None: + error_msg = "Protection Domain is a mandatory parameter " \ + "for creating a SDS. Please enter a valid value." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + if sds_ip_list is None or len(sds_ip_list) == 0: + error_msg = "Please provide valid sds_ip_list values for " \ + "creation of SDS." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + if sds_ip_state is not None and sds_ip_state != "present-in-sds": + error_msg = "Incorrect IP state given for creation of SDS." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + # Restructure IP-role parameter format + if sds_ip_list and sds_ip_state == "present-in-sds": + sds_ip_list = self.restructure_ip_role_dict(sds_ip_list) + + if rmcache_size is not None: + self.validate_rmcache_size_parameter(rmcache_enabled, + rmcache_size) + # set rmcache size in KB + rmcache_size = rmcache_size * 1024 + + create_params = ("protection_domain_id: %s," + " sds_ip_list: %s," + " sds_name: %s," + " rmcache_enabled: %s, " + " rmcache_size_KB: %s" + % (protection_domain_id, sds_ip_list, + sds_name, rmcache_enabled, rmcache_size)) + LOG.info("Creating SDS with params: %s", create_params) + + self.powerflex_conn.sds.create( + protection_domain_id=protection_domain_id, + sds_ips=sds_ip_list, + name=sds_name, + rmcache_enabled=rmcache_enabled, + rmcache_size_in_kb=rmcache_size) + return True + + except Exception as e: + error_msg = "Create SDS '%s' operation failed with error '%s'" \ + % (sds_name, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def to_modify(self, sds_details, sds_new_name, rfcache_enabled, + rmcache_enabled, rmcache_size, performance_profile): + """ + :param sds_details: Details of the SDS + :type sds_details: dict + :param sds_new_name: New name of SDS + :type sds_new_name: str + :param rfcache_enabled: Whether to enable the Read Flash cache + :type rfcache_enabled: bool + :param rmcache_enabled: Whether to enable the Read RAM cache + :type rmcache_enabled: bool + :param rmcache_size: Read RAM cache size (in MB) + :type rmcache_size: int + :param performance_profile: Performance profile to apply to the SDS + :type performance_profile: str + :return: Dictionary containing the attributes of SDS which are to be + updated + :rtype: dict + """ + modify_dict = {} + + if sds_new_name is not None: + if len(sds_new_name.strip()) == 0: + error_msg = "Please provide valid SDS name." + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + if sds_new_name != sds_details['name']: + modify_dict['name'] = sds_new_name + + if rfcache_enabled is not None and \ + sds_details['rfcacheEnabled'] != rfcache_enabled: + modify_dict['rfcacheEnabled'] = rfcache_enabled + + if rmcache_enabled is not None and \ + sds_details['rmcacheEnabled'] != rmcache_enabled: + modify_dict['rmcacheEnabled'] = rmcache_enabled + + if rmcache_size is not None: + self.validate_rmcache_size_parameter(rmcache_enabled, + rmcache_size) + exisitng_size_mb = sds_details['rmcacheSizeInKb'] / 1024 + if rmcache_size != exisitng_size_mb: + if sds_details['rmcacheEnabled']: + modify_dict['rmcacheSizeInMB'] = rmcache_size + else: + error_msg = "Failed to update RM cache size for the " \ + "SDS '%s' as RM cache is disabled " \ + "previously, please enable it before " \ + "setting the size." \ + % sds_details['name'] + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + if performance_profile is not None and \ + sds_details['perfProfile'] != performance_profile: + modify_dict['perfProfile'] = performance_profile + + return modify_dict + + def modify_sds_attributes(self, sds_id, modify_dict, + create_flag=False): + """Modify SDS attributes + :param sds_id: SDS ID + :type sds_id: str + :param modify_dict: Dictionary containing the attributes of SDS + which are to be updated + :type modify_dict: dict + :param create_flag: Flag to indicate whether modify operation is + followed by create operation or not + :type create_flag: bool + :return: Boolean indicating if the operation is successful + """ + try: + msg = "Dictionary containing attributes which are to be" \ + " updated is '%s'." % (str(modify_dict)) + LOG.info(msg) + + if 'name' in modify_dict: + self.powerflex_conn.sds.rename(sds_id, modify_dict['name']) + msg = "The name of the SDS is updated to '%s' successfully." \ + % modify_dict['name'] + LOG.info(msg) + + if 'rfcacheEnabled' in modify_dict: + self.powerflex_conn.sds.set_rfcache_enabled( + sds_id, modify_dict['rfcacheEnabled']) + msg = "The use RFcache is updated to '%s' successfully." \ + % modify_dict['rfcacheEnabled'] + LOG.info(msg) + + if 'rmcacheEnabled' in modify_dict: + self.powerflex_conn.sds.set_rmcache_enabled( + sds_id, modify_dict['rmcacheEnabled']) + msg = "The use RMcache is updated to '%s' successfully." \ + % modify_dict['rmcacheEnabled'] + LOG.info(msg) + + if 'rmcacheSizeInMB' in modify_dict: + self.powerflex_conn.sds.set_rmcache_size( + sds_id, modify_dict['rmcacheSizeInMB']) + msg = "The size of RMcache is updated to '%s' successfully." \ + % modify_dict['rmcacheSizeInMB'] + LOG.info(msg) + + if 'perfProfile' in modify_dict: + self.powerflex_conn.sds.set_performance_parameters( + sds_id, modify_dict['perfProfile']) + msg = "The performance profile is updated to '%s'" \ + % modify_dict['perfProfile'] + LOG.info(msg) + + return True + except Exception as e: + if create_flag: + error_msg = "Create SDS is successful, but failed to update" \ + " the SDS '%s' with error '%s'"\ + % (sds_id, str(e)) + else: + error_msg = "Failed to update the SDS '%s' with error '%s'" \ + % (sds_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def identify_ip_role(self, sds_ip_list, sds_details, sds_ip_state): + """Identify IPs before addition/removal + :param sds_ip_list: List of one or more IP addresses and + their roles + :type sds_ip_list: list[dict] + :param sds_details: SDS details + :type sds_details: dict + :param sds_ip_state: State of IP in SDS + :type sds_ip_state: str + :return: List containing the key-value pairs of IP-role for an + SDS + :rtype: list[dict] + """ + existing_ip_role_list = sds_details['ipList'] + + # identify IPs to add or roles to update + if sds_ip_state == "present-in-sds": + update_role = [] + ips_to_add = [] + + # identify IPs to add + existing_ip_list = [] + if existing_ip_role_list: + for ip in existing_ip_role_list: + existing_ip_list.append(ip['ip']) + for given_ip in sds_ip_list: + ip = given_ip['ip'] + if ip not in existing_ip_list: + ips_to_add.append(given_ip) + LOG.info("IP(s) to be added: %s", ips_to_add) + + if len(ips_to_add) != 0: + for ip in ips_to_add: + sds_ip_list.remove(ip) + + # identify IPs whose role needs to be updated + update_role = [ip for ip in sds_ip_list + if ip not in existing_ip_role_list] + LOG.info("Role update needed for: %s", update_role) + + return ips_to_add, update_role + + elif sds_ip_state == "absent-in-sds": + # identify IPs to remove + ips_to_remove = [ip for ip in existing_ip_role_list + if ip in sds_ip_list] + if len(ips_to_remove) != 0: + LOG.info("IP(s) to remove: %s", ips_to_remove) + return ips_to_remove + else: + LOG.info("IP(s) do not exists.") + return False, None + + def add_ip(self, sds_id, sds_ip_list): + """Add IP to SDS + :param sds_id: SDS ID + :type sds_id: str + :param sds_ip_list: List of one or more IP addresses and + their roles + :type sds_ip_list: list[dict] + :return: Boolean indicating if add IP operation is successful + """ + try: + for ip in sds_ip_list: + LOG.info("IP to add: %s", ip) + self.powerflex_conn.sds.add_ip(sds_id=sds_id, sds_ip=ip) + LOG.info("IP added successfully.") + return True + except Exception as e: + error_msg = "Add IP to SDS '%s' operation failed with " \ + "error '%s'" % (sds_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def update_role(self, sds_id, sds_ip_list): + """Update IP's role for an SDS + :param sds_id: SDS ID + :type sds_id: str + :param sds_ip_list: List of one or more IP addresses and + their roles + :type sds_ip_list: list[dict] + :return: Boolean indicating if add IP operation is successful + """ + try: + LOG.info("Role updates for: %s", sds_ip_list) + if len(sds_ip_list) != 0: + for ip in sds_ip_list: + LOG.info("ip-role: %s", ip) + self.powerflex_conn.sds.set_ip_role(sds_id, ip['ip'], + ip['role']) + msg = "The role '%s' for IP '%s' is updated " \ + "successfully." % (ip['role'], ip['ip']) + LOG.info(msg) + return True + except Exception as e: + error_msg = "Update role of IP for SDS '%s' operation failed " \ + "with error '%s'" % (sds_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def remove_ip(self, sds_id, sds_ip_list): + """Remove IP from SDS + :param sds_id: SDS ID + :type sds_id: str + :param sds_ip_list: List of one or more IP addresses and + their roles. + :type sds_ip_list: list[dict] + :return: Boolean indicating if remove IP operation is successful + """ + try: + for ip in sds_ip_list: + LOG.info("IP to remove: %s", ip) + self.powerflex_conn.sds.remove_ip(sds_id=sds_id, ip=ip['ip']) + LOG.info("IP removed successfully.") + return True + except Exception as e: + error_msg = "Remove IP from SDS '%s' operation failed with " \ + "error '%s'" % (sds_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def delete_sds(self, sds_id): + """Delete SDS + :param sds_id: SDS ID + :type sds_id: str + :return: Boolean indicating if delete operation is successful + """ + try: + self.powerflex_conn.sds.delete(sds_id) + return True + except Exception as e: + error_msg = "Delete SDS '%s' operation failed with error '%s'" \ + % (sds_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + def perform_module_operation(self): + """ + Perform different actions on SDS based on parameters passed in + the playbook + """ + sds_name = self.module.params['sds_name'] + sds_id = self.module.params['sds_id'] + sds_new_name = self.module.params['sds_new_name'] + protection_domain_name = self.module.params['protection_domain_name'] + protection_domain_id = self.module.params['protection_domain_id'] + rfcache_enabled = self.module.params['rfcache_enabled'] + rmcache_enabled = self.module.params['rmcache_enabled'] + rmcache_size = self.module.params['rmcache_size'] + sds_ip_list = copy.deepcopy(self.module.params['sds_ip_list']) + sds_ip_state = self.module.params['sds_ip_state'] + performance_profile = self.module.params['performance_profile'] + state = self.module.params['state'] + + # result is a dictionary to contain end state and SDS details + changed = False + result = dict( + changed=False, + sds_details={} + ) + + # get SDS details + sds_details = self.get_sds_details(sds_name, sds_id) + if sds_details: + sds_id = sds_details['id'] + msg = "Fetched the SDS details %s" % (str(sds_details)) + LOG.info(msg) + + # get Protection Domain ID from name + if protection_domain_name: + pd_details = self.get_protection_domain(protection_domain_name) + if pd_details: + protection_domain_id = pd_details['id'] + msg = "Fetched the protection domain details with id '%s', " \ + "name '%s'" % (protection_domain_id, protection_domain_name) + LOG.info(msg) + + # create operation + create_changed = False + if state == 'present' and not sds_details: + if sds_id: + error_msg = "Creation of SDS is allowed using sds_name " \ + "only, sds_id given." + LOG.info(error_msg) + self.module.fail_json(msg=error_msg) + + if sds_new_name: + error_msg = "sds_new_name parameter is not supported " \ + "during creation of a SDS. Try renaming the " \ + "SDS after the creation." + LOG.info(error_msg) + self.module.fail_json(msg=error_msg) + + self.validate_ip_parameter(sds_ip_list) + + create_changed = self.create_sds(protection_domain_id, + sds_ip_list, sds_ip_state, + sds_name, rmcache_enabled, + rmcache_size) + if create_changed: + sds_details = self.get_sds_details(sds_name) + sds_id = sds_details['id'] + msg = "SDS created successfully, fetched SDS details %s"\ + % (str(sds_details)) + LOG.info(msg) + + # checking if basic SDS parameters are modified or not + modify_dict = {} + if sds_details and state == 'present': + modify_dict = self.to_modify(sds_details, sds_new_name, + rfcache_enabled, rmcache_enabled, + rmcache_size, performance_profile) + msg = "Parameters to be modified are as follows: %s"\ + % (str(modify_dict)) + LOG.info(msg) + + # modify operation + modify_changed = False + if modify_dict and state == 'present': + LOG.info("Modify SDS params.") + modify_changed = self.modify_sds_attributes(sds_id, modify_dict, + create_changed) + + # get updated SDS details + sds_details = self.get_sds_details(sds_id=sds_id) + + # add IPs to SDS + # update IP's role for an SDS + add_ip_changed = False + update_role_changed = False + if sds_details and state == 'present' \ + and sds_ip_state == "present-in-sds": + self.validate_ip_parameter(sds_ip_list) + ips_to_add, roles_to_update = self.identify_ip_role( + sds_ip_list, sds_details, sds_ip_state) + if ips_to_add: + add_ip_changed = self.add_ip(sds_id, ips_to_add) + if roles_to_update: + update_role_changed = self.update_role(sds_id, + roles_to_update) + + # remove IPs from SDS + remove_ip_changed = False + if sds_details and state == 'present' \ + and sds_ip_state == "absent-in-sds": + self.validate_ip_parameter(sds_ip_list) + ips_to_remove = self.identify_ip_role(sds_ip_list, sds_details, + sds_ip_state) + if ips_to_remove: + remove_ip_changed = self.remove_ip(sds_id, ips_to_remove) + + # delete operation + delete_changed = False + if sds_details and state == 'absent': + delete_changed = self.delete_sds(sds_id) + + if create_changed or modify_changed or add_ip_changed \ + or update_role_changed or remove_ip_changed or delete_changed: + changed = True + + # Returning the updated SDS details + if state == 'present': + sds_details = self.show_output(sds_id) + result['sds_details'] = sds_details + result['changed'] = changed + self.module.exit_json(**result) + + def show_output(self, sds_id): + """Show SDS details + :param sds_id: ID of the SDS + :type sds_id: str + :return: Details of SDS + :rtype: dict + """ + + try: + sds_details = self.powerflex_conn.sds.get( + filter_fields={'id': sds_id}) + + if len(sds_details) == 0: + msg = "SDS with identifier '%s' not found" % sds_id + LOG.error(msg) + return None + + # Append protection domain name + if 'protectionDomainId' in sds_details[0] \ + and sds_details[0]['protectionDomainId']: + pd_details = self.get_protection_domain( + protection_domain_id=sds_details[0]['protectionDomainId']) + sds_details[0]['protectionDomainName'] = pd_details['name'] + + # Append rmcache size in MB + if 'rmcacheSizeInKb' in sds_details[0] \ + and sds_details[0]['rmcacheSizeInKb']: + rmcache_size_mb = sds_details[0]['rmcacheSizeInKb'] / 1024 + sds_details[0]['rmcacheSizeInMb'] = int(rmcache_size_mb) + + return sds_details[0] + + except Exception as e: + error_msg = "Failed to get the SDS '%s' with error '%s'"\ + % (sds_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + +def get_powerflex_sds_parameters(): + """This method provide parameter required for the SDS module on + PowerFlex""" + return dict( + sds_name=dict(), + sds_id=dict(), + sds_new_name=dict(), + protection_domain_name=dict(), + protection_domain_id=dict(), + sds_ip_list=dict( + type='list', elements='dict', options=dict( + ip=dict(required=True), + role=dict(required=True, choices=['all', 'sdsOnly', + 'sdcOnly']) + ) + ), + sds_ip_state=dict(choices=['present-in-sds', 'absent-in-sds']), + rfcache_enabled=dict(type='bool'), + rmcache_enabled=dict(type='bool'), + rmcache_size=dict(type='int'), + performance_profile=dict(choices=['Compact', 'HighPerformance']), + state=dict(required=True, type='str', choices=['present', 'absent']) + ) + + +def main(): + """ Create PowerFlex SDS object and perform actions on it + based on user input from playbook""" + obj = PowerFlexSDS() + obj.perform_module_operation() + + +if __name__ == '__main__': + main() 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 000000000..69caea075 --- /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() diff --git a/ansible_collections/dellemc/powerflex/plugins/modules/storagepool.py b/ansible_collections/dellemc/powerflex/plugins/modules/storagepool.py new file mode 100644 index 000000000..ca343212d --- /dev/null +++ b/ansible_collections/dellemc/powerflex/plugins/modules/storagepool.py @@ -0,0 +1,914 @@ +#!/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 Dell Technologies (Dell) PowerFlex storage pool""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: storagepool + +version_added: '1.0.0' + +short_description: Managing Dell PowerFlex storage pool + +description: +- Dell PowerFlex storage pool module includes getting the details of + storage pool, creating a new storage pool, and modifying the attribute of + a storage pool. + +extends_documentation_fragment: + - dellemc.powerflex.powerflex + +author: +- Arindam Datta (@dattaarindam) <ansible.team@dell.com> +- P Srinivas Rao (@srinivas-rao5) <ansible.team@dell.com> + +options: + storage_pool_name: + description: + - The name of the storage pool. + - If more than one storage pool is found with the same name then + protection domain id/name is required to perform the task. + - Mutually exclusive with I(storage_pool_id). + type: str + storage_pool_id: + description: + - The id of the storage pool. + - It is auto generated, hence should not be provided during + creation of a storage pool. + - Mutually exclusive with I(storage_pool_name). + type: str + protection_domain_name: + description: + - The name of the protection domain. + - During creation of a pool, either protection domain name or id must be + mentioned. + - Mutually exclusive with I(protection_domain_id). + type: str + protection_domain_id: + description: + - The id of the protection domain. + - During creation of a pool, either protection domain name or id must + be mentioned. + - Mutually exclusive with I(protection_domain_name). + type: str + media_type: + description: + - Type of devices in the storage pool. + type: str + choices: ['HDD', 'SSD', 'TRANSITIONAL'] + storage_pool_new_name: + description: + - New name for the storage pool can be provided. + - This parameter is used for renaming the storage pool. + type: str + use_rfcache: + description: + - Enable/Disable RFcache on a specific storage pool. + type: bool + use_rmcache: + description: + - Enable/Disable RMcache on a specific storage pool. + type: bool + state: + description: + - State of the storage pool. + type: str + choices: ["present", "absent"] + required: true +notes: + - TRANSITIONAL media type is supported only during modification. + - The I(check_mode) is not supported. +''' + +EXAMPLES = r''' + +- name: Get the details of storage pool by name + dellemc.powerflex.storagepool: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + storage_pool_name: "sample_pool_name" + protection_domain_name: "sample_protection_domain" + state: "present" + +- name: Get the details of storage pool by id + dellemc.powerflex.storagepool: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + storage_pool_id: "abcd1234ab12r" + state: "present" + +- name: Create a new storage pool by name + dellemc.powerflex.storagepool: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + storage_pool_name: "ansible_test_pool" + protection_domain_id: "1c957da800000000" + media_type: "HDD" + state: "present" + +- name: Modify a storage pool by name + dellemc.powerflex.storagepool: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + storage_pool_name: "ansible_test_pool" + protection_domain_id: "1c957da800000000" + use_rmcache: True + use_rfcache: True + state: "present" + +- name: Rename storage pool by id + dellemc.powerflex.storagepool: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + storage_pool_id: "abcd1234ab12r" + storage_pool_new_name: "new_ansible_pool" + state: "present" +''' + +RETURN = r''' +changed: + description: Whether or not the resource has changed. + returned: always + type: bool + sample: 'false' +storage_pool_details: + description: Details of the storage pool. + returned: When storage pool exists + type: dict + contains: + mediaType: + description: Type of devices in the storage pool. + type: str + useRfcache: + description: Enable/Disable RFcache on a specific storage pool. + type: bool + useRmcache: + description: Enable/Disable RMcache on a specific storage pool. + type: bool + id: + description: ID of the storage pool under protection domain. + type: str + name: + description: Name of the storage pool under protection domain. + type: str + protectionDomainId: + description: ID of the protection domain in which pool resides. + type: str + protectionDomainName: + description: Name of the protection domain in which pool resides. + type: str + "statistics": + description: Statistics details of the storage pool. + type: dict + contains: + "capacityInUseInKb": + description: Total capacity of the storage pool. + type: str + "unusedCapacityInKb": + description: Unused capacity of the storage pool. + type: str + "deviceIds": + description: Device Ids of the storage pool. + type: list + sample: { + "addressSpaceUsage": "Normal", + "addressSpaceUsageType": "DeviceCapacityLimit", + "backgroundScannerBWLimitKBps": 3072, + "backgroundScannerMode": "DataComparison", + "bgScannerCompareErrorAction": "ReportAndFix", + "bgScannerReadErrorAction": "ReportAndFix", + "capacityAlertCriticalThreshold": 90, + "capacityAlertHighThreshold": 80, + "capacityUsageState": "Normal", + "capacityUsageType": "NetCapacity", + "checksumEnabled": false, + "compressionMethod": "Invalid", + "dataLayout": "MediumGranularity", + "externalAccelerationType": "None", + "fglAccpId": null, + "fglExtraCapacity": null, + "fglMaxCompressionRatio": null, + "fglMetadataSizeXx100": null, + "fglNvdimmMetadataAmortizationX100": null, + "fglNvdimmWriteCacheSizeInMb": null, + "fglOverProvisioningFactor": null, + "fglPerfProfile": null, + "fglWriteAtomicitySize": null, + "fragmentationEnabled": true, + "id": "e0d8f6c900000000", + "links": [ + { + "href": "/api/instances/StoragePool::e0d8f6c900000000", + "rel": "self" + }, + { + "href": "/api/instances/StoragePool::e0d8f6c900000000 + /relationships/Statistics", + "rel": "/api/StoragePool/relationship/Statistics" + }, + { + "href": "/api/instances/StoragePool::e0d8f6c900000000 + /relationships/SpSds", + "rel": "/api/StoragePool/relationship/SpSds" + }, + { + "href": "/api/instances/StoragePool::e0d8f6c900000000 + /relationships/Volume", + "rel": "/api/StoragePool/relationship/Volume" + }, + { + "href": "/api/instances/StoragePool::e0d8f6c900000000 + /relationships/Device", + "rel": "/api/StoragePool/relationship/Device" + }, + { + "href": "/api/instances/StoragePool::e0d8f6c900000000 + /relationships/VTree", + "rel": "/api/StoragePool/relationship/VTree" + }, + { + "href": "/api/instances/ProtectionDomain::9300c1f900000000", + "rel": "/api/parent/relationship/protectionDomainId" + } + ], + "statistics": { + "BackgroundScannedInMB": 3466920, + "activeBckRebuildCapacityInKb": 0, + "activeEnterProtectedMaintenanceModeCapacityInKb": 0, + "aggregateCompressionLevel": "Uncompressed", + "atRestCapacityInKb": 1248256, + "backgroundScanCompareErrorCount": 0, + "backgroundScanFixedCompareErrorCount": 0, + "bckRebuildReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "bckRebuildWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "capacityAvailableForVolumeAllocationInKb": 369098752, + "capacityInUseInKb": 2496512, + "capacityInUseNoOverheadInKb": 2496512, + "capacityLimitInKb": 845783040, + "compressedDataCompressionRatio": 0.0, + "compressionRatio": 1.0, + "currentFglMigrationSizeInKb": 0, + "deviceIds": [ + ], + "enterProtectedMaintenanceModeCapacityInKb": 0, + "enterProtectedMaintenanceModeReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "enterProtectedMaintenanceModeWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "exitProtectedMaintenanceModeReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "exitProtectedMaintenanceModeWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "exposedCapacityInKb": 0, + "failedCapacityInKb": 0, + "fwdRebuildReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "fwdRebuildWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "inMaintenanceCapacityInKb": 0, + "inMaintenanceVacInKb": 0, + "inUseVacInKb": 184549376, + "inaccessibleCapacityInKb": 0, + "logWrittenBlocksInKb": 0, + "maxCapacityInKb": 845783040, + "migratingVolumeIds": [ + ], + "migratingVtreeIds": [ + ], + "movingCapacityInKb": 0, + "netCapacityInUseInKb": 1248256, + "normRebuildCapacityInKb": 0, + "normRebuildReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "normRebuildWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "numOfDeviceAtFaultRebuilds": 0, + "numOfDevices": 3, + "numOfIncomingVtreeMigrations": 0, + "numOfVolumes": 8, + "numOfVolumesInDeletion": 0, + "numOfVtrees": 8, + "overallUsageRatio": 73.92289, + "pendingBckRebuildCapacityInKb": 0, + "pendingEnterProtectedMaintenanceModeCapacityInKb": 0, + "pendingExitProtectedMaintenanceModeCapacityInKb": 0, + "pendingFwdRebuildCapacityInKb": 0, + "pendingMovingCapacityInKb": 0, + "pendingMovingInBckRebuildJobs": 0, + "persistentChecksumBuilderProgress": 100.0, + "persistentChecksumCapacityInKb": 414720, + "primaryReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "primaryReadFromDevBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "primaryReadFromRmcacheBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "primaryVacInKb": 92274688, + "primaryWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "protectedCapacityInKb": 2496512, + "protectedVacInKb": 184549376, + "provisionedAddressesInKb": 2496512, + "rebalanceCapacityInKb": 0, + "rebalanceReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "rebalanceWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "rfacheReadHit": 0, + "rfacheWriteHit": 0, + "rfcacheAvgReadTime": 0, + "rfcacheAvgWriteTime": 0, + "rfcacheIoErrors": 0, + "rfcacheIosOutstanding": 0, + "rfcacheIosSkipped": 0, + "rfcacheReadMiss": 0, + "rmPendingAllocatedInKb": 0, + "rmPendingThickInKb": 0, + "rplJournalCapAllowed": 0, + "rplTotalJournalCap": 0, + "rplUsedJournalCap": 0, + "secondaryReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "secondaryReadFromDevBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "secondaryReadFromRmcacheBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "secondaryVacInKb": 92274688, + "secondaryWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "semiProtectedCapacityInKb": 0, + "semiProtectedVacInKb": 0, + "snapCapacityInUseInKb": 0, + "snapCapacityInUseOccupiedInKb": 0, + "snapshotCapacityInKb": 0, + "spSdsIds": [ + "abdfe71b00030001", + "abdce71d00040001", + "abdde71e00050001" + ], + "spareCapacityInKb": 84578304, + "targetOtherLatency": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "targetReadLatency": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "targetWriteLatency": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "tempCapacityInKb": 0, + "tempCapacityVacInKb": 0, + "thickCapacityInUseInKb": 0, + "thinAndSnapshotRatio": 73.92289, + "thinCapacityAllocatedInKm": 184549376, + "thinCapacityInUseInKb": 0, + "thinUserDataCapacityInKb": 2496512, + "totalFglMigrationSizeInKb": 0, + "totalReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "totalWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "trimmedUserDataCapacityInKb": 0, + "unreachableUnusedCapacityInKb": 0, + "unusedCapacityInKb": 758708224, + "userDataCapacityInKb": 2496512, + "userDataCapacityNoTrimInKb": 2496512, + "userDataReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataSdcReadLatency": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataSdcTrimLatency": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataSdcWriteLatency": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataTrimBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "volMigrationReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "volMigrationWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "volumeAddressSpaceInKb": 922XXXXX, + "volumeAllocationLimitInKb": 3707XXXXX, + "volumeIds": [ + "456afc7900XXXXXXXX" + ], + "vtreeAddresSpaceInKb": 92274688, + "vtreeIds": [ + "32b1681bXXXXXXXX", + ] + }, + "mediaType": "HDD", + "name": "pool1", + "numOfParallelRebuildRebalanceJobsPerDevice": 2, + "persistentChecksumBuilderLimitKb": 3072, + "persistentChecksumEnabled": true, + "persistentChecksumState": "Protected", + "persistentChecksumValidateOnRead": false, + "protectedMaintenanceModeIoPriorityAppBwPerDeviceThresholdInKbps": null, + "protectedMaintenanceModeIoPriorityAppIopsPerDeviceThreshold": null, + "protectedMaintenanceModeIoPriorityBwLimitPerDeviceInKbps": 10240, + "protectedMaintenanceModeIoPriorityNumOfConcurrentIosPerDevice": 1, + "protectedMaintenanceModeIoPriorityPolicy": "limitNumOfConcurrentIos", + "protectedMaintenanceModeIoPriorityQuietPeriodInMsec": null, + "protectionDomainId": "9300c1f900000000", + "protectionDomainName": "domain1", + "rebalanceEnabled": true, + "rebalanceIoPriorityAppBwPerDeviceThresholdInKbps": null, + "rebalanceIoPriorityAppIopsPerDeviceThreshold": null, + "rebalanceIoPriorityBwLimitPerDeviceInKbps": 10240, + "rebalanceIoPriorityNumOfConcurrentIosPerDevice": 1, + "rebalanceIoPriorityPolicy": "favorAppIos", + "rebalanceIoPriorityQuietPeriodInMsec": null, + "rebuildEnabled": true, + "rebuildIoPriorityAppBwPerDeviceThresholdInKbps": null, + "rebuildIoPriorityAppIopsPerDeviceThreshold": null, + "rebuildIoPriorityBwLimitPerDeviceInKbps": 10240, + "rebuildIoPriorityNumOfConcurrentIosPerDevice": 1, + "rebuildIoPriorityPolicy": "limitNumOfConcurrentIos", + "rebuildIoPriorityQuietPeriodInMsec": null, + "replicationCapacityMaxRatio": 32, + "rmcacheWriteHandlingMode": "Cached", + "sparePercentage": 10, + "useRfcache": false, + "useRmcache": false, + "vtreeMigrationIoPriorityAppBwPerDeviceThresholdInKbps": null, + "vtreeMigrationIoPriorityAppIopsPerDeviceThreshold": null, + "vtreeMigrationIoPriorityBwLimitPerDeviceInKbps": 10240, + "vtreeMigrationIoPriorityNumOfConcurrentIosPerDevice": 1, + "vtreeMigrationIoPriorityPolicy": "favorAppIos", + "vtreeMigrationIoPriorityQuietPeriodInMsec": null, + "zeroPaddingEnabled": true + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.dellemc.powerflex.plugins.module_utils.storage.dell\ + import utils + +LOG = utils.get_logger('storagepool') + + +class PowerFlexStoragePool(object): + """Class with StoragePool 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_storagepool_parameters()) + + """ initialize the ansible module """ + mut_ex_args = [['storage_pool_name', 'storage_pool_id'], + ['protection_domain_name', 'protection_domain_id'], + ['storage_pool_id', 'protection_domain_name'], + ['storage_pool_id', 'protection_domain_id']] + + required_one_of_args = [['storage_pool_name', 'storage_pool_id']] + self.module = AnsibleModule(argument_spec=self.module_params, + supports_check_mode=False, + mutually_exclusive=mut_ex_args, + required_one_of=required_one_of_args) + + 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_protection_domain(self, protection_domain_name=None, + protection_domain_id=None): + """Get protection domain details + :param protection_domain_name: Name of the protection domain + :param protection_domain_id: ID of the protection domain + :return: Protection domain details + """ + name_or_id = protection_domain_id if protection_domain_id \ + else protection_domain_name + try: + filter_fields = {} + if protection_domain_id: + filter_fields = {'id': protection_domain_id} + if protection_domain_name: + filter_fields = {'name': protection_domain_name} + + pd_details = self.powerflex_conn.protection_domain.get( + filter_fields=filter_fields) + if pd_details: + return pd_details[0] + + if not pd_details: + err_msg = "Unable to find the protection domain with {0}. " \ + "Please enter a valid protection domain" \ + " name/id.".format(name_or_id) + self.module.fail_json(msg=err_msg) + + except Exception as e: + errormsg = "Failed to get the protection domain {0} with" \ + " error {1}".format(name_or_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def get_storage_pool(self, storage_pool_id=None, storage_pool_name=None, + pd_id=None): + """Get storage pool details + :param pd_id: ID of the protection domain + :param storage_pool_name: The name of the storage pool + :param storage_pool_id: The storage pool id + :return: Storage pool details + """ + name_or_id = storage_pool_id if storage_pool_id \ + else storage_pool_name + try: + filter_fields = {} + if storage_pool_id: + filter_fields = {'id': storage_pool_id} + if storage_pool_name: + filter_fields.update({'name': storage_pool_name}) + if pd_id: + filter_fields.update({'protectionDomainId': pd_id}) + pool_details = self.powerflex_conn.storage_pool.get( + filter_fields=filter_fields) + if pool_details: + if len(pool_details) > 1: + + err_msg = "More than one storage pool found with {0}," \ + " Please provide protection domain Name/Id" \ + " to fetch the unique" \ + " storage pool".format(storage_pool_name) + LOG.error(err_msg) + self.module.fail_json(msg=err_msg) + elif len(pool_details) == 1: + pool_details = pool_details[0] + statistics = self.powerflex_conn.storage_pool.get_statistics(pool_details['id']) + pool_details['statistics'] = statistics if statistics else {} + pd_id = pool_details['protectionDomainId'] + pd_name = self.get_protection_domain( + protection_domain_id=pd_id)['name'] + # adding protection domain name in the pool details + pool_details['protectionDomainName'] = pd_name + else: + pool_details = None + + return pool_details + + except Exception as e: + errormsg = "Failed to get the storage pool {0} with error " \ + "{1}".format(name_or_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def create_storage_pool(self, pool_name, pd_id, media_type, + use_rfcache=None, use_rmcache=None): + """ + Create a storage pool + :param pool_name: Name of the storage pool + :param pd_id: ID of the storage pool + :param media_type: Type of storage device in the pool + :param use_rfcache: Enable/Disable RFcache on pool + :param use_rmcache: Enable/Disable RMcache on pool + :return: True, if the operation is successful + """ + try: + if media_type == "Transitional": + self.module.fail_json(msg="TRANSITIONAL media type is not" + " supported during creation." + " Please enter a valid media type") + + if pd_id is None: + self.module.fail_json( + msg="Please provide protection domain details for " + "creation of a storage pool") + self.powerflex_conn.storage_pool.create( + media_type=media_type, + protection_domain_id=pd_id, name=pool_name, + use_rfcache=use_rfcache, use_rmcache=use_rmcache) + + return True + except Exception as e: + errormsg = "Failed to create the storage pool {0} with error " \ + "{1}".format(pool_name, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def modify_storage_pool(self, pool_id, modify_dict): + """ + Modify the parameters of the storage pool. + :param modify_dict: Dict containing parameters which are to be + modified + :param pool_id: Id of the pool. + :return: True, if the operation is successful. + """ + + try: + + if 'new_name' in modify_dict: + self.powerflex_conn.storage_pool.rename( + pool_id, modify_dict['new_name']) + if 'use_rmcache' in modify_dict: + self.powerflex_conn.storage_pool.set_use_rmcache( + pool_id, modify_dict['use_rmcache']) + if 'use_rfcache' in modify_dict: + self.powerflex_conn.storage_pool.set_use_rfcache( + pool_id, modify_dict['use_rfcache']) + if 'media_type' in modify_dict: + self.powerflex_conn.storage_pool.set_media_type( + pool_id, modify_dict['media_type']) + return True + + except Exception as e: + err_msg = "Failed to update the storage pool {0} with error " \ + "{1}".format(pool_id, str(e)) + LOG.error(err_msg) + self.module.fail_json(msg=err_msg) + + def verify_params(self, pool_details, pd_name, pd_id): + """ + :param pool_details: Details of the storage pool + :param pd_name: Name of the protection domain + :param pd_id: Id of the protection domain + """ + if pd_id and pd_id != pool_details['protectionDomainId']: + self.module.fail_json(msg="Entered protection domain id does not" + " match with the storage pool's " + "protection domain id. Please enter " + "a correct protection domain id.") + + if pd_name and pd_name != pool_details['protectionDomainName']: + self.module.fail_json(msg="Entered protection domain name does" + " not match with the storage pool's " + "protection domain name. Please enter" + " a correct protection domain name.") + + def perform_module_operation(self): + """ Perform different actions on Storage Pool based on user input + in the playbook """ + + pool_name = self.module.params['storage_pool_name'] + pool_id = self.module.params['storage_pool_id'] + pool_new_name = self.module.params['storage_pool_new_name'] + state = self.module.params['state'] + pd_name = self.module.params['protection_domain_name'] + pd_id = self.module.params['protection_domain_id'] + use_rmcache = self.module.params['use_rmcache'] + use_rfcache = self.module.params['use_rfcache'] + media_type = self.module.params['media_type'] + if media_type == "TRANSITIONAL": + media_type = 'Transitional' + + result = dict( + storage_pool_details={} + ) + changed = False + pd_details = None + if pd_name or pd_id: + pd_details = self.get_protection_domain( + protection_domain_id=pd_id, + protection_domain_name=pd_name) + if pd_details: + pd_id = pd_details['id'] + + if pool_name is not None and (len(pool_name.strip()) == 0): + self.module.fail_json( + msg="Empty or white spaced string provided in " + "storage_pool_name. Please provide valid storage" + " pool name.") + + # Get the details of the storage pool. + pool_details = self.get_storage_pool(storage_pool_id=pool_id, + storage_pool_name=pool_name, + pd_id=pd_id) + if pool_name and pool_details: + pool_id = pool_details['id'] + self.verify_params(pool_details, pd_name, pd_id) + + # create a storage pool + if state == 'present' and not pool_details: + LOG.info("Creating new storage pool") + if pool_id: + self.module.fail_json( + msg="storage_pool_name is missing & name required to " + "create a storage pool. Please enter a valid " + "storage_pool_name.") + if pool_new_name is not None: + self.module.fail_json( + msg="storage_pool_new_name is passed during creation. " + "storage_pool_new_name is not allowed during " + "creation of a storage pool.") + changed = self.create_storage_pool( + pool_name, pd_id, media_type, use_rfcache, use_rmcache) + if changed: + pool_id = self.get_storage_pool(storage_pool_id=pool_id, + storage_pool_name=pool_name, + pd_id=pd_id)['id'] + + # modify the storage pool parameters + if state == 'present' and pool_details: + # check if the parameters are to be updated or not + if pool_new_name is not None and len(pool_new_name.strip()) == 0: + self.module.fail_json( + msg="Empty/White spaced name is not allowed during " + "renaming of a storage pool. Please enter a valid " + "storage pool new name.") + modify_dict = to_modify(pool_details, use_rmcache, use_rfcache, + pool_new_name, media_type) + if bool(modify_dict): + LOG.info("Modify attributes of storage pool") + changed = self.modify_storage_pool(pool_id, modify_dict) + + # Delete a storage pool + if state == 'absent' and pool_details: + msg = "Deleting storage pool is not supported through" \ + " ansible module." + LOG.error(msg) + self.module.fail_json(msg=msg) + + # Show the updated storage pool details + if state == 'present': + pool_details = self.get_storage_pool(storage_pool_id=pool_id) + # fetching Id from pool details to address a case where + # protection domain is not passed + pd_id = pool_details['protectionDomainId'] + pd_name = self.get_protection_domain( + protection_domain_id=pd_id)['name'] + # adding protection domain name in the pool details + pool_details['protectionDomainName'] = pd_name + result['storage_pool_details'] = pool_details + result['changed'] = changed + + self.module.exit_json(**result) + + +def to_modify(pool_details, use_rmcache, use_rfcache, new_name, media_type): + """ + Check whether a parameter is required to be updated. + + :param media_type: Type of the media supported by the pool. + :param pool_details: Details of the storage pool + :param use_rmcache: Enable/Disable RMcache on pool + :param use_rfcache: Enable/Disable RFcache on pool + :param new_name: New name for the storage pool + :return: dict, containing parameters to be modified + """ + pool_name = pool_details['name'] + pool_use_rfcache = pool_details['useRfcache'] + pool_use_rmcache = pool_details['useRmcache'] + pool_media_type = pool_details['mediaType'] + modify_params = {} + + if new_name is not None and pool_name != new_name: + modify_params['new_name'] = new_name + if use_rfcache is not None and pool_use_rfcache != use_rfcache: + modify_params['use_rfcache'] = use_rfcache + if use_rmcache is not None and pool_use_rmcache != use_rmcache: + modify_params['use_rmcache'] = use_rmcache + if media_type is not None and media_type != pool_media_type: + modify_params['media_type'] = media_type + return modify_params + + +def get_powerflex_storagepool_parameters(): + """This method provides parameters required for the ansible + Storage Pool module on powerflex""" + return dict( + storage_pool_name=dict(required=False, type='str'), + storage_pool_id=dict(required=False, type='str'), + protection_domain_name=dict(required=False, type='str'), + protection_domain_id=dict(required=False, type='str'), + media_type=dict(required=False, type='str', + choices=['HDD', 'SSD', 'TRANSITIONAL']), + use_rfcache=dict(required=False, type='bool'), + use_rmcache=dict(required=False, type='bool'), + storage_pool_new_name=dict(required=False, type='str'), + state=dict(required=True, type='str', choices=['present', 'absent'])) + + +def main(): + """ Create PowerFlex Storage Pool object and perform action on it + based on user input from playbook""" + obj = PowerFlexStoragePool() + obj.perform_module_operation() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/powerflex/plugins/modules/volume.py b/ansible_collections/dellemc/powerflex/plugins/modules/volume.py new file mode 100644 index 000000000..9c1e1cd29 --- /dev/null +++ b/ansible_collections/dellemc/powerflex/plugins/modules/volume.py @@ -0,0 +1,1599 @@ +#!/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 volumes on Dell Technologies (Dell) PowerFlex""" + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +DOCUMENTATION = r''' +module: volume +version_added: '1.0.0' +short_description: Manage volumes on Dell PowerFlex +description: +- Managing volumes on PowerFlex storage system includes + creating, getting details, modifying attributes and deleting volume. +- It also includes adding/removing snapshot policy, + mapping/unmapping volume to/from SDC and listing + associated snapshots. +author: +- P Srinivas Rao (@srinivas-rao5) <ansible.team@dell.com> +extends_documentation_fragment: + - dellemc.powerflex.powerflex +options: + vol_name: + description: + - The name of the volume. + - Mandatory for create operation. + - It is unique across the PowerFlex array. + - Mutually exclusive with I(vol_id). + type: str + vol_id: + description: + - The ID of the volume. + - Except create operation, all other operations can be performed + using I(vol_id). + - Mutually exclusive with I(vol_name). + type: str + storage_pool_name: + description: + - The name of the storage pool. + - Either name or the id of the storage pool is required for creating a + volume. + - During creation, if storage pool name is provided then either + protection domain name or id must be mentioned along with it. + - Mutually exclusive with I(storage_pool_id). + type: str + storage_pool_id: + description: + - The ID of the storage pool. + - Either name or the id of the storage pool is required for creating + a volume. + - Mutually exclusive with I(storage_pool_name). + type: str + protection_domain_name: + description: + - The name of the protection domain. + - During creation of a volume, if more than one storage pool exists with + the same name then either protection domain name or id must be + mentioned along with it. + - Mutually exclusive with I(protection_domain_id). + type: str + protection_domain_id: + description: + - The ID of the protection domain. + - During creation of a volume, if more than one storage pool exists with + the same name then either protection domain name or id must be + mentioned along with it. + - Mutually exclusive with I(protection_domain_name). + type: str + vol_type: + description: + - Type of volume provisioning. + choices: ["THICK_PROVISIONED", "THIN_PROVISIONED"] + type: str + compression_type: + description: + - Type of the compression method. + choices: ["NORMAL", "NONE"] + type: str + use_rmcache: + description: + - Whether to use RM Cache or not. + type: bool + snapshot_policy_name: + description: + - Name of the snapshot policy. + - To remove/detach snapshot policy, empty + I(snapshot_policy_id)/I(snapshot_policy_name) is to be passed along with + I(auto_snap_remove_type). + type: str + snapshot_policy_id: + description: + - ID of the snapshot policy. + - To remove/detach snapshot policy, empty + I(snapshot_policy_id)/I(snapshot_policy_name) is to be passed along with + I(auto_snap_remove_type). + type: str + auto_snap_remove_type: + description: + - Whether to remove or detach the snapshot policy. + - To remove/detach snapshot policy, empty + I(snapshot_policy_id)/I(snapshot_policy_name) is to be passed along with + I(auto_snap_remove_type). + - If the snapshot policy name/id is passed empty then + I(auto_snap_remove_type) is defaulted to C(detach). + choices: ['remove', 'detach'] + type: str + size: + description: + - The size of the volume. + - Size of the volume will be assigned as higher multiple of 8 GB. + type: int + cap_unit: + description: + - The unit of the volume size. It defaults to 'GB'. + choices: ['GB' , 'TB'] + type: str + vol_new_name: + description: + - New name of the volume. Used to rename the volume. + type: str + allow_multiple_mappings: + description: + - Specifies whether to allow or not allow multiple mappings. + - If the volume is mapped to one SDC then for every new mapping + I(allow_multiple_mappings) has to be passed as True. + type: bool + 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 volume. + choices: ['READ_WRITE', 'READ_ONLY', 'NO_ACCESS'] + type: str + bandwidth_limit: + description: + - Limit of volume 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 volume 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 + delete_snapshots: + description: + - If C(True), the volume and all its dependent snapshots will be deleted. + - If C(False), only the volume will be deleted. + - It can be specified only when the I(state) is C(absent). + - It defaults to C(False), if not specified. + type: bool + state: + description: + - State of the volume. + choices: ['present', 'absent'] + required: true + type: str +notes: + - The I(check_mode) is not supported. +''' + +EXAMPLES = r''' +- name: Create a volume + dellemc.powerflex.volume: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + vol_name: "sample_volume" + storage_pool_name: "pool_1" + protection_domain_name: "pd_1" + vol_type: "THICK_PROVISIONED" + compression_type: "NORMAL" + use_rmcache: True + size: 16 + state: "present" + +- name: Map a SDC to volume + dellemc.powerflex.volume: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + vol_name: "sample_volume" + allow_multiple_mappings: True + sdc: + - sdc_id: "92A304DB-EFD7-44DF-A07E-D78134CC9764" + access_mode: "READ_WRITE" + sdc_state: "mapped" + state: "present" + +- name: Unmap a SDC to volume + dellemc.powerflex.volume: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + vol_name: "sample_volume" + sdc: + - sdc_id: "92A304DB-EFD7-44DF-A07E-D78134CC9764" + sdc_state: "unmapped" + state: "present" + +- name: Map multiple SDCs to a volume + dellemc.powerflex.volume: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + vol_name: "sample_volume" + protection_domain_name: "pd_1" + sdc: + - sdc_id: "92A304DB-EFD7-44DF-A07E-D78134CC9764" + access_mode: "READ_WRITE" + bandwidth_limit: 2048 + iops_limit: 20 + - sdc_ip: "198.10.xxx.xxx" + access_mode: "READ_ONLY" + allow_multiple_mappings: True + sdc_state: "mapped" + state: "present" + +- name: Get the details of the volume + dellemc.powerflex.volume: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + vol_id: "fe6c8b7100000005" + state: "present" + +- name: Modify the details of the Volume + dellemc.powerflex.volume: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + vol_name: "sample_volume" + storage_pool_name: "pool_1" + new_vol_name: "new_sample_volume" + size: 64 + state: "present" + +- name: Delete the Volume + dellemc.powerflex.volume: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + vol_name: "sample_volume" + delete_snapshots: False + state: "absent" + +- name: Delete the Volume and all its dependent snapshots + dellemc.powerflex.volume: + hostname: "{{hostname}}" + username: "{{username}}" + password: "{{password}}" + validate_certs: "{{validate_certs}}" + port: "{{port}}" + vol_name: "sample_volume" + delete_snapshots: True + state: "absent" +''' + +RETURN = r''' +changed: + description: Whether or not the resource has changed. + returned: always + type: bool + sample: 'false' +volume_details: + description: Details of the volume. + returned: When volume exists + type: dict + contains: + id: + description: The ID of the volume. + 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 volume. + 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 volume. + type: str + sizeInKb: + description: Size of the volume in Kb. + type: int + sizeInGb: + description: Size of the volume in Gb. + type: int + storagePoolId: + description: ID of the storage pool in which volume resides. + type: str + storagePoolName: + description: Name of the storage pool in which volume resides. + type: str + protectionDomainId: + description: ID of the protection domain in which volume resides. + type: str + protectionDomainName: + description: Name of the protection domain in which volume resides. + type: str + snapshotPolicyId: + description: ID of the snapshot policy associated with volume. + type: str + snapshotPolicyName: + description: Name of the snapshot policy associated with volume. + type: str + snapshotsList: + description: List of snapshots associated with the volume. + type: str + "statistics": + description: Statistics details of the storage pool. + type: dict + contains: + "numOfChildVolumes": + description: Number of child volumes. + type: int + "numOfMappedSdcs": + description: Number of mapped Sdcs of the volume. + type: int + sample: { + "accessModeLimit": "ReadWrite", + "ancestorVolumeId": null, + "autoSnapshotGroupId": null, + "compressionMethod": "Invalid", + "consistencyGroupId": null, + "creationTime": 1631618520, + "dataLayout": "MediumGranularity", + "id": "cdd883cf00000002", + "links": [ + { + "href": "/api/instances/Volume::cdd883cf00000002", + "rel": "self" + }, + { + "href": "/api/instances/Volume::cdd883cf00000002/relationships + /Statistics", + "rel": "/api/Volume/relationship/Statistics" + }, + { + "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-volume-1", + "notGenuineSnapshot": false, + "originalExpiryTime": 0, + "pairIds": null, + "protectionDomainId": "9300c1f900000000", + "protectionDomainName": "domain1", + "replicationJournalVolume": false, + "replicationTimeStamp": 0, + "retentionLevels": [], + "secureSnapshotExpTime": 0, + "sizeInGB": 16, + "sizeInKb": 16777216, + "snapshotPolicyId": null, + "snapshotPolicyName": null, + "snapshotsList": [ + { + "accessModeLimit": "ReadOnly", + "ancestorVolumeId": "cdd883cf00000002", + "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, + "retentionLevels": [], + "secureSnapshotExpTime": 0, + "sizeInKb": 16777216, + "snplIdOfAutoSnapshot": null, + "snplIdOfSourceVolume": null, + "storagePoolId": "e0d8f6c900000000", + "timeStampIsAccurate": false, + "useRmcache": false, + "volumeReplicationState": "UnmarkedForReplication", + "volumeType": "Snapshot", + "vtreeId": "6e86255c00000001" + } + ], + "statistics": { + "childVolumeIds": [ + ], + "descendantVolumeIds": [ + ], + "initiatorSdcId": null, + "mappedSdcIds": [ + "c42425XXXXXX" + ], + "numOfChildVolumes": 0, + "numOfDescendantVolumes": 0, + "numOfMappedSdcs": 1, + "registrationKey": null, + "registrationKeys": [ + ], + "replicationJournalVolume": false, + "replicationState": "UnmarkedForReplication", + "reservationType": "NotReserved", + "rplTotalJournalCap": 0, + "rplUsedJournalCap": 0, + "userDataReadBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataSdcReadLatency": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataSdcTrimLatency": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataSdcWriteLatency": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataTrimBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + }, + "userDataWriteBwc": { + "numOccured": 0, + "numSeconds": 0, + "totalWeightInKb": 0 + } + }, + "snplIdOfAutoSnapshot": null, + "snplIdOfSourceVolume": null, + "storagePoolId": "e0d8f6c900000000", + "storagePoolName": "pool1", + "timeStampIsAccurate": false, + "useRmcache": false, + "volumeReplicationState": "UnmarkedForReplication", + "volumeType": "ThinProvisioned", + "vtreeId": "6e86255c00000001" + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.dellemc.powerflex.plugins.module_utils.storage.dell\ + import utils +import copy + +LOG = utils.get_logger('volume') + + +class PowerFlexVolume(object): + """Class with volume 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_volume_parameters()) + + mut_ex_args = [['vol_name', 'vol_id'], + ['storage_pool_name', 'storage_pool_id'], + ['protection_domain_name', 'protection_domain_id'], + ['snapshot_policy_name', 'snapshot_policy_id'], + ['vol_id', 'storage_pool_name'], + ['vol_id', 'storage_pool_id'], + ['vol_id', 'protection_domain_name'], + ['vol_id', 'protection_domain_id']] + + required_together_args = [['sdc', 'sdc_state']] + + required_one_of_args = [['vol_name', 'vol_id']] + + # initialize the Ansible module + self.module = AnsibleModule( + argument_spec=self.module_params, + supports_check_mode=False, + mutually_exclusive=mut_ex_args, + required_together=required_together_args, + required_one_of=required_one_of_args) + + 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_protection_domain(self, protection_domain_name=None, + protection_domain_id=None): + """Get protection domain details + :param protection_domain_name: Name of the protection domain + :param protection_domain_id: ID of the protection domain + :return: Protection domain details + """ + name_or_id = protection_domain_id if protection_domain_id \ + else protection_domain_name + try: + pd_details = None + if protection_domain_id: + pd_details = self.powerflex_conn.protection_domain.get( + filter_fields={'id': protection_domain_id}) + + if protection_domain_name: + pd_details = self.powerflex_conn.protection_domain.get( + filter_fields={'name': protection_domain_name}) + + if not pd_details: + err_msg = "Unable to find the protection domain with {0}. " \ + "Please enter a valid protection domain" \ + " name/id.".format(name_or_id) + self.module.fail_json(msg=err_msg) + + return pd_details[0] + + except Exception as e: + errormsg = "Failed to get the protection domain {0} with" \ + " error {1}".format(name_or_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def get_snapshot_policy(self, snap_pol_id=None, snap_pol_name=None): + """Get snapshot policy details + :param snap_pol_name: Name of the snapshot policy + :param snap_pol_id: ID of the snapshot policy + :return: snapshot policy details + """ + name_or_id = snap_pol_id if snap_pol_id else snap_pol_name + try: + snap_pol_details = None + if snap_pol_id: + snap_pol_details = self.powerflex_conn.snapshot_policy.get( + filter_fields={'id': snap_pol_id}) + + if snap_pol_name: + snap_pol_details = self.powerflex_conn.snapshot_policy.get( + filter_fields={'name': snap_pol_name}) + + if not snap_pol_details: + err_msg = "Unable to find the snapshot policy with {0}. " \ + "Please enter a valid snapshot policy" \ + " name/id.".format(name_or_id) + self.module.fail_json(msg=err_msg) + + return snap_pol_details[0] + + except Exception as e: + errormsg = "Failed to get the snapshot policy {0} with" \ + " error {1}".format(name_or_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def get_storage_pool(self, storage_pool_id=None, storage_pool_name=None, + protection_domain_id=None): + """Get storage pool details + :param protection_domain_id: ID of the protection domain + :param storage_pool_name: The name of the storage pool + :param storage_pool_id: The storage pool id + :return: Storage pool details + """ + name_or_id = storage_pool_id if storage_pool_id \ + else storage_pool_name + try: + sp_details = None + if storage_pool_id: + sp_details = self.powerflex_conn.storage_pool.get( + filter_fields={'id': storage_pool_id}) + + if storage_pool_name: + sp_details = self.powerflex_conn.storage_pool.get( + filter_fields={'name': storage_pool_name}) + + if len(sp_details) > 1 and protection_domain_id is None: + err_msg = "More than one storage pool found with {0}," \ + " Please provide protection domain Name/Id" \ + " to fetch the unique" \ + " pool".format(storage_pool_name) + self.module.fail_json(msg=err_msg) + + if len(sp_details) > 1 and protection_domain_id: + sp_details = self.powerflex_conn.storage_pool.get( + filter_fields={'name': storage_pool_name, + 'protectionDomainId': + protection_domain_id}) + if not sp_details: + err_msg = "Unable to find the storage pool with {0}. " \ + "Please enter a valid pool " \ + "name/id.".format(name_or_id) + self.module.fail_json(msg=err_msg) + return sp_details[0] + + except Exception as e: + errormsg = "Failed to get the storage pool {0} with error " \ + "{1}".format(name_or_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def get_volume(self, vol_name=None, vol_id=None): + """Get volume details + :param vol_name: Name of the volume + :param vol_id: ID of the volume + :return: Details of volume if exist. + """ + + id_or_name = vol_id if vol_id else vol_name + + try: + if vol_name: + volume_details = self.powerflex_conn.volume.get( + filter_fields={'name': vol_name}) + else: + volume_details = self.powerflex_conn.volume.get( + filter_fields={'id': vol_id}) + + if len(volume_details) == 0: + msg = "Volume with identifier {0} not found".format( + id_or_name) + LOG.info(msg) + return None + + # Append size in GB in the volume details + if 'sizeInKb' in volume_details[0] and \ + volume_details[0]['sizeInKb']: + volume_details[0]['sizeInGB'] = utils.get_size_in_gb( + volume_details[0]['sizeInKb'], 'KB') + + # Append storage pool name and id. + sp = None + pd_id = None + if 'storagePoolId' in volume_details[0] and \ + volume_details[0]['storagePoolId']: + sp = \ + self.get_storage_pool(volume_details[0]['storagePoolId']) + if len(sp) > 0: + volume_details[0]['storagePoolName'] = sp['name'] + pd_id = sp['protectionDomainId'] + + # Append protection domain name and id + if sp and 'protectionDomainId' in sp and \ + sp['protectionDomainId']: + pd = self.get_protection_domain(protection_domain_id=pd_id) + volume_details[0]['protectionDomainId'] = pd_id + volume_details[0]['protectionDomainName'] = pd['name'] + + # Append snapshot policy name and id + if volume_details[0]['snplIdOfSourceVolume'] is not None: + snap_policy_id = volume_details[0]['snplIdOfSourceVolume'] + volume_details[0]['snapshotPolicyId'] = snap_policy_id + volume_details[0]['snapshotPolicyName'] = \ + self.get_snapshot_policy(snap_policy_id)['name'] + + return volume_details[0] + + except Exception as e: + error_msg = "Failed to get the volume {0} with error {1}" + error_msg = error_msg.format(id_or_name, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + 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 {0} with error " \ + "{1}".format(id_ip_name, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def create_volume(self, vol_name, pool_id, size, vol_type=None, + use_rmcache=None, comp_type=None): + """Create volume + :param use_rmcache: Boolean indicating whether to use RM cache. + :param comp_type: Type of compression method for the volume. + :param vol_type: Type of volume. + :param size: Size of the volume. + :param pool_id: Id of the storage pool. + :param vol_name: The name of the volume. + :return: Boolean indicating if create operation is successful + """ + try: + if vol_name is None or len(vol_name.strip()) == 0: + self.module.fail_json(msg="Please provide valid volume name.") + + if not size: + self.module.fail_json(msg="Size is a mandatory parameter " + "for creating a volume. Please " + "enter a valid size") + pool_data_layout = None + if pool_id: + pool_details = self.get_storage_pool(storage_pool_id=pool_id) + pool_data_layout = pool_details['dataLayout'] + if comp_type and pool_data_layout and \ + pool_data_layout != "FineGranularity": + err_msg = "compression_type for volume can only be " \ + "mentioned when storage pools have Fine " \ + "Granularity layout. Storage Pool found" \ + " with {0}".format(pool_data_layout) + self.module.fail_json(msg=err_msg) + + # Basic volume created. + self.powerflex_conn.volume.create( + storage_pool_id=pool_id, size_in_gb=size, name=vol_name, + volume_type=vol_type, use_rmcache=use_rmcache, + compression_method=comp_type) + return True + + except Exception as e: + errormsg = "Create volume {0} operation failed with " \ + "error {1}".format(vol_name, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def modify_access_mode(self, vol_id, access_mode_list): + """Modify access mode of SDCs mapped to volume + :param vol_id: The volume 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=vol_id, sdc_id=temp['sdc_id'], + access_mode=temp['accessMode']) + changed = True + return changed + except Exception as e: + errormsg = "Modify access mode of SDC operation failed " \ + "with error {0}".format(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 volume + :param payload: 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 delete_volume(self, vol_id, remove_mode): + """Delete volume + :param vol_id: The volume id + :param remove_mode: Removal mode for the volume + :return: Boolean indicating if delete operation is successful + """ + + try: + self.powerflex_conn.volume.delete(vol_id, remove_mode) + return True + except Exception as e: + errormsg = "Delete volume {0} operation failed with " \ + "error {1}".format(vol_id, str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def unmap_volume_from_sdc(self, volume, sdc): + """Unmap SDC's from volume + :param volume: volume details + :param sdc: List of SDCs to be unmapped + :return: Boolean indicating if unmap operation is successful + """ + + current_sdcs = volume['mappedSdcInfo'] + current_sdc_ids = [] + sdc_id_list = [] + sdc_id = None + 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( + volume['id'], sdc_id) + return True + except Exception as e: + errormsg = "Unmap SDC {0} from volume {1} failed with error " \ + "{2}".format(sdc_id, volume['id'], str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def map_volume_to_sdc(self, volume, sdc): + """Map SDC's to volume + :param volume: volume details + :param sdc: List of SDCs + :return: Boolean indicating if mapping operation is successful + """ + + current_sdcs = volume['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( + volume, 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": volume['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": volume['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 volume {0} to SDC {1} " \ + "failed with error {2}".format(volume['name'], + sdc['sdc_id'], str(e)) + LOG.error(errormsg) + self.module.fail_json(msg=errormsg) + + def validate_parameters(self, auto_snap_remove_type, snap_pol_id, + snap_pol_name, delete_snaps, state): + """Validate the input parameters""" + + sdc = self.module.params['sdc'] + cap_unit = self.module.params['cap_unit'] + size = self.module.params['size'] + + 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 only. Please enter a valid" + " value for size") + + if auto_snap_remove_type and snap_pol_name is None \ + and snap_pol_id is None: + err_msg = "To remove/detach snapshot policy, please provide" \ + " empty snapshot policy name/id along with " \ + "auto_snap_remove_type parameter" + LOG.error(err_msg) + self.module.fail_json(msg=err_msg) + + if state == "present" and delete_snaps is not None: + self.module.fail_json( + msg="delete_snapshots can be specified only when the state" + " is passed as absent.") + + def modify_volume(self, vol_id, modify_dict): + """ + Update the volume attributes + :param vol_id: Id of the volume + :param modify_dict: Dictionary containing the attributes of + volume which are to be updated + :return: True, if the operation is successful + """ + try: + msg = "Dictionary containing attributes which are to be" \ + " updated is {0}.".format(str(modify_dict)) + LOG.info(msg) + + if 'auto_snap_remove_type' in modify_dict: + snap_type = modify_dict['auto_snap_remove_type'] + msg = "Removing/detaching the snapshot policy from a " \ + "volume. auto_snap_remove_type: {0} and snapshot " \ + "policy id: " \ + "{1}".format(snap_type, modify_dict['snap_pol_id']) + LOG.info(msg) + self.powerflex_conn.snapshot_policy.remove_source_volume( + modify_dict['snap_pol_id'], vol_id, snap_type) + msg = "The snapshot policy has been {0}ed " \ + "successfully".format(snap_type) + LOG.info(msg) + + if 'auto_snap_remove_type' not in modify_dict\ + and 'snap_pol_id' in modify_dict: + self.powerflex_conn.snapshot_policy.add_source_volume( + modify_dict['snap_pol_id'], vol_id) + msg = "Attached the snapshot policy {0} to volume" \ + " successfully.".format(modify_dict['snap_pol_id']) + LOG.info(msg) + + if 'new_name' in modify_dict: + self.powerflex_conn.volume.rename(vol_id, + modify_dict['new_name']) + msg = "The name of the volume is updated" \ + " to {0} sucessfully.".format(modify_dict['new_name']) + LOG.info(msg) + + if 'new_size' in modify_dict: + self.powerflex_conn.volume.extend(vol_id, + modify_dict['new_size']) + msg = "The size of the volume is extended to {0} " \ + "sucessfully.".format(str(modify_dict['new_size'])) + LOG.info(msg) + + if 'use_rmcache' in modify_dict: + self.powerflex_conn.volume.set_use_rmcache( + vol_id, modify_dict['use_rmcache']) + msg = "The use RMcache is updated to {0}" \ + " sucessfully.".format(modify_dict['use_rmcache']) + LOG.info(msg) + + if 'comp_type' in modify_dict: + self.powerflex_conn.volume.set_compression_method( + vol_id, modify_dict['comp_type']) + msg = "The compression method is updated to {0}" \ + " successfully.".format(modify_dict['comp_type']) + LOG.info(msg) + return True + + except Exception as e: + err_msg = "Failed to update the volume {0}" \ + " with error {1}".format(vol_id, str(e)) + LOG.error(err_msg) + self.module.fail_json(msg=err_msg) + + def to_modify(self, vol_details, new_size, use_rmcache, comp_type, + new_name, snap_pol_id, + auto_snap_remove_type): + """ + + :param vol_details: Details of the volume + :param new_size: Size of the volume + :param use_rmcache: Bool value of use rm cache + :param comp_type: Type of compression method + :param new_name: The new name of the volume + :param snap_pol_id: Id of the snapshot policy + :param auto_snap_remove_type: Whether to remove or detach the policy + :return: Dictionary containing the attributes of + volume which are to be updated + """ + modify_dict = {} + + if comp_type: + pool_id = vol_details['storagePoolId'] + pool_details = self.get_storage_pool(storage_pool_id=pool_id) + pool_data_layout = pool_details['dataLayout'] + if pool_data_layout != "FineGranularity": + err_msg = "compression_type for volume can only be " \ + "mentioned when storage pools have Fine " \ + "Granularity layout. Storage Pool found" \ + " with {0}".format(pool_data_layout) + self.module.fail_json(msg=err_msg) + + if comp_type != vol_details['compressionMethod']: + modify_dict['comp_type'] = comp_type + + if use_rmcache is not None and \ + vol_details['useRmcache'] != use_rmcache: + modify_dict['use_rmcache'] = use_rmcache + + vol_size_in_gb = utils.get_size_in_gb(vol_details['sizeInKb'], 'KB') + + if new_size is not None and \ + not ((vol_size_in_gb - 8) < new_size <= vol_size_in_gb): + modify_dict['new_size'] = new_size + + if new_name is not None: + if new_name is None or len(new_name.strip()) == 0: + self.module.fail_json(msg="Please provide valid volume " + "name.") + if new_name != vol_details['name']: + modify_dict['new_name'] = new_name + + if snap_pol_id is not None and snap_pol_id == "" and \ + auto_snap_remove_type and vol_details['snplIdOfSourceVolume']: + modify_dict['auto_snap_remove_type'] = auto_snap_remove_type + modify_dict['snap_pol_id'] = \ + vol_details['snplIdOfSourceVolume'] + + if snap_pol_id is not None and snap_pol_id != "": + if auto_snap_remove_type and vol_details['snplIdOfSourceVolume']: + err_msg = "To remove/detach a snapshot policy, provide the" \ + " snapshot policy name/id as empty string" + self.module.fail_json(msg=err_msg) + if auto_snap_remove_type is None and \ + vol_details['snplIdOfSourceVolume'] is None: + modify_dict['snap_pol_id'] = snap_pol_id + + return modify_dict + + def verify_params(self, vol_details, snap_pol_name, snap_pol_id, pd_name, + pd_id, pool_name, pool_id): + """ + :param vol_details: Details of the volume + :param snap_pol_name: Name of the snapshot policy + :param snap_pol_id: Id of the snapshot policy + :param pd_name: Name of the protection domain + :param pd_id: Id of the protection domain + :param pool_name: Name of the storage pool + :param pool_id: Id of the storage pool + """ + + if snap_pol_id and 'snapshotPolicyId' in vol_details and \ + snap_pol_id != vol_details['snapshotPolicyId']: + self.module.fail_json(msg="Entered snapshot policy id does not" + " match with the snapshot policy's id" + " attached to the volume. Please enter" + " a correct snapshot policy id.") + + if snap_pol_name and 'snapshotPolicyId' in vol_details and \ + snap_pol_name != vol_details['snapshotPolicyName']: + self.module.fail_json(msg="Entered snapshot policy name does not" + " match with the snapshot policy's " + "name attached to the volume. Please" + " enter a correct snapshot policy" + " name.") + + if pd_id and pd_id != vol_details['protectionDomainId']: + self.module.fail_json(msg="Entered protection domain id does not" + " match with the volume's protection" + " domain id. Please enter a correct" + " protection domain id.") + + if pool_id and pool_id != vol_details['storagePoolId']: + self.module.fail_json(msg="Entered storage pool id does" + " not match with the volume's " + "storage pool id. Please enter" + " a correct storage pool id.") + + if pd_name and pd_name != vol_details['protectionDomainName']: + self.module.fail_json(msg="Entered protection domain name does" + " not match with the volume's " + "protection domain name. Please enter" + " a correct protection domain name.") + + if pool_name and pool_name != vol_details['storagePoolName']: + self.module.fail_json(msg="Entered storage pool name does" + " not match with the volume's " + "storage pool name. Please enter" + " a correct storage pool name.") + + def perform_module_operation(self): + """ + Perform different actions on volume based on parameters passed in + the playbook + """ + vol_name = self.module.params['vol_name'] + vol_id = self.module.params['vol_id'] + vol_type = self.module.params['vol_type'] + compression_type = self.module.params['compression_type'] + sp_name = self.module.params['storage_pool_name'] + sp_id = self.module.params['storage_pool_id'] + pd_name = self.module.params['protection_domain_name'] + pd_id = self.module.params['protection_domain_id'] + snap_pol_name = self.module.params['snapshot_policy_name'] + snap_pol_id = self.module.params['snapshot_policy_id'] + auto_snap_remove_type = self.module.params['auto_snap_remove_type'] + use_rmcache = self.module.params['use_rmcache'] + size = self.module.params['size'] + cap_unit = self.module.params['cap_unit'] + vol_new_name = self.module.params['vol_new_name'] + sdc = copy.deepcopy(self.module.params['sdc']) + sdc_state = self.module.params['sdc_state'] + delete_snapshots = self.module.params['delete_snapshots'] + state = self.module.params['state'] + + if compression_type: + compression_type = compression_type.capitalize() + if vol_type: + vol_type = get_vol_type(vol_type) + if auto_snap_remove_type: + auto_snap_remove_type = auto_snap_remove_type.capitalize() + + # result is a dictionary to contain end state and volume details + changed = False + result = dict( + changed=False, + volume_details={} + ) + self.validate_parameters(auto_snap_remove_type, snap_pol_id, + snap_pol_name, delete_snapshots, state) + + if not auto_snap_remove_type and\ + (snap_pol_name == "" or snap_pol_id == ""): + auto_snap_remove_type = "Detach" + if size: + if not cap_unit: + cap_unit = 'GB' + + if cap_unit == 'TB': + size = size * 1024 + + if pd_name: + pd_details = self.get_protection_domain(pd_name) + if pd_details: + pd_id = pd_details['id'] + msg = "Fetched the protection domain details with id {0}," \ + " name {1}".format(pd_id, pd_name) + LOG.info(msg) + + if sp_name: + sp_details = self.get_storage_pool(storage_pool_name=sp_name, + protection_domain_id=pd_id) + if sp_details: + sp_id = sp_details['id'] + msg = "Fetched the storage pool details id {0}," \ + " name {1}".format(sp_id, sp_name) + LOG.info(msg) + + if snap_pol_name is not None: + snap_pol_details = None + if snap_pol_name: + snap_pol_details = \ + self.get_snapshot_policy(snap_pol_name=snap_pol_name) + if snap_pol_details: + snap_pol_id = snap_pol_details['id'] + + if snap_pol_name == "": + snap_pol_id = "" + msg = "Fetched the snapshot policy details with id {0}," \ + " name {1}".format(snap_pol_id, snap_pol_name) + LOG.info(msg) + + # get volume details + volume_details = self.get_volume(vol_name, vol_id) + if volume_details: + vol_id = volume_details['id'] + msg = "Fetched the volume details {0}".format(str(volume_details)) + LOG.info(msg) + + if vol_name and volume_details: + self.verify_params( + volume_details, snap_pol_name, snap_pol_id, pd_name, pd_id, + sp_name, sp_id) + + # create operation + create_changed = False + if state == 'present' and not volume_details: + if vol_id: + self.module.fail_json(msg="Creation of volume is allowed " + "using vol_name only, " + "vol_id given.") + + if vol_new_name: + self.module.fail_json( + msg="vol_new_name parameter is not supported during " + "creation of a volume. Try renaming the volume after" + " the creation.") + create_changed = self.create_volume(vol_name, sp_id, size, + vol_type, use_rmcache, + compression_type) + if create_changed: + volume_details = self.get_volume(vol_name) + vol_id = volume_details['id'] + msg = "Volume created successfully, fetched " \ + "volume details {0}".format(str(volume_details)) + LOG.info(msg) + + # checking if basic volume parameters are modified or not. + modify_dict = {} + if volume_details and state == 'present': + modify_dict = self.to_modify( + volume_details, size, use_rmcache, compression_type, + vol_new_name, snap_pol_id, auto_snap_remove_type) + msg = "Parameters to be modified are as" \ + " follows: {0}".format(str(modify_dict)) + LOG.info(msg) + + # Mapping the SDCs to a volume + mode_changed = False + limits_changed = False + map_changed = False + if state == 'present' and volume_details and sdc and \ + sdc_state == 'mapped': + map_changed, access_mode_list, limits_list = \ + self.map_volume_to_sdc(volume_details, sdc) + if len(access_mode_list) > 0: + mode_changed = self.modify_access_mode(vol_id, + access_mode_list) + if len(limits_list) > 0: + for temp in limits_list: + payload = { + "volume_id": volume_details['id'], + "sdc_id": temp['sdc_id'], + "bandwidth_limit": temp['bandwidth_limit'], + "iops_limit": temp['iops_limit'] + } + limits_changed = self.modify_limits(payload) + + # Unmap the SDCs to a volume + unmap_changed = False + if state == 'present' and volume_details and sdc and \ + sdc_state == 'unmapped': + unmap_changed = self.unmap_volume_from_sdc(volume_details, sdc) + + # Update the basic volume attributes + modify_changed = False + if modify_dict and state == 'present': + modify_changed = self.modify_volume(vol_id, modify_dict) + + # delete operation + del_changed = False + if state == 'absent' and volume_details: + if delete_snapshots is True: + delete_snapshots = 'INCLUDING_DESCENDANTS' + if delete_snapshots is None or delete_snapshots is False: + delete_snapshots = 'ONLY_ME' + del_changed = \ + self.delete_volume(vol_id, delete_snapshots) + + if modify_changed or unmap_changed or map_changed or create_changed\ + or del_changed or mode_changed or limits_changed: + changed = True + + # Returning the updated volume details + if state == 'present': + vol_details = self.show_output(vol_id) + result['volume_details'] = vol_details + result['changed'] = changed + self.module.exit_json(**result) + + def show_output(self, vol_id): + """Show volume details + :param vol_id: ID of the volume + :return: Details of volume if exist. + """ + + try: + volume_details = self.powerflex_conn.volume.get( + filter_fields={'id': vol_id}) + + if len(volume_details) == 0: + msg = "Volume with identifier {0} not found".format( + vol_id) + LOG.error(msg) + return None + + # Append size in GB in the volume details + if 'sizeInKb' in volume_details[0] and \ + volume_details[0]['sizeInKb']: + volume_details[0]['sizeInGB'] = utils.get_size_in_gb( + volume_details[0]['sizeInKb'], 'KB') + + # Append storage pool name and id. + sp = None + pd_id = None + if 'storagePoolId' in volume_details[0] and \ + volume_details[0]['storagePoolId']: + sp = \ + self.get_storage_pool(volume_details[0]['storagePoolId']) + if len(sp) > 0: + volume_details[0]['storagePoolName'] = sp['name'] + pd_id = sp['protectionDomainId'] + + # Append protection domain name and id + if sp and 'protectionDomainId' in sp and \ + sp['protectionDomainId']: + pd = self.get_protection_domain(protection_domain_id=pd_id) + volume_details[0]['protectionDomainId'] = pd_id + volume_details[0]['protectionDomainName'] = pd['name'] + + # Append snapshot policy name and id + if volume_details[0]['snplIdOfSourceVolume'] is not None: + snap_policy_id = volume_details[0]['snplIdOfSourceVolume'] + volume_details[0]['snapshotPolicyId'] = snap_policy_id + volume_details[0]['snapshotPolicyName'] = \ + self.get_snapshot_policy(snap_policy_id)['name'] + else: + volume_details[0]['snapshotPolicyId'] = None + volume_details[0]['snapshotPolicyName'] = None + + # Append the list of snapshots associated with the volume + list_of_snaps = self.powerflex_conn.volume.get( + filter_fields={'ancestorVolumeId': volume_details[0]['id']}) + volume_details[0]['snapshotsList'] = list_of_snaps + + # Append statistics + statistics = self.powerflex_conn.volume.get_statistics(volume_details[0]['id']) + volume_details[0]['statistics'] = statistics if statistics else {} + + return volume_details[0] + + except Exception as e: + error_msg = "Failed to get the volume {0} with error {1}" + error_msg = error_msg.format(vol_id, str(e)) + LOG.error(error_msg) + self.module.fail_json(msg=error_msg) + + +def check_for_sdc_modification(volume, sdc_id, sdc_details): + """ + :param volume: The volume 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 volume['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_vol_type(vol_type): + """ + :param vol_type: Type of the volume + :return: Corresponding value for the entered vol_type + """ + vol_type_dict = { + "THICK_PROVISIONED": "ThickProvisioned", + "THIN_PROVISIONED": "ThinProvisioned", + } + return vol_type_dict.get(vol_type) + + +def get_powerflex_volume_parameters(): + """This method provide parameter required for the volume + module on PowerFlex""" + return dict( + vol_name=dict(), vol_id=dict(), + storage_pool_name=dict(), storage_pool_id=dict(), + protection_domain_name=dict(), protection_domain_id=dict(), + use_rmcache=dict(type='bool'), snapshot_policy_name=dict(), + snapshot_policy_id=dict(), + size=dict(type='int'), + cap_unit=dict(choices=['GB', 'TB']), + vol_type=dict(choices=['THICK_PROVISIONED', 'THIN_PROVISIONED']), + compression_type=dict(choices=['NORMAL', 'NONE']), + auto_snap_remove_type=dict(choices=['detach', 'remove']), + vol_new_name=dict(), + allow_multiple_mappings=dict(type='bool'), + delete_snapshots=dict(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') + ) + ), + sdc_state=dict(choices=['mapped', 'unmapped']), + state=dict(required=True, type='str', choices=['present', 'absent']) + ) + + +def main(): + """ Create PowerFlex volume object and perform actions on it + based on user input from playbook""" + obj = PowerFlexVolume() + obj.perform_module_operation() + + +if __name__ == '__main__': + main() |