mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
935 lines
44 KiB
Python
935 lines
44 KiB
Python
#!/usr/bin/python
|
|
|
|
# (c) 2016, NetApp, Inc
|
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
from __future__ import absolute_import, division, print_function
|
|
|
|
__metaclass__ = type
|
|
|
|
ANSIBLE_METADATA = {"metadata_version": "1.1",
|
|
"status": ["preview"],
|
|
"supported_by": "community"}
|
|
|
|
DOCUMENTATION = '''
|
|
---
|
|
module: netapp_e_storagepool
|
|
short_description: NetApp E-Series manage volume groups and disk pools
|
|
description: Create or remove volume groups and disk pools for NetApp E-series storage arrays.
|
|
author:
|
|
- Kevin Hulquest (@hulquest)
|
|
- Nathan Swartz (@ndswartz)
|
|
extends_documentation_fragment:
|
|
- netapp.ontap.netapp.eseries
|
|
|
|
options:
|
|
state:
|
|
description:
|
|
- Whether the specified storage pool should exist or not.
|
|
- Note that removing a storage pool currently requires the removal of all defined volumes first.
|
|
required: true
|
|
choices: ["present", "absent"]
|
|
name:
|
|
description:
|
|
- The name of the storage pool to manage
|
|
required: true
|
|
criteria_drive_count:
|
|
description:
|
|
- The number of disks to use for building the storage pool.
|
|
- When I(state=="present") then I(criteria_drive_count) or I(criteria_min_usable_capacity) must be specified.
|
|
- The pool will be expanded if this number exceeds the number of disks already in place (See expansion note below)
|
|
required: false
|
|
type: int
|
|
criteria_min_usable_capacity:
|
|
description:
|
|
- The minimum size of the storage pool (in size_unit).
|
|
- When I(state=="present") then I(criteria_drive_count) or I(criteria_min_usable_capacity) must be specified.
|
|
- The pool will be expanded if this value exceeds its current size. (See expansion note below)
|
|
required: false
|
|
type: float
|
|
criteria_drive_type:
|
|
description:
|
|
- The type of disk (hdd or ssd) to use when searching for candidates to use.
|
|
- When not specified each drive type will be evaluated until successful drive candidates are found starting with
|
|
the most prevalent drive type.
|
|
required: false
|
|
choices: ["hdd","ssd"]
|
|
criteria_size_unit:
|
|
description:
|
|
- The unit used to interpret size parameters
|
|
choices: ["bytes", "b", "kb", "mb", "gb", "tb", "pb", "eb", "zb", "yb"]
|
|
default: "gb"
|
|
criteria_drive_min_size:
|
|
description:
|
|
- The minimum individual drive size (in size_unit) to consider when choosing drives for the storage pool.
|
|
criteria_drive_interface_type:
|
|
description:
|
|
- The interface type to use when selecting drives for the storage pool
|
|
- If not provided then all interface types will be considered.
|
|
choices: ["sas", "sas4k", "fibre", "fibre520b", "scsi", "sata", "pata"]
|
|
required: false
|
|
criteria_drive_require_da:
|
|
description:
|
|
- Ensures the storage pool will be created with only data assurance (DA) capable drives.
|
|
- Only available for new storage pools; existing storage pools cannot be converted.
|
|
default: false
|
|
type: bool
|
|
criteria_drive_require_fde:
|
|
description:
|
|
- Whether full disk encryption ability is required for drives to be added to the storage pool
|
|
default: false
|
|
type: bool
|
|
raid_level:
|
|
description:
|
|
- The RAID level of the storage pool to be created.
|
|
- Required only when I(state=="present").
|
|
- When I(raid_level=="raidDiskPool") then I(criteria_drive_count >= 10 or criteria_drive_count >= 11) is required
|
|
depending on the storage array specifications.
|
|
- When I(raid_level=="raid0") then I(1<=criteria_drive_count) is required.
|
|
- When I(raid_level=="raid1") then I(2<=criteria_drive_count) is required.
|
|
- When I(raid_level=="raid3") then I(3<=criteria_drive_count<=30) is required.
|
|
- When I(raid_level=="raid5") then I(3<=criteria_drive_count<=30) is required.
|
|
- When I(raid_level=="raid6") then I(5<=criteria_drive_count<=30) is required.
|
|
- Note that raidAll will be treated as raidDiskPool and raid3 as raid5.
|
|
required: false
|
|
choices: ["raidAll", "raid0", "raid1", "raid3", "raid5", "raid6", "raidDiskPool"]
|
|
default: "raidDiskPool"
|
|
secure_pool:
|
|
description:
|
|
- Enables security at rest feature on the storage pool.
|
|
- Will only work if all drives in the pool are security capable (FDE, FIPS, or mix)
|
|
- Warning, once security is enabled it is impossible to disable without erasing the drives.
|
|
required: false
|
|
type: bool
|
|
reserve_drive_count:
|
|
description:
|
|
- Set the number of drives reserved by the storage pool for reconstruction operations.
|
|
- Only valid on raid disk pools.
|
|
required: false
|
|
remove_volumes:
|
|
description:
|
|
- Prior to removing a storage pool, delete all volumes in the pool.
|
|
default: true
|
|
erase_secured_drives:
|
|
description:
|
|
- If I(state=="absent") then all storage pool drives will be erase
|
|
- If I(state=="present") then delete all available storage array drives that have security enabled.
|
|
default: true
|
|
type: bool
|
|
notes:
|
|
- The expansion operations are non-blocking due to the time consuming nature of expanding volume groups
|
|
- Traditional volume groups (raid0, raid1, raid5, raid6) are performed in steps dictated by the storage array. Each
|
|
required step will be attempted until the request fails which is likely because of the required expansion time.
|
|
- raidUnsupported will be treated as raid0, raidAll as raidDiskPool and raid3 as raid5.
|
|
- Tray loss protection and drawer loss protection will be chosen if at all possible.
|
|
'''
|
|
EXAMPLES = """
|
|
- name: No disk groups
|
|
netapp_e_storagepool:
|
|
ssid: "{{ ssid }}"
|
|
name: "{{ item }}"
|
|
state: absent
|
|
api_url: "{{ netapp_api_url }}"
|
|
api_username: "{{ netapp_api_username }}"
|
|
api_password: "{{ netapp_api_password }}"
|
|
validate_certs: "{{ netapp_api_validate_certs }}"
|
|
"""
|
|
RETURN = """
|
|
msg:
|
|
description: Success message
|
|
returned: success
|
|
type: str
|
|
sample: Json facts for the pool that was created.
|
|
"""
|
|
import functools
|
|
from itertools import groupby
|
|
from time import sleep
|
|
from pprint import pformat
|
|
from ansible_collections.netapp.ontap.plugins.module_utils.netapp import NetAppESeriesModule
|
|
from ansible.module_utils._text import to_native
|
|
|
|
|
|
def get_most_common_elements(iterator):
|
|
"""Returns a generator containing a descending list of most common elements."""
|
|
if not isinstance(iterator, list):
|
|
raise TypeError("iterator must be a list.")
|
|
|
|
grouped = [(key, len(list(group))) for key, group in groupby(sorted(iterator))]
|
|
return sorted(grouped, key=lambda x: x[1], reverse=True)
|
|
|
|
|
|
def memoize(func):
|
|
"""Generic memoizer for any function with any number of arguments including zero."""
|
|
|
|
@functools.wraps(func)
|
|
def wrapper(*args, **kwargs):
|
|
class MemoizeFuncArgs(dict):
|
|
def __missing__(self, _key):
|
|
self[_key] = func(*args, **kwargs)
|
|
return self[_key]
|
|
|
|
key = str((args, kwargs)) if args and kwargs else "no_argument_response"
|
|
return MemoizeFuncArgs().__getitem__(key)
|
|
|
|
return wrapper
|
|
|
|
|
|
class NetAppESeriesStoragePool(NetAppESeriesModule):
|
|
EXPANSION_TIMEOUT_SEC = 10
|
|
DEFAULT_DISK_POOL_MINIMUM_DISK_COUNT = 11
|
|
|
|
def __init__(self):
|
|
version = "02.00.0000.0000"
|
|
ansible_options = dict(
|
|
state=dict(required=True, choices=["present", "absent"], type="str"),
|
|
name=dict(required=True, type="str"),
|
|
criteria_size_unit=dict(choices=["bytes", "b", "kb", "mb", "gb", "tb", "pb", "eb", "zb", "yb"],
|
|
default="gb", type="str"),
|
|
criteria_drive_count=dict(type="int"),
|
|
criteria_drive_interface_type=dict(choices=["sas", "sas4k", "fibre", "fibre520b", "scsi", "sata", "pata"],
|
|
type="str"),
|
|
criteria_drive_type=dict(choices=["ssd", "hdd"], type="str", required=False),
|
|
criteria_drive_min_size=dict(type="float"),
|
|
criteria_drive_require_da=dict(type="bool", required=False),
|
|
criteria_drive_require_fde=dict(type="bool", required=False),
|
|
criteria_min_usable_capacity=dict(type="float"),
|
|
raid_level=dict(choices=["raidAll", "raid0", "raid1", "raid3", "raid5", "raid6", "raidDiskPool"],
|
|
default="raidDiskPool"),
|
|
erase_secured_drives=dict(type="bool", default=True),
|
|
secure_pool=dict(type="bool", default=False),
|
|
reserve_drive_count=dict(type="int"),
|
|
remove_volumes=dict(type="bool", default=True))
|
|
|
|
required_if = [["state", "present", ["raid_level"]]]
|
|
super(NetAppESeriesStoragePool, self).__init__(ansible_options=ansible_options,
|
|
web_services_version=version,
|
|
supports_check_mode=True,
|
|
required_if=required_if)
|
|
|
|
args = self.module.params
|
|
self.state = args["state"]
|
|
self.ssid = args["ssid"]
|
|
self.name = args["name"]
|
|
self.criteria_drive_count = args["criteria_drive_count"]
|
|
self.criteria_min_usable_capacity = args["criteria_min_usable_capacity"]
|
|
self.criteria_size_unit = args["criteria_size_unit"]
|
|
self.criteria_drive_min_size = args["criteria_drive_min_size"]
|
|
self.criteria_drive_type = args["criteria_drive_type"]
|
|
self.criteria_drive_interface_type = args["criteria_drive_interface_type"]
|
|
self.criteria_drive_require_fde = args["criteria_drive_require_fde"]
|
|
self.criteria_drive_require_da = args["criteria_drive_require_da"]
|
|
self.raid_level = args["raid_level"]
|
|
self.erase_secured_drives = args["erase_secured_drives"]
|
|
self.secure_pool = args["secure_pool"]
|
|
self.reserve_drive_count = args["reserve_drive_count"]
|
|
self.remove_volumes = args["remove_volumes"]
|
|
self.pool_detail = None
|
|
|
|
# Change all sizes to be measured in bytes
|
|
if self.criteria_min_usable_capacity:
|
|
self.criteria_min_usable_capacity = int(self.criteria_min_usable_capacity *
|
|
self.SIZE_UNIT_MAP[self.criteria_size_unit])
|
|
if self.criteria_drive_min_size:
|
|
self.criteria_drive_min_size = int(self.criteria_drive_min_size *
|
|
self.SIZE_UNIT_MAP[self.criteria_size_unit])
|
|
self.criteria_size_unit = "bytes"
|
|
|
|
# Adjust unused raid level option to reflect documentation
|
|
if self.raid_level == "raidAll":
|
|
self.raid_level = "raidDiskPool"
|
|
if self.raid_level == "raid3":
|
|
self.raid_level = "raid5"
|
|
|
|
@property
|
|
@memoize
|
|
def available_drives(self):
|
|
"""Determine the list of available drives"""
|
|
return [drive["id"] for drive in self.drives if drive["available"] and drive["status"] == "optimal"]
|
|
|
|
@property
|
|
@memoize
|
|
def available_drive_types(self):
|
|
"""Determine the types of available drives sorted by the most common first."""
|
|
types = [drive["driveMediaType"] for drive in self.drives]
|
|
return [entry[0] for entry in get_most_common_elements(types)]
|
|
|
|
@property
|
|
@memoize
|
|
def available_drive_interface_types(self):
|
|
"""Determine the types of available drives."""
|
|
interfaces = [drive["phyDriveType"] for drive in self.drives]
|
|
return [entry[0] for entry in get_most_common_elements(interfaces)]
|
|
|
|
@property
|
|
def storage_pool_drives(self, exclude_hotspares=True):
|
|
"""Retrieve list of drives found in storage pool."""
|
|
if exclude_hotspares:
|
|
return [drive for drive in self.drives
|
|
if drive["currentVolumeGroupRef"] == self.pool_detail["id"] and not drive["hotSpare"]]
|
|
|
|
return [drive for drive in self.drives if drive["currentVolumeGroupRef"] == self.pool_detail["id"]]
|
|
|
|
@property
|
|
def expandable_drive_count(self):
|
|
"""Maximum number of drives that a storage pool can be expanded at a given time."""
|
|
capabilities = None
|
|
if self.raid_level == "raidDiskPool":
|
|
return len(self.available_drives)
|
|
|
|
try:
|
|
rc, capabilities = self.request("storage-systems/%s/capabilities" % self.ssid)
|
|
except Exception as error:
|
|
self.module.fail_json(msg="Failed to fetch maximum expandable drive count. Array id [%s]. Error[%s]."
|
|
% (self.ssid, to_native(error)))
|
|
|
|
return capabilities["featureParameters"]["maxDCEDrives"]
|
|
|
|
@property
|
|
def disk_pool_drive_minimum(self):
|
|
"""Provide the storage array's minimum disk pool drive count."""
|
|
rc, attr = self.request("storage-systems/%s/symbol/getSystemAttributeDefaults" % self.ssid, ignore_errors=True)
|
|
|
|
# Standard minimum is 11 drives but some allow 10 drives. 10 will be the default
|
|
if (rc != 200 or "minimumDriveCount" not in attr["defaults"]["diskPoolDefaultAttributes"].keys() or
|
|
attr["defaults"]["diskPoolDefaultAttributes"]["minimumDriveCount"] == 0):
|
|
return self.DEFAULT_DISK_POOL_MINIMUM_DISK_COUNT
|
|
|
|
return attr["defaults"]["diskPoolDefaultAttributes"]["minimumDriveCount"]
|
|
|
|
def get_available_drive_capacities(self, drive_id_list=None):
|
|
"""Determine the list of available drive capacities."""
|
|
if drive_id_list:
|
|
available_drive_capacities = set([int(drive["usableCapacity"]) for drive in self.drives
|
|
if drive["id"] in drive_id_list and drive["available"] and
|
|
drive["status"] == "optimal"])
|
|
else:
|
|
available_drive_capacities = set([int(drive["usableCapacity"]) for drive in self.drives
|
|
if drive["available"] and drive["status"] == "optimal"])
|
|
|
|
self.module.log("available drive capacities: %s" % available_drive_capacities)
|
|
return list(available_drive_capacities)
|
|
|
|
@property
|
|
def drives(self):
|
|
"""Retrieve list of drives found in storage pool."""
|
|
drives = None
|
|
try:
|
|
rc, drives = self.request("storage-systems/%s/drives" % self.ssid)
|
|
except Exception as error:
|
|
self.module.fail_json(msg="Failed to fetch disk drives. Array id [%s]. Error[%s]."
|
|
% (self.ssid, to_native(error)))
|
|
|
|
return drives
|
|
|
|
def is_drive_count_valid(self, drive_count):
|
|
"""Validate drive count criteria is met."""
|
|
if self.criteria_drive_count and drive_count < self.criteria_drive_count:
|
|
return False
|
|
|
|
if self.raid_level == "raidDiskPool":
|
|
return drive_count >= self.disk_pool_drive_minimum
|
|
if self.raid_level == "raid0":
|
|
return drive_count > 0
|
|
if self.raid_level == "raid1":
|
|
return drive_count >= 2 and (drive_count % 2) == 0
|
|
if self.raid_level in ["raid3", "raid5"]:
|
|
return 3 <= drive_count <= 30
|
|
if self.raid_level == "raid6":
|
|
return 5 <= drive_count <= 30
|
|
return False
|
|
|
|
@property
|
|
def storage_pool(self):
|
|
"""Retrieve storage pool information."""
|
|
storage_pools_resp = None
|
|
try:
|
|
rc, storage_pools_resp = self.request("storage-systems/%s/storage-pools" % self.ssid)
|
|
except Exception as err:
|
|
self.module.fail_json(msg="Failed to get storage pools. Array id [%s]. Error[%s]. State[%s]."
|
|
% (self.ssid, to_native(err), self.state))
|
|
|
|
pool_detail = [pool for pool in storage_pools_resp if pool["name"] == self.name]
|
|
return pool_detail[0] if pool_detail else dict()
|
|
|
|
@property
|
|
def storage_pool_volumes(self):
|
|
"""Retrieve list of volumes associated with storage pool."""
|
|
volumes_resp = None
|
|
try:
|
|
rc, volumes_resp = self.request("storage-systems/%s/volumes" % self.ssid)
|
|
except Exception as err:
|
|
self.module.fail_json(msg="Failed to get storage pools. Array id [%s]. Error[%s]. State[%s]."
|
|
% (self.ssid, to_native(err), self.state))
|
|
|
|
group_ref = self.storage_pool["volumeGroupRef"]
|
|
storage_pool_volume_list = [volume["id"] for volume in volumes_resp if volume["volumeGroupRef"] == group_ref]
|
|
return storage_pool_volume_list
|
|
|
|
def get_ddp_capacity(self, expansion_drive_list):
|
|
"""Return the total usable capacity based on the additional drives."""
|
|
|
|
def get_ddp_error_percent(_drive_count, _extent_count):
|
|
"""Determine the space reserved for reconstruction"""
|
|
if _drive_count <= 36:
|
|
if _extent_count <= 600:
|
|
return 0.40
|
|
elif _extent_count <= 1400:
|
|
return 0.35
|
|
elif _extent_count <= 6200:
|
|
return 0.20
|
|
elif _extent_count <= 50000:
|
|
return 0.15
|
|
elif _drive_count <= 64:
|
|
if _extent_count <= 600:
|
|
return 0.20
|
|
elif _extent_count <= 1400:
|
|
return 0.15
|
|
elif _extent_count <= 6200:
|
|
return 0.10
|
|
elif _extent_count <= 50000:
|
|
return 0.05
|
|
elif _drive_count <= 480:
|
|
if _extent_count <= 600:
|
|
return 0.20
|
|
elif _extent_count <= 1400:
|
|
return 0.15
|
|
elif _extent_count <= 6200:
|
|
return 0.10
|
|
elif _extent_count <= 50000:
|
|
return 0.05
|
|
|
|
self.module.fail_json(msg="Drive count exceeded the error percent table. Array[%s]" % self.ssid)
|
|
|
|
def get_ddp_reserved_drive_count(_disk_count):
|
|
"""Determine the number of reserved drive."""
|
|
reserve_count = 0
|
|
|
|
if self.reserve_drive_count:
|
|
reserve_count = self.reserve_drive_count
|
|
elif _disk_count >= 256:
|
|
reserve_count = 8
|
|
elif _disk_count >= 192:
|
|
reserve_count = 7
|
|
elif _disk_count >= 128:
|
|
reserve_count = 6
|
|
elif _disk_count >= 64:
|
|
reserve_count = 4
|
|
elif _disk_count >= 32:
|
|
reserve_count = 3
|
|
elif _disk_count >= 12:
|
|
reserve_count = 2
|
|
elif _disk_count == 11:
|
|
reserve_count = 1
|
|
|
|
return reserve_count
|
|
|
|
if self.pool_detail:
|
|
drive_count = len(self.storage_pool_drives) + len(expansion_drive_list)
|
|
else:
|
|
drive_count = len(expansion_drive_list)
|
|
|
|
drive_usable_capacity = min(min(self.get_available_drive_capacities()),
|
|
min(self.get_available_drive_capacities(expansion_drive_list)))
|
|
drive_data_extents = ((drive_usable_capacity - 8053063680) / 536870912)
|
|
maximum_stripe_count = (drive_count * drive_data_extents) / 10
|
|
|
|
error_percent = get_ddp_error_percent(drive_count, drive_data_extents)
|
|
error_overhead = (drive_count * drive_data_extents / 10 * error_percent + 10) / 10
|
|
|
|
total_stripe_count = maximum_stripe_count - error_overhead
|
|
stripe_count_per_drive = total_stripe_count / drive_count
|
|
reserved_stripe_count = get_ddp_reserved_drive_count(drive_count) * stripe_count_per_drive
|
|
available_stripe_count = total_stripe_count - reserved_stripe_count
|
|
|
|
return available_stripe_count * 4294967296
|
|
|
|
@memoize
|
|
def get_candidate_drives(self):
|
|
"""Retrieve set of drives candidates for creating a new storage pool."""
|
|
|
|
def get_candidate_drive_request():
|
|
"""Perform request for new volume creation."""
|
|
candidates_list = list()
|
|
drive_types = [self.criteria_drive_type] if self.criteria_drive_type else self.available_drive_types
|
|
interface_types = [self.criteria_drive_interface_type] \
|
|
if self.criteria_drive_interface_type else self.available_drive_interface_types
|
|
|
|
for interface_type in interface_types:
|
|
for drive_type in drive_types:
|
|
candidates = None
|
|
volume_candidate_request_data = dict(
|
|
type="diskPool" if self.raid_level == "raidDiskPool" else "traditional",
|
|
diskPoolVolumeCandidateRequestData=dict(
|
|
reconstructionReservedDriveCount=65535))
|
|
candidate_selection_type = dict(
|
|
candidateSelectionType="count",
|
|
driveRefList=dict(driveRef=self.available_drives))
|
|
criteria = dict(raidLevel=self.raid_level,
|
|
phyDriveType=interface_type,
|
|
dssPreallocEnabled=False,
|
|
securityType="capable" if self.criteria_drive_require_fde else "none",
|
|
driveMediaType=drive_type,
|
|
onlyProtectionInformationCapable=True if self.criteria_drive_require_da else False,
|
|
volumeCandidateRequestData=volume_candidate_request_data,
|
|
allocateReserveSpace=False,
|
|
securityLevel="fde" if self.criteria_drive_require_fde else "none",
|
|
candidateSelectionType=candidate_selection_type)
|
|
|
|
try:
|
|
rc, candidates = self.request("storage-systems/%s/symbol/getVolumeCandidates?verboseError"
|
|
"Response=true" % self.ssid, data=criteria, method="POST")
|
|
except Exception as error:
|
|
self.module.fail_json(msg="Failed to retrieve volume candidates. Array [%s]. Error [%s]."
|
|
% (self.ssid, to_native(error)))
|
|
|
|
if candidates:
|
|
candidates_list.extend(candidates["volumeCandidate"])
|
|
|
|
# Sort output based on tray and then drawer protection first
|
|
tray_drawer_protection = list()
|
|
tray_protection = list()
|
|
drawer_protection = list()
|
|
no_protection = list()
|
|
sorted_candidates = list()
|
|
for item in candidates_list:
|
|
if item["trayLossProtection"]:
|
|
if item["drawerLossProtection"]:
|
|
tray_drawer_protection.append(item)
|
|
else:
|
|
tray_protection.append(item)
|
|
elif item["drawerLossProtection"]:
|
|
drawer_protection.append(item)
|
|
else:
|
|
no_protection.append(item)
|
|
|
|
if tray_drawer_protection:
|
|
sorted_candidates.extend(tray_drawer_protection)
|
|
if tray_protection:
|
|
sorted_candidates.extend(tray_protection)
|
|
if drawer_protection:
|
|
sorted_candidates.extend(drawer_protection)
|
|
if no_protection:
|
|
sorted_candidates.extend(no_protection)
|
|
|
|
return sorted_candidates
|
|
|
|
# Determine the appropriate candidate list
|
|
for candidate in get_candidate_drive_request():
|
|
|
|
# Evaluate candidates for required drive count, collective drive usable capacity and minimum drive size
|
|
if self.criteria_drive_count:
|
|
if self.criteria_drive_count != int(candidate["driveCount"]):
|
|
continue
|
|
if self.criteria_min_usable_capacity:
|
|
if ((self.raid_level == "raidDiskPool" and self.criteria_min_usable_capacity >
|
|
self.get_ddp_capacity(candidate["driveRefList"]["driveRef"])) or
|
|
self.criteria_min_usable_capacity > int(candidate["usableSize"])):
|
|
continue
|
|
if self.criteria_drive_min_size:
|
|
if self.criteria_drive_min_size > min(self.get_available_drive_capacities(candidate["driveRefList"]["driveRef"])):
|
|
continue
|
|
|
|
return candidate
|
|
|
|
self.module.fail_json(msg="Not enough drives to meet the specified criteria. Array [%s]." % self.ssid)
|
|
|
|
@memoize
|
|
def get_expansion_candidate_drives(self):
|
|
"""Retrieve required expansion drive list.
|
|
|
|
Note: To satisfy the expansion criteria each item in the candidate list must added specified group since there
|
|
is a potential limitation on how many drives can be incorporated at a time.
|
|
* Traditional raid volume groups must be added two drives maximum at a time. No limits on raid disk pools.
|
|
|
|
:return list(candidate): list of candidate structures from the getVolumeGroupExpansionCandidates symbol endpoint
|
|
"""
|
|
|
|
def get_expansion_candidate_drive_request():
|
|
"""Perform the request for expanding existing volume groups or disk pools.
|
|
|
|
Note: the list of candidate structures do not necessarily produce candidates that meet all criteria.
|
|
"""
|
|
candidates_list = None
|
|
url = "storage-systems/%s/symbol/getVolumeGroupExpansionCandidates?verboseErrorResponse=true" % self.ssid
|
|
if self.raid_level == "raidDiskPool":
|
|
url = "storage-systems/%s/symbol/getDiskPoolExpansionCandidates?verboseErrorResponse=true" % self.ssid
|
|
|
|
try:
|
|
rc, candidates_list = self.request(url, method="POST", data=self.pool_detail["id"])
|
|
except Exception as error:
|
|
self.module.fail_json(msg="Failed to retrieve volume candidates. Array [%s]. Error [%s]."
|
|
% (self.ssid, to_native(error)))
|
|
|
|
return candidates_list["candidates"]
|
|
|
|
required_candidate_list = list()
|
|
required_additional_drives = 0
|
|
required_additional_capacity = 0
|
|
total_required_capacity = 0
|
|
|
|
# determine whether and how much expansion is need to satisfy the specified criteria
|
|
if self.criteria_min_usable_capacity:
|
|
total_required_capacity = self.criteria_min_usable_capacity
|
|
required_additional_capacity = self.criteria_min_usable_capacity - int(self.pool_detail["totalRaidedSpace"])
|
|
|
|
if self.criteria_drive_count:
|
|
required_additional_drives = self.criteria_drive_count - len(self.storage_pool_drives)
|
|
|
|
# Determine the appropriate expansion candidate list
|
|
if required_additional_drives > 0 or required_additional_capacity > 0:
|
|
for candidate in get_expansion_candidate_drive_request():
|
|
|
|
if self.criteria_drive_min_size:
|
|
if self.criteria_drive_min_size > min(self.get_available_drive_capacities(candidate["drives"])):
|
|
continue
|
|
|
|
if self.raid_level == "raidDiskPool":
|
|
if (len(candidate["drives"]) >= required_additional_drives and
|
|
self.get_ddp_capacity(candidate["drives"]) >= total_required_capacity):
|
|
required_candidate_list.append(candidate)
|
|
break
|
|
else:
|
|
required_additional_drives -= len(candidate["drives"])
|
|
required_additional_capacity -= int(candidate["usableCapacity"])
|
|
required_candidate_list.append(candidate)
|
|
|
|
# Determine if required drives and capacities are satisfied
|
|
if required_additional_drives <= 0 and required_additional_capacity <= 0:
|
|
break
|
|
else:
|
|
self.module.fail_json(msg="Not enough drives to meet the specified criteria. Array [%s]." % self.ssid)
|
|
|
|
return required_candidate_list
|
|
|
|
def get_reserve_drive_count(self):
|
|
"""Retrieve the current number of reserve drives for raidDiskPool (Only for raidDiskPool)."""
|
|
|
|
if not self.pool_detail:
|
|
self.module.fail_json(msg="The storage pool must exist. Array [%s]." % self.ssid)
|
|
|
|
if self.raid_level != "raidDiskPool":
|
|
self.module.fail_json(msg="The storage pool must be a raidDiskPool. Pool [%s]. Array [%s]."
|
|
% (self.pool_detail["id"], self.ssid))
|
|
|
|
return self.pool_detail["volumeGroupData"]["diskPoolData"]["reconstructionReservedDriveCount"]
|
|
|
|
def get_maximum_reserve_drive_count(self):
|
|
"""Retrieve the maximum number of reserve drives for storage pool (Only for raidDiskPool)."""
|
|
if self.raid_level != "raidDiskPool":
|
|
self.module.fail_json(msg="The storage pool must be a raidDiskPool. Pool [%s]. Array [%s]."
|
|
% (self.pool_detail["id"], self.ssid))
|
|
|
|
drives_ids = list()
|
|
|
|
if self.pool_detail:
|
|
drives_ids.extend(self.storage_pool_drives)
|
|
for candidate in self.get_expansion_candidate_drives():
|
|
drives_ids.extend((candidate["drives"]))
|
|
else:
|
|
candidate = self.get_candidate_drives()
|
|
drives_ids.extend(candidate["driveRefList"]["driveRef"])
|
|
|
|
drive_count = len(drives_ids)
|
|
maximum_reserve_drive_count = min(int(drive_count * 0.2 + 1), drive_count - 10)
|
|
if maximum_reserve_drive_count > 10:
|
|
maximum_reserve_drive_count = 10
|
|
|
|
return maximum_reserve_drive_count
|
|
|
|
def set_reserve_drive_count(self, check_mode=False):
|
|
"""Set the reserve drive count for raidDiskPool."""
|
|
changed = False
|
|
|
|
if self.raid_level == "raidDiskPool" and self.reserve_drive_count:
|
|
maximum_count = self.get_maximum_reserve_drive_count()
|
|
|
|
if self.reserve_drive_count < 0 or self.reserve_drive_count > maximum_count:
|
|
self.module.fail_json(msg="Supplied reserve drive count is invalid or exceeds the maximum allowed. "
|
|
"Note that it may be necessary to wait for expansion operations to complete "
|
|
"before the adjusting the reserve drive count. Maximum [%s]. Array [%s]."
|
|
% (maximum_count, self.ssid))
|
|
|
|
if self.reserve_drive_count != self.get_reserve_drive_count():
|
|
changed = True
|
|
|
|
if not check_mode:
|
|
try:
|
|
rc, resp = self.request("storage-systems/%s/symbol/setDiskPoolReservedDriveCount" % self.ssid,
|
|
method="POST", data=dict(volumeGroupRef=self.pool_detail["id"],
|
|
newDriveCount=self.reserve_drive_count))
|
|
except Exception as error:
|
|
self.module.fail_json(msg="Failed to set reserve drive count for disk pool. Disk Pool [%s]."
|
|
" Array [%s]." % (self.pool_detail["id"], self.ssid))
|
|
|
|
return changed
|
|
|
|
def erase_all_available_secured_drives(self, check_mode=False):
|
|
"""Erase all available drives that have encryption at rest feature enabled."""
|
|
changed = False
|
|
drives_list = list()
|
|
for drive in self.drives:
|
|
if drive["available"] and drive["fdeEnabled"]:
|
|
changed = True
|
|
drives_list.append(drive["id"])
|
|
|
|
if drives_list and not check_mode:
|
|
try:
|
|
rc, resp = self.request("storage-systems/%s/symbol/reprovisionDrive?verboseErrorResponse=true"
|
|
% self.ssid, method="POST", data=dict(driveRef=drives_list))
|
|
except Exception as error:
|
|
self.module.fail_json(msg="Failed to erase all secured drives. Array [%s]" % self.ssid)
|
|
|
|
return changed
|
|
|
|
def create_storage_pool(self):
|
|
"""Create new storage pool."""
|
|
url = "storage-systems/%s/symbol/createVolumeGroup?verboseErrorResponse=true" % self.ssid
|
|
request_body = dict(label=self.name,
|
|
candidate=self.get_candidate_drives())
|
|
|
|
if self.raid_level == "raidDiskPool":
|
|
url = "storage-systems/%s/symbol/createDiskPool?verboseErrorResponse=true" % self.ssid
|
|
|
|
request_body.update(
|
|
dict(backgroundOperationPriority="useDefault",
|
|
criticalReconstructPriority="useDefault",
|
|
degradedReconstructPriority="useDefault",
|
|
poolUtilizationCriticalThreshold=65535,
|
|
poolUtilizationWarningThreshold=0))
|
|
|
|
if self.reserve_drive_count:
|
|
request_body.update(dict(volumeCandidateData=dict(
|
|
diskPoolVolumeCandidateData=dict(reconstructionReservedDriveCount=self.reserve_drive_count))))
|
|
|
|
try:
|
|
rc, resp = self.request(url, method="POST", data=request_body)
|
|
except Exception as error:
|
|
self.module.fail_json(msg="Failed to create storage pool. Array id [%s]. Error[%s]."
|
|
% (self.ssid, to_native(error)))
|
|
|
|
# Update drive and storage pool information
|
|
self.pool_detail = self.storage_pool
|
|
|
|
def delete_storage_pool(self):
|
|
"""Delete storage pool."""
|
|
storage_pool_drives = [drive["id"] for drive in self.storage_pool_drives if drive["fdeEnabled"]]
|
|
try:
|
|
delete_volumes_parameter = "?delete-volumes=true" if self.remove_volumes else ""
|
|
rc, resp = self.request("storage-systems/%s/storage-pools/%s%s"
|
|
% (self.ssid, self.pool_detail["id"], delete_volumes_parameter), method="DELETE")
|
|
except Exception as error:
|
|
self.module.fail_json(msg="Failed to delete storage pool. Pool id [%s]. Array id [%s]. Error[%s]."
|
|
% (self.pool_detail["id"], self.ssid, to_native(error)))
|
|
|
|
if storage_pool_drives and self.erase_secured_drives:
|
|
try:
|
|
rc, resp = self.request("storage-systems/%s/symbol/reprovisionDrive?verboseErrorResponse=true"
|
|
% self.ssid, method="POST", data=dict(driveRef=storage_pool_drives))
|
|
except Exception as error:
|
|
self.module.fail_json(msg="Failed to erase drives prior to creating new storage pool. Array [%s]."
|
|
" Error [%s]." % (self.ssid, to_native(error)))
|
|
|
|
def secure_storage_pool(self, check_mode=False):
|
|
"""Enable security on an existing storage pool"""
|
|
self.pool_detail = self.storage_pool
|
|
needs_secure_pool = False
|
|
|
|
if not self.secure_pool and self.pool_detail["securityType"] == "enabled":
|
|
self.module.fail_json(msg="It is not possible to disable storage pool security! See array documentation.")
|
|
if self.secure_pool and self.pool_detail["securityType"] != "enabled":
|
|
needs_secure_pool = True
|
|
|
|
if needs_secure_pool and not check_mode:
|
|
try:
|
|
rc, resp = self.request("storage-systems/%s/storage-pools/%s" % (self.ssid, self.pool_detail["id"]),
|
|
data=dict(securePool=True), method="POST")
|
|
except Exception as error:
|
|
self.module.fail_json(msg="Failed to secure storage pool. Pool id [%s]. Array [%s]. Error"
|
|
" [%s]." % (self.pool_detail["id"], self.ssid, to_native(error)))
|
|
|
|
self.pool_detail = self.storage_pool
|
|
return needs_secure_pool
|
|
|
|
def migrate_raid_level(self, check_mode=False):
|
|
"""Request storage pool raid level migration."""
|
|
needs_migration = self.raid_level != self.pool_detail["raidLevel"]
|
|
if needs_migration and self.pool_detail["raidLevel"] == "raidDiskPool":
|
|
self.module.fail_json(msg="Raid level cannot be changed for disk pools")
|
|
|
|
if needs_migration and not check_mode:
|
|
sp_raid_migrate_req = dict(raidLevel=self.raid_level)
|
|
|
|
try:
|
|
rc, resp = self.request("storage-systems/%s/storage-pools/%s/raid-type-migration"
|
|
% (self.ssid, self.name), data=sp_raid_migrate_req, method="POST")
|
|
except Exception as error:
|
|
self.module.fail_json(msg="Failed to change the raid level of storage pool. Array id [%s]."
|
|
" Error[%s]." % (self.ssid, to_native(error)))
|
|
|
|
self.pool_detail = self.storage_pool
|
|
return needs_migration
|
|
|
|
def expand_storage_pool(self, check_mode=False):
|
|
"""Add drives to existing storage pool.
|
|
|
|
:return bool: whether drives were required to be added to satisfy the specified criteria."""
|
|
expansion_candidate_list = self.get_expansion_candidate_drives()
|
|
changed_required = bool(expansion_candidate_list)
|
|
estimated_completion_time = 0.0
|
|
|
|
# build expandable groupings of traditional raid candidate
|
|
required_expansion_candidate_list = list()
|
|
while expansion_candidate_list:
|
|
subset = list()
|
|
while expansion_candidate_list and len(subset) < self.expandable_drive_count:
|
|
subset.extend(expansion_candidate_list.pop()["drives"])
|
|
required_expansion_candidate_list.append(subset)
|
|
|
|
if required_expansion_candidate_list and not check_mode:
|
|
url = "storage-systems/%s/symbol/startVolumeGroupExpansion?verboseErrorResponse=true" % self.ssid
|
|
if self.raid_level == "raidDiskPool":
|
|
url = "storage-systems/%s/symbol/startDiskPoolExpansion?verboseErrorResponse=true" % self.ssid
|
|
|
|
while required_expansion_candidate_list:
|
|
candidate_drives_list = required_expansion_candidate_list.pop()
|
|
request_body = dict(volumeGroupRef=self.pool_detail["volumeGroupRef"],
|
|
driveRef=candidate_drives_list)
|
|
try:
|
|
rc, resp = self.request(url, method="POST", data=request_body)
|
|
except Exception as error:
|
|
rc, actions_resp = self.request("storage-systems/%s/storage-pools/%s/action-progress"
|
|
% (self.ssid, self.pool_detail["id"]), ignore_errors=True)
|
|
if rc == 200 and actions_resp:
|
|
actions = [action["currentAction"] for action in actions_resp
|
|
if action["volumeRef"] in self.storage_pool_volumes]
|
|
self.module.fail_json(msg="Failed to add drives to the storage pool possibly because of actions"
|
|
" in progress. Actions [%s]. Pool id [%s]. Array id [%s]. Error[%s]."
|
|
% (", ".join(actions), self.pool_detail["id"], self.ssid,
|
|
to_native(error)))
|
|
|
|
self.module.fail_json(msg="Failed to add drives to storage pool. Pool id [%s]. Array id [%s]."
|
|
" Error[%s]." % (self.pool_detail["id"], self.ssid, to_native(error)))
|
|
|
|
# Wait for expansion completion unless it is the last request in the candidate list
|
|
if required_expansion_candidate_list:
|
|
for dummy in range(self.EXPANSION_TIMEOUT_SEC):
|
|
rc, actions_resp = self.request("storage-systems/%s/storage-pools/%s/action-progress"
|
|
% (self.ssid, self.pool_detail["id"]), ignore_errors=True)
|
|
if rc == 200:
|
|
for action in actions_resp:
|
|
if (action["volumeRef"] in self.storage_pool_volumes and
|
|
action["currentAction"] == "remappingDce"):
|
|
sleep(1)
|
|
estimated_completion_time = action["estimatedTimeToCompletion"]
|
|
break
|
|
else:
|
|
estimated_completion_time = 0.0
|
|
break
|
|
|
|
return changed_required, estimated_completion_time
|
|
|
|
def apply(self):
|
|
"""Apply requested state to storage array."""
|
|
changed = False
|
|
|
|
if self.state == "present":
|
|
if self.criteria_drive_count is None and self.criteria_min_usable_capacity is None:
|
|
self.module.fail_json(msg="One of criteria_min_usable_capacity or criteria_drive_count must be"
|
|
" specified.")
|
|
if self.criteria_drive_count and not self.is_drive_count_valid(self.criteria_drive_count):
|
|
self.module.fail_json(msg="criteria_drive_count must be valid for the specified raid level.")
|
|
|
|
self.pool_detail = self.storage_pool
|
|
self.module.log(pformat(self.pool_detail))
|
|
|
|
if self.state == "present" and self.erase_secured_drives:
|
|
self.erase_all_available_secured_drives(check_mode=True)
|
|
|
|
# Determine whether changes need to be applied to the storage array
|
|
if self.pool_detail:
|
|
|
|
if self.state == "absent":
|
|
changed = True
|
|
|
|
elif self.state == "present":
|
|
|
|
if self.criteria_drive_count and self.criteria_drive_count < len(self.storage_pool_drives):
|
|
self.module.fail_json(msg="Failed to reduce the size of the storage pool. Array [%s]. Pool [%s]."
|
|
% (self.ssid, self.pool_detail["id"]))
|
|
|
|
if self.criteria_drive_type and self.criteria_drive_type != self.pool_detail["driveMediaType"]:
|
|
self.module.fail_json(msg="Failed! It is not possible to modify storage pool media type."
|
|
" Array [%s]. Pool [%s]." % (self.ssid, self.pool_detail["id"]))
|
|
|
|
if (self.criteria_drive_require_da is not None and self.criteria_drive_require_da !=
|
|
self.pool_detail["protectionInformationCapabilities"]["protectionInformationCapable"]):
|
|
self.module.fail_json(msg="Failed! It is not possible to modify DA-capability. Array [%s]."
|
|
" Pool [%s]." % (self.ssid, self.pool_detail["id"]))
|
|
|
|
# Evaluate current storage pool for required change.
|
|
needs_expansion, estimated_completion_time = self.expand_storage_pool(check_mode=True)
|
|
if needs_expansion:
|
|
changed = True
|
|
if self.migrate_raid_level(check_mode=True):
|
|
changed = True
|
|
if self.secure_storage_pool(check_mode=True):
|
|
changed = True
|
|
if self.set_reserve_drive_count(check_mode=True):
|
|
changed = True
|
|
|
|
elif self.state == "present":
|
|
changed = True
|
|
|
|
# Apply changes to storage array
|
|
msg = "No changes were required for the storage pool [%s]."
|
|
if changed and not self.module.check_mode:
|
|
if self.state == "present":
|
|
if self.erase_secured_drives:
|
|
self.erase_all_available_secured_drives()
|
|
|
|
if self.pool_detail:
|
|
change_list = list()
|
|
|
|
# Expansion needs to occur before raid level migration to account for any sizing needs.
|
|
expanded, estimated_completion_time = self.expand_storage_pool()
|
|
if expanded:
|
|
change_list.append("expanded")
|
|
if self.migrate_raid_level():
|
|
change_list.append("raid migration")
|
|
if self.secure_storage_pool():
|
|
change_list.append("secured")
|
|
if self.set_reserve_drive_count():
|
|
change_list.append("adjusted reserve drive count")
|
|
|
|
if change_list:
|
|
msg = "Following changes have been applied to the storage pool [%s]: " + ", ".join(change_list)
|
|
|
|
if expanded:
|
|
msg += "\nThe expansion operation will complete in an estimated %s minutes."\
|
|
% estimated_completion_time
|
|
else:
|
|
self.create_storage_pool()
|
|
msg = "Storage pool [%s] was created."
|
|
|
|
if self.secure_storage_pool():
|
|
msg = "Storage pool [%s] was created and secured."
|
|
if self.set_reserve_drive_count():
|
|
msg += " Adjusted reserve drive count."
|
|
|
|
elif self.pool_detail:
|
|
self.delete_storage_pool()
|
|
msg = "Storage pool [%s] removed."
|
|
|
|
self.pool_detail = self.storage_pool
|
|
self.module.log(pformat(self.pool_detail))
|
|
self.module.log(msg % self.name)
|
|
self.module.exit_json(msg=msg % self.name, changed=changed, **self.pool_detail)
|
|
|
|
|
|
def main():
|
|
storage_pool = NetAppESeriesStoragePool()
|
|
storage_pool.apply()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|