summaryrefslogtreecommitdiffstats
path: root/ansible_collections/dellemc/powerflex/plugins
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
commit975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch)
tree89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/dellemc/powerflex/plugins
parentInitial commit. (diff)
downloadansible-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')
-rw-r--r--ansible_collections/dellemc/powerflex/plugins/doc_fragments/powerflex.py61
-rw-r--r--ansible_collections/dellemc/powerflex/plugins/module_utils/storage/dell/__init__.py0
-rw-r--r--ansible_collections/dellemc/powerflex/plugins/module_utils/storage/dell/logging_handler.py24
-rw-r--r--ansible_collections/dellemc/powerflex/plugins/module_utils/storage/dell/utils.py186
-rw-r--r--ansible_collections/dellemc/powerflex/plugins/modules/device.py1105
-rw-r--r--ansible_collections/dellemc/powerflex/plugins/modules/info.py1578
-rw-r--r--ansible_collections/dellemc/powerflex/plugins/modules/mdm_cluster.py1339
-rw-r--r--ansible_collections/dellemc/powerflex/plugins/modules/protection_domain.py1122
-rw-r--r--ansible_collections/dellemc/powerflex/plugins/modules/replication_consistency_group.py907
-rw-r--r--ansible_collections/dellemc/powerflex/plugins/modules/replication_pair.py695
-rw-r--r--ansible_collections/dellemc/powerflex/plugins/modules/sdc.py365
-rw-r--r--ansible_collections/dellemc/powerflex/plugins/modules/sds.py1160
-rw-r--r--ansible_collections/dellemc/powerflex/plugins/modules/snapshot.py1285
-rw-r--r--ansible_collections/dellemc/powerflex/plugins/modules/storagepool.py914
-rw-r--r--ansible_collections/dellemc/powerflex/plugins/modules/volume.py1599
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()