mirror of https://github.com/ansible/ansible.git
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
885 lines
39 KiB
Python
885 lines
39 KiB
Python
#!/usr/bin/python
|
|
|
|
# (c) 2016, NetApp, Inc
|
|
#
|
|
# This file is part of Ansible
|
|
#
|
|
# Ansible is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Ansible is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
DOCUMENTATION = '''
|
|
---
|
|
module: netapp_e_storagepool
|
|
short_description: Manage disk groups and disk pools
|
|
version_added: '2.2'
|
|
description:
|
|
- Create or remove disk groups and disk pools for NetApp E-series storage arrays.
|
|
options:
|
|
api_username:
|
|
required: true
|
|
description:
|
|
- The username to authenticate with the SANtricity WebServices Proxy or embedded REST API.
|
|
api_password:
|
|
required: true
|
|
description:
|
|
- The password to authenticate with the SANtricity WebServices Proxy or embedded REST API.
|
|
api_url:
|
|
required: true
|
|
description:
|
|
- The url to the SANtricity WebServices Proxy or embedded REST API.
|
|
example:
|
|
- https://prod-1.wahoo.acme.com/devmgr/v2
|
|
validate_certs:
|
|
required: false
|
|
default: true
|
|
description:
|
|
- Should https certificates be validated?
|
|
ssid:
|
|
required: true
|
|
description:
|
|
- The ID of the array to manage (as configured on the web services proxy).
|
|
state:
|
|
required: true
|
|
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.
|
|
choices: ['present', 'absent']
|
|
name:
|
|
required: true
|
|
description:
|
|
- The name of the storage pool to manage
|
|
criteria_drive_count:
|
|
description:
|
|
- The number of disks to use for building the storage pool. The pool will be expanded if this number exceeds the number of disks already in place
|
|
criteria_drive_type:
|
|
description:
|
|
- The type of disk (hdd or ssd) to use when searching for candidates to use.
|
|
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_min_usable_capacity:
|
|
description:
|
|
- The minimum size of the storage pool (in size_unit). The pool will be expanded if this value exceeds itscurrent size.
|
|
criteria_drive_interface_type:
|
|
description:
|
|
- The interface type to use when selecting drives for the storage pool (no value means all interface types will be considered)
|
|
choices: ['sas', 'sas4k', 'fibre', 'fibre520b', 'scsi', 'sata', 'pata']
|
|
criteria_drive_require_fde:
|
|
description:
|
|
- Whether full disk encryption ability is required for drives to be added to the storage pool
|
|
raid_level:
|
|
required: true
|
|
choices: ['raidAll', 'raid0', 'raid1', 'raid3', 'raid5', 'raid6', 'raidDiskPool']
|
|
description:
|
|
- "Only required when the requested state is 'present'. The RAID level of the storage pool to be created."
|
|
erase_secured_drives:
|
|
required: false
|
|
choices: ['true', 'false']
|
|
description:
|
|
- Whether to erase secured disks before adding to storage pool
|
|
secure_pool:
|
|
required: false
|
|
choices: ['true', 'false']
|
|
description:
|
|
- Whether to convert to a secure storage pool. Will only work if all drives in the pool are security capable.
|
|
reserve_drive_count:
|
|
required: false
|
|
description:
|
|
- Set the number of drives reserved by the storage pool for reconstruction operations. Only valide on raid disk pools.
|
|
remove_volumes:
|
|
required: false
|
|
default: False
|
|
description:
|
|
- Prior to removing a storage pool, delete all volumes in the pool.
|
|
author: Kevin Hulquest (@hulquest)
|
|
|
|
'''
|
|
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: string
|
|
sample: Json facts for the pool that was created.
|
|
'''
|
|
|
|
import json
|
|
import logging
|
|
from traceback import format_exc
|
|
|
|
from ansible.module_utils.api import basic_auth_argument_spec
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
from ansible.module_utils.pycompat24 import get_exception
|
|
from ansible.module_utils.urls import open_url
|
|
from ansible.module_utils.six.moves.urllib.error import HTTPError
|
|
|
|
|
|
def request(url, data=None, headers=None, method='GET', use_proxy=True,
|
|
force=False, last_mod_time=None, timeout=10, validate_certs=True,
|
|
url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False):
|
|
try:
|
|
r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy,
|
|
force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs,
|
|
url_username=url_username, url_password=url_password, http_agent=http_agent,
|
|
force_basic_auth=force_basic_auth)
|
|
except HTTPError:
|
|
err = get_exception()
|
|
r = err.fp
|
|
|
|
try:
|
|
raw_data = r.read()
|
|
if raw_data:
|
|
data = json.loads(raw_data)
|
|
else:
|
|
raw_data = None
|
|
except:
|
|
if ignore_errors:
|
|
pass
|
|
else:
|
|
raise Exception(raw_data)
|
|
|
|
resp_code = r.getcode()
|
|
|
|
if resp_code >= 400 and not ignore_errors:
|
|
raise Exception(resp_code, data)
|
|
else:
|
|
return resp_code, data
|
|
|
|
|
|
def select(predicate, iterable):
|
|
# python 2, 3 generic filtering.
|
|
if predicate is None:
|
|
predicate = bool
|
|
for x in iterable:
|
|
if predicate(x):
|
|
yield x
|
|
|
|
|
|
class groupby(object):
|
|
# python 2, 3 generic grouping.
|
|
def __init__(self, iterable, key=None):
|
|
if key is None:
|
|
key = lambda x: x
|
|
self.keyfunc = key
|
|
self.it = iter(iterable)
|
|
self.tgtkey = self.currkey = self.currvalue = object()
|
|
|
|
def __iter__(self):
|
|
return self
|
|
|
|
def next(self):
|
|
while self.currkey == self.tgtkey:
|
|
self.currvalue = next(self.it) # Exit on StopIteration
|
|
self.currkey = self.keyfunc(self.currvalue)
|
|
self.tgtkey = self.currkey
|
|
return (self.currkey, self._grouper(self.tgtkey))
|
|
|
|
def _grouper(self, tgtkey):
|
|
while self.currkey == tgtkey:
|
|
yield self.currvalue
|
|
self.currvalue = next(self.it) # Exit on StopIteration
|
|
self.currkey = self.keyfunc(self.currvalue)
|
|
|
|
|
|
class NetAppESeriesStoragePool(object):
|
|
def __init__(self):
|
|
self._sp_drives_cached = None
|
|
|
|
self._size_unit_map = dict(
|
|
bytes=1,
|
|
b=1,
|
|
kb=1024,
|
|
mb=1024 ** 2,
|
|
gb=1024 ** 3,
|
|
tb=1024 ** 4,
|
|
pb=1024 ** 5,
|
|
eb=1024 ** 6,
|
|
zb=1024 ** 7,
|
|
yb=1024 ** 8
|
|
)
|
|
|
|
argument_spec = basic_auth_argument_spec()
|
|
argument_spec.update(dict(
|
|
api_username=dict(type='str', required=True),
|
|
api_password=dict(type='str', required=True, no_log=True),
|
|
api_url=dict(type='str', required=True),
|
|
state=dict(required=True, choices=['present', 'absent'], type='str'),
|
|
ssid=dict(required=True, type='str'),
|
|
name=dict(required=True, type='str'),
|
|
criteria_size_unit=dict(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'),
|
|
criteria_drive_min_size=dict(type='int'),
|
|
criteria_drive_require_fde=dict(type='bool'),
|
|
criteria_min_usable_capacity=dict(type='int'),
|
|
raid_level=dict(
|
|
choices=['raidUnsupported', 'raidAll', 'raid0', 'raid1', 'raid3', 'raid5', 'raid6', 'raidDiskPool']),
|
|
erase_secured_drives=dict(type='bool'),
|
|
log_path=dict(type='str'),
|
|
remove_drives=dict(type='list'),
|
|
secure_pool=dict(type='bool', default=False),
|
|
reserve_drive_count=dict(type='int'),
|
|
remove_volumes=dict(type='bool', default=False)
|
|
))
|
|
|
|
self.module = AnsibleModule(
|
|
argument_spec=argument_spec,
|
|
required_if=[
|
|
('state', 'present', ['raid_level'])
|
|
],
|
|
mutually_exclusive=[
|
|
|
|
],
|
|
# TODO: update validation for various selection criteria
|
|
supports_check_mode=True
|
|
)
|
|
|
|
p = self.module.params
|
|
|
|
log_path = p['log_path']
|
|
|
|
# logging setup
|
|
self._logger = logging.getLogger(self.__class__.__name__)
|
|
self.debug = self._logger.debug
|
|
|
|
if log_path:
|
|
logging.basicConfig(level=logging.DEBUG, filename=log_path)
|
|
|
|
self.state = p['state']
|
|
self.ssid = p['ssid']
|
|
self.name = p['name']
|
|
self.validate_certs = p['validate_certs']
|
|
|
|
self.criteria_drive_count = p['criteria_drive_count']
|
|
self.criteria_drive_type = p['criteria_drive_type']
|
|
self.criteria_size_unit = p['criteria_size_unit']
|
|
self.criteria_drive_min_size = p['criteria_drive_min_size']
|
|
self.criteria_min_usable_capacity = p['criteria_min_usable_capacity']
|
|
self.criteria_drive_interface_type = p['criteria_drive_interface_type']
|
|
self.criteria_drive_require_fde = p['criteria_drive_require_fde']
|
|
|
|
self.raid_level = p['raid_level']
|
|
self.erase_secured_drives = p['erase_secured_drives']
|
|
self.remove_drives = p['remove_drives']
|
|
self.secure_pool = p['secure_pool']
|
|
self.reserve_drive_count = p['reserve_drive_count']
|
|
self.remove_volumes = p['remove_volumes']
|
|
|
|
try:
|
|
self.api_usr = p['api_username']
|
|
self.api_pwd = p['api_password']
|
|
self.api_url = p['api_url']
|
|
except KeyError:
|
|
self.module.fail_json(msg="You must pass in api_username "
|
|
"and api_password and api_url to the module.")
|
|
|
|
self.post_headers = dict(Accept="application/json")
|
|
self.post_headers['Content-Type'] = 'application/json'
|
|
|
|
# Quick and dirty drive selector, since the one provided by web service proxy is broken for min_disk_size as of 2016-03-12.
|
|
# Doesn't really need to be a class once this is in module_utils or retired- just groups everything together so we
|
|
# can copy/paste to other modules more easily.
|
|
# Filters all disks by specified criteria, then groups remaining disks by capacity, interface and disk type, and selects
|
|
# the first set that matches the specified count and/or aggregate capacity.
|
|
# class DriveSelector(object):
|
|
def filter_drives(
|
|
self,
|
|
drives, # raw drives resp
|
|
interface_type=None, # sas, sata, fibre, etc
|
|
drive_type=None, # ssd/hdd
|
|
spindle_speed=None, # 7200, 10000, 15000, ssd (=0)
|
|
min_drive_size=None,
|
|
max_drive_size=None,
|
|
fde_required=None,
|
|
size_unit='gb',
|
|
min_total_capacity=None,
|
|
min_drive_count=None,
|
|
exact_drive_count=None,
|
|
raid_level=None
|
|
):
|
|
if min_total_capacity is None and exact_drive_count is None:
|
|
raise Exception("One of criteria_min_total_capacity or criteria_drive_count must be specified.")
|
|
|
|
if min_total_capacity:
|
|
min_total_capacity = min_total_capacity * self._size_unit_map[size_unit]
|
|
|
|
# filter clearly invalid/unavailable drives first
|
|
drives = select(lambda d: self._is_valid_drive(d), drives)
|
|
|
|
if interface_type:
|
|
drives = select(lambda d: d['phyDriveType'] == interface_type, drives)
|
|
|
|
if drive_type:
|
|
drives = select(lambda d: d['driveMediaType'] == drive_type, drives)
|
|
|
|
if spindle_speed is not None: # 0 is valid for ssds
|
|
drives = select(lambda d: d['spindleSpeed'] == spindle_speed, drives)
|
|
|
|
if min_drive_size:
|
|
min_drive_size_bytes = min_drive_size * self._size_unit_map[size_unit]
|
|
drives = select(lambda d: int(d['rawCapacity']) >= min_drive_size_bytes, drives)
|
|
|
|
if max_drive_size:
|
|
max_drive_size_bytes = max_drive_size * self._size_unit_map[size_unit]
|
|
drives = select(lambda d: int(d['rawCapacity']) <= max_drive_size_bytes, drives)
|
|
|
|
if fde_required:
|
|
drives = select(lambda d: d['fdeCapable'], drives)
|
|
|
|
# initial implementation doesn't have a preference for any of these values...
|
|
# just return the first set we find that matches the requested disk count and/or minimum total capacity
|
|
for (cur_capacity, drives_by_capacity) in groupby(drives, lambda d: int(d['rawCapacity'])):
|
|
for (cur_interface_type, drives_by_interface_type) in groupby(drives_by_capacity,
|
|
lambda d: d['phyDriveType']):
|
|
for (cur_drive_type, drives_by_drive_type) in groupby(drives_by_interface_type,
|
|
lambda d: d['driveMediaType']):
|
|
# listify so we can consume more than once
|
|
drives_by_drive_type = list(drives_by_drive_type)
|
|
candidate_set = list() # reset candidate list on each iteration of the innermost loop
|
|
|
|
if exact_drive_count:
|
|
if len(drives_by_drive_type) < exact_drive_count:
|
|
continue # we know this set is too small, move on
|
|
|
|
for drive in drives_by_drive_type:
|
|
candidate_set.append(drive)
|
|
if self._candidate_set_passes(candidate_set, min_capacity_bytes=min_total_capacity,
|
|
min_drive_count=min_drive_count,
|
|
exact_drive_count=exact_drive_count, raid_level=raid_level):
|
|
return candidate_set
|
|
|
|
raise Exception("couldn't find an available set of disks to match specified criteria")
|
|
|
|
def _is_valid_drive(self, d):
|
|
is_valid = d['available'] \
|
|
and d['status'] == 'optimal' \
|
|
and not d['pfa'] \
|
|
and not d['removed'] \
|
|
and not d['uncertified'] \
|
|
and not d['invalidDriveData'] \
|
|
and not d['nonRedundantAccess']
|
|
|
|
return is_valid
|
|
|
|
def _candidate_set_passes(self, candidate_set, min_capacity_bytes=None, min_drive_count=None,
|
|
exact_drive_count=None, raid_level=None):
|
|
if not self._is_drive_count_valid(len(candidate_set), min_drive_count=min_drive_count,
|
|
exact_drive_count=exact_drive_count, raid_level=raid_level):
|
|
return False
|
|
# TODO: this assumes candidate_set is all the same size- if we want to allow wastage, need to update to use min size of set
|
|
if min_capacity_bytes is not None and self._calculate_usable_capacity(int(candidate_set[0]['rawCapacity']),
|
|
len(candidate_set),
|
|
raid_level=raid_level) < min_capacity_bytes:
|
|
return False
|
|
|
|
return True
|
|
|
|
def _calculate_usable_capacity(self, disk_size_bytes, disk_count, raid_level=None):
|
|
if raid_level in [None, 'raid0']:
|
|
return disk_size_bytes * disk_count
|
|
if raid_level == 'raid1':
|
|
return (disk_size_bytes * disk_count) / 2
|
|
if raid_level in ['raid3', 'raid5']:
|
|
return (disk_size_bytes * disk_count) - disk_size_bytes
|
|
if raid_level in ['raid6', 'raidDiskPool']:
|
|
return (disk_size_bytes * disk_count) - (disk_size_bytes * 2)
|
|
raise Exception("unsupported raid_level: %s" % raid_level)
|
|
|
|
def _is_drive_count_valid(self, drive_count, min_drive_count=0, exact_drive_count=None, raid_level=None):
|
|
if exact_drive_count and exact_drive_count != drive_count:
|
|
return False
|
|
if raid_level == 'raidDiskPool':
|
|
if drive_count < 11:
|
|
return False
|
|
if raid_level == 'raid1':
|
|
if drive_count % 2 != 0:
|
|
return False
|
|
if raid_level in ['raid3', 'raid5']:
|
|
if drive_count < 3:
|
|
return False
|
|
if raid_level == 'raid6':
|
|
if drive_count < 4:
|
|
return False
|
|
if min_drive_count and drive_count < min_drive_count:
|
|
return False
|
|
|
|
return True
|
|
|
|
def get_storage_pool(self, storage_pool_name):
|
|
# global ifilter
|
|
self.debug("fetching storage pools")
|
|
# map the storage pool name to its id
|
|
try:
|
|
(rc, resp) = request(self.api_url + "/storage-systems/%s/storage-pools" % (self.ssid),
|
|
headers=dict(Accept="application/json"), url_username=self.api_usr,
|
|
url_password=self.api_pwd, validate_certs=self.validate_certs)
|
|
except Exception:
|
|
err = get_exception()
|
|
rc = err.args[0]
|
|
if rc == 404 and self.state == 'absent':
|
|
self.module.exit_json(
|
|
msg="Storage pool [%s] did not exist." % (self.name))
|
|
else:
|
|
err = get_exception()
|
|
self.module.exit_json(
|
|
msg="Failed to get storage pools. Array id [%s]. Error[%s]. State[%s]. RC[%s]." %
|
|
(self.ssid, str(err), self.state, rc))
|
|
|
|
self.debug("searching for storage pool '%s'" % storage_pool_name)
|
|
|
|
pool_detail = next(select(lambda a: a['name'] == storage_pool_name, resp), None)
|
|
|
|
if pool_detail:
|
|
found = 'found'
|
|
else:
|
|
found = 'not found'
|
|
self.debug(found)
|
|
|
|
return pool_detail
|
|
|
|
def get_candidate_disks(self):
|
|
self.debug("getting candidate disks...")
|
|
|
|
# driveCapacityMin is broken on /drives POST. Per NetApp request we built our own
|
|
# switch back to commented code below if it gets fixed
|
|
# drives_req = dict(
|
|
# driveCount = self.criteria_drive_count,
|
|
# sizeUnit = 'mb',
|
|
# raidLevel = self.raid_level
|
|
# )
|
|
#
|
|
# if self.criteria_drive_type:
|
|
# drives_req['driveType'] = self.criteria_drive_type
|
|
# if self.criteria_disk_min_aggregate_size_mb:
|
|
# drives_req['targetUsableCapacity'] = self.criteria_disk_min_aggregate_size_mb
|
|
#
|
|
# # TODO: this arg appears to be ignored, uncomment if it isn't
|
|
# #if self.criteria_disk_min_size_gb:
|
|
# # drives_req['driveCapacityMin'] = self.criteria_disk_min_size_gb * 1024
|
|
# (rc,drives_resp) = request(self.api_url + "/storage-systems/%s/drives" % (self.ssid), data=json.dumps(drives_req), headers=self.post_headers, method='POST', url_username=self.api_usr, url_password=self.api_pwd, validate_certs=self.validate_certs)
|
|
#
|
|
# if rc == 204:
|
|
# self.module.fail_json(msg='Cannot find disks to match requested criteria for storage pool')
|
|
|
|
# disk_ids = [d['id'] for d in drives_resp]
|
|
|
|
try:
|
|
(rc, drives_resp) = request(self.api_url + "/storage-systems/%s/drives" % (self.ssid), method='GET',
|
|
url_username=self.api_usr, url_password=self.api_pwd,
|
|
validate_certs=self.validate_certs)
|
|
except:
|
|
err = get_exception()
|
|
self.module.exit_json(
|
|
msg="Failed to fetch disk drives. Array id [%s]. Error[%s]." % (self.ssid, str(err)))
|
|
|
|
try:
|
|
candidate_set = self.filter_drives(drives_resp,
|
|
exact_drive_count=self.criteria_drive_count,
|
|
drive_type=self.criteria_drive_type,
|
|
min_drive_size=self.criteria_drive_min_size,
|
|
raid_level=self.raid_level,
|
|
size_unit=self.criteria_size_unit,
|
|
min_total_capacity=self.criteria_min_usable_capacity,
|
|
interface_type=self.criteria_drive_interface_type,
|
|
fde_required=self.criteria_drive_require_fde
|
|
)
|
|
except:
|
|
err = get_exception()
|
|
self.module.fail_json(
|
|
msg="Failed to allocate adequate drive count. Id [%s]. Error [%s]." % (self.ssid, str(err)))
|
|
|
|
disk_ids = [d['id'] for d in candidate_set]
|
|
|
|
return disk_ids
|
|
|
|
def create_storage_pool(self):
|
|
self.debug("creating storage pool...")
|
|
|
|
sp_add_req = dict(
|
|
raidLevel=self.raid_level,
|
|
diskDriveIds=self.disk_ids,
|
|
name=self.name
|
|
)
|
|
|
|
if self.erase_secured_drives:
|
|
sp_add_req['eraseSecuredDrives'] = self.erase_secured_drives
|
|
|
|
try:
|
|
(rc, resp) = request(self.api_url + "/storage-systems/%s/storage-pools" % (self.ssid),
|
|
data=json.dumps(sp_add_req), headers=self.post_headers, method='POST',
|
|
url_username=self.api_usr, url_password=self.api_pwd,
|
|
validate_certs=self.validate_certs,
|
|
timeout=120)
|
|
except:
|
|
err = get_exception()
|
|
pool_id = self.pool_detail['id']
|
|
self.module.exit_json(
|
|
msg="Failed to create storage pool. Pool id [%s]. Array id [%s]. Error[%s]." % (pool_id,
|
|
self.ssid,
|
|
str(err)))
|
|
|
|
self.pool_detail = self.get_storage_pool(self.name)
|
|
|
|
if self.secure_pool:
|
|
secure_pool_data = dict(securePool=True)
|
|
try:
|
|
(retc, r) = request(
|
|
self.api_url + "/storage-systems/%s/storage-pools/%s" % (self.ssid, self.pool_detail['id']),
|
|
data=json.dumps(secure_pool_data), headers=self.post_headers, method='POST',
|
|
url_username=self.api_usr,
|
|
url_password=self.api_pwd, validate_certs=self.validate_certs, timeout=120, ignore_errors=True)
|
|
except:
|
|
err = get_exception()
|
|
pool_id = self.pool_detail['id']
|
|
self.module.exit_json(
|
|
msg="Failed to update storage pool. Pool id [%s]. Array id [%s]. Error[%s]." % (pool_id,
|
|
self.ssid,
|
|
str(err)))
|
|
|
|
@property
|
|
def needs_raid_level_migration(self):
|
|
current_raid_level = self.pool_detail['raidLevel']
|
|
needs_migration = self.raid_level != current_raid_level
|
|
|
|
if needs_migration: # sanity check some things so we can fail early/check-mode
|
|
if current_raid_level == 'raidDiskPool':
|
|
self.module.fail_json(msg="raid level cannot be changed for disk pools")
|
|
|
|
return needs_migration
|
|
|
|
def migrate_raid_level(self):
|
|
self.debug("migrating storage pool to raid level '%s'..." % self.raid_level)
|
|
sp_raid_migrate_req = dict(
|
|
raidLevel=self.raid_level
|
|
)
|
|
try:
|
|
(rc, resp) = request(
|
|
self.api_url + "/storage-systems/%s/storage-pools/%s/raid-type-migration" % (self.ssid,
|
|
self.name),
|
|
data=json.dumps(sp_raid_migrate_req), headers=self.post_headers, method='POST',
|
|
url_username=self.api_usr,
|
|
url_password=self.api_pwd, validate_certs=self.validate_certs, timeout=120)
|
|
except:
|
|
err = get_exception()
|
|
pool_id = self.pool_detail['id']
|
|
self.module.exit_json(
|
|
msg="Failed to change the raid level of storage pool. Pool id [%s]. Array id [%s]. Error[%s]." % (
|
|
pool_id, self.ssid, str(err)))
|
|
|
|
@property
|
|
def sp_drives(self, exclude_hotspares=True):
|
|
if not self._sp_drives_cached:
|
|
|
|
self.debug("fetching drive list...")
|
|
try:
|
|
(rc, resp) = request(self.api_url + "/storage-systems/%s/drives" % (self.ssid), method='GET',
|
|
url_username=self.api_usr, url_password=self.api_pwd,
|
|
validate_certs=self.validate_certs)
|
|
except:
|
|
err = get_exception()
|
|
pool_id = self.pool_detail['id']
|
|
self.module.exit_json(
|
|
msg="Failed to fetch disk drives. Pool id [%s]. Array id [%s]. Error[%s]." % (pool_id, self.ssid, str(err)))
|
|
|
|
sp_id = self.pool_detail['id']
|
|
if exclude_hotspares:
|
|
self._sp_drives_cached = [d for d in resp if d['currentVolumeGroupRef'] == sp_id and not d['hotSpare']]
|
|
else:
|
|
self._sp_drives_cached = [d for d in resp if d['currentVolumeGroupRef'] == sp_id]
|
|
|
|
return self._sp_drives_cached
|
|
|
|
@property
|
|
def reserved_drive_count_differs(self):
|
|
if int(self.pool_detail['volumeGroupData']['diskPoolData'][
|
|
'reconstructionReservedDriveCount']) != self.reserve_drive_count:
|
|
return True
|
|
return False
|
|
|
|
@property
|
|
def needs_expansion(self):
|
|
if self.criteria_drive_count > len(self.sp_drives):
|
|
return True
|
|
# TODO: is totalRaidedSpace the best attribute for "how big is this SP"?
|
|
if self.criteria_min_usable_capacity and \
|
|
(self.criteria_min_usable_capacity * self._size_unit_map[self.criteria_size_unit]) > int(self.pool_detail['totalRaidedSpace']):
|
|
return True
|
|
|
|
return False
|
|
|
|
def get_expansion_candidate_drives(self):
|
|
# sanity checks; don't call this if we can't/don't need to expand
|
|
if not self.needs_expansion:
|
|
self.module.fail_json(msg="can't get expansion candidates when pool doesn't need expansion")
|
|
|
|
self.debug("fetching expansion candidate drives...")
|
|
try:
|
|
(rc, resp) = request(
|
|
self.api_url + "/storage-systems/%s/storage-pools/%s/expand" % (self.ssid,
|
|
self.pool_detail['id']),
|
|
method='GET', url_username=self.api_usr, url_password=self.api_pwd, validate_certs=self.validate_certs,
|
|
timeout=120)
|
|
except:
|
|
err = get_exception()
|
|
pool_id = self.pool_detail['id']
|
|
self.module.exit_json(
|
|
msg="Failed to fetch candidate drives for storage pool. Pool id [%s]. Array id [%s]. Error[%s]." % (
|
|
pool_id, self.ssid, str(err)))
|
|
|
|
current_drive_count = len(self.sp_drives)
|
|
current_capacity_bytes = int(self.pool_detail['totalRaidedSpace']) # TODO: is this the right attribute to use?
|
|
|
|
if self.criteria_min_usable_capacity:
|
|
requested_capacity_bytes = self.criteria_min_usable_capacity * self._size_unit_map[self.criteria_size_unit]
|
|
else:
|
|
requested_capacity_bytes = current_capacity_bytes
|
|
|
|
if self.criteria_drive_count:
|
|
minimum_disks_to_add = max((self.criteria_drive_count - current_drive_count), 1)
|
|
else:
|
|
minimum_disks_to_add = 1
|
|
|
|
minimum_bytes_to_add = max(requested_capacity_bytes - current_capacity_bytes, 0)
|
|
|
|
# FUTURE: allow more control over expansion candidate selection?
|
|
# loop over candidate disk sets and add until we've met both criteria
|
|
|
|
added_drive_count = 0
|
|
added_capacity_bytes = 0
|
|
|
|
drives_to_add = set()
|
|
|
|
for s in resp:
|
|
# don't trust the API not to give us duplicate drives across candidate sets, especially in multi-drive sets
|
|
candidate_drives = s['drives']
|
|
if len(drives_to_add.intersection(candidate_drives)) != 0:
|
|
# duplicate, skip
|
|
continue
|
|
drives_to_add.update(candidate_drives)
|
|
added_drive_count += len(candidate_drives)
|
|
added_capacity_bytes += int(s['usableCapacity'])
|
|
|
|
if added_drive_count >= minimum_disks_to_add and added_capacity_bytes >= minimum_bytes_to_add:
|
|
break
|
|
|
|
if (added_drive_count < minimum_disks_to_add) or (added_capacity_bytes < minimum_bytes_to_add):
|
|
self.module.fail_json(
|
|
msg="unable to find at least %s drives to add that would add at least %s bytes of capacity" % (
|
|
minimum_disks_to_add, minimum_bytes_to_add))
|
|
|
|
return list(drives_to_add)
|
|
|
|
def expand_storage_pool(self):
|
|
drives_to_add = self.get_expansion_candidate_drives()
|
|
|
|
self.debug("adding %s drives to storage pool..." % len(drives_to_add))
|
|
sp_expand_req = dict(
|
|
drives=drives_to_add
|
|
)
|
|
try:
|
|
request(
|
|
self.api_url + "/storage-systems/%s/storage-pools/%s/expand" % (self.ssid,
|
|
self.pool_detail['id']),
|
|
data=json.dumps(sp_expand_req), headers=self.post_headers, method='POST', url_username=self.api_usr,
|
|
url_password=self.api_pwd, validate_certs=self.validate_certs, timeout=120)
|
|
except:
|
|
err = get_exception()
|
|
pool_id = self.pool_detail['id']
|
|
self.module.exit_json(
|
|
msg="Failed to add drives to storage pool. Pool id [%s]. Array id [%s]. Error[%s]." % (pool_id,
|
|
self.ssid,
|
|
str(
|
|
err)))
|
|
|
|
# TODO: check response
|
|
# TODO: support blocking wait?
|
|
|
|
def reduce_drives(self, drive_list):
|
|
if all(drive in drive_list for drive in self.sp_drives):
|
|
# all the drives passed in are present in the system
|
|
pass
|
|
else:
|
|
self.module.fail_json(
|
|
msg="One of the drives you wish to remove does not currently exist in the storage pool you specified")
|
|
|
|
try:
|
|
(rc, resp) = request(
|
|
self.api_url + "/storage-systems/%s/storage-pools/%s/reduction" % (self.ssid,
|
|
self.pool_detail['id']),
|
|
data=json.dumps(drive_list), headers=self.post_headers, method='POST', url_username=self.api_usr,
|
|
url_password=self.api_pwd, validate_certs=self.validate_certs, timeout=120)
|
|
except:
|
|
err = get_exception()
|
|
pool_id = self.pool_detail['id']
|
|
self.module.exit_json(
|
|
msg="Failed to remove drives from storage pool. Pool id [%s]. Array id [%s]. Error[%s]." % (
|
|
pool_id, self.ssid, str(err)))
|
|
|
|
def update_reserve_drive_count(self, qty):
|
|
data = dict(reservedDriveCount=qty)
|
|
try:
|
|
(rc, resp) = request(
|
|
self.api_url + "/storage-systems/%s/storage-pools/%s" % (self.ssid, self.pool_detail['id']),
|
|
data=json.dumps(data), headers=self.post_headers, method='POST', url_username=self.api_usr,
|
|
url_password=self.api_pwd, validate_certs=self.validate_certs, timeout=120)
|
|
except:
|
|
err = get_exception()
|
|
pool_id = self.pool_detail['id']
|
|
self.module.exit_json(
|
|
msg="Failed to update reserve drive count. Pool id [%s]. Array id [%s]. Error[%s]." % (pool_id,
|
|
self.ssid,
|
|
str(
|
|
err)))
|
|
|
|
def apply(self):
|
|
changed = False
|
|
pool_exists = False
|
|
|
|
self.pool_detail = self.get_storage_pool(self.name)
|
|
|
|
if self.pool_detail:
|
|
pool_exists = True
|
|
pool_id = self.pool_detail['id']
|
|
|
|
if self.state == 'absent':
|
|
self.debug("CHANGED: storage pool exists, but requested state is 'absent'")
|
|
changed = True
|
|
elif self.state == 'present':
|
|
# sanity checks first- we can't change these, so we'll bomb if they're specified
|
|
if self.criteria_drive_type and self.criteria_drive_type != self.pool_detail['driveMediaType']:
|
|
self.module.fail_json(
|
|
msg="drive media type %s cannot be changed to %s" % (self.pool_detail['driveMediaType'],
|
|
self.criteria_drive_type))
|
|
|
|
# now the things we can change...
|
|
if self.needs_expansion:
|
|
self.debug("CHANGED: storage pool needs expansion")
|
|
changed = True
|
|
|
|
if self.needs_raid_level_migration:
|
|
self.debug(
|
|
"CHANGED: raid level migration required; storage pool uses '%s', requested is '%s'" % (
|
|
self.pool_detail['raidLevel'], self.raid_level))
|
|
changed = True
|
|
|
|
# if self.reserved_drive_count_differs:
|
|
# changed = True
|
|
|
|
# TODO: validate other state details? (pool priority, alert threshold)
|
|
|
|
# per FPoole and others, pool reduce operations will not be supported. Automatic "smart" reduction
|
|
# presents a difficult parameter issue, as the disk count can increase due to expansion, so we
|
|
# can't just use disk count > criteria_drive_count.
|
|
|
|
else: # pool does not exist
|
|
if self.state == 'present':
|
|
self.debug("CHANGED: storage pool does not exist, but requested state is 'present'")
|
|
changed = True
|
|
|
|
# ensure we can get back a workable set of disks
|
|
# (doing this early so candidate selection runs under check mode)
|
|
self.disk_ids = self.get_candidate_disks()
|
|
else:
|
|
self.module.exit_json(msg="Storage pool [%s] did not exist." % (self.name))
|
|
|
|
if changed and not self.module.check_mode:
|
|
# apply changes
|
|
if self.state == 'present':
|
|
if not pool_exists:
|
|
self.create_storage_pool()
|
|
else: # pool exists but differs, modify...
|
|
if self.needs_expansion:
|
|
self.expand_storage_pool()
|
|
|
|
if self.remove_drives:
|
|
self.reduce_drives(self.remove_drives)
|
|
|
|
if self.needs_raid_level_migration:
|
|
self.migrate_raid_level()
|
|
|
|
# if self.reserved_drive_count_differs:
|
|
# self.update_reserve_drive_count(self.reserve_drive_count)
|
|
|
|
if self.secure_pool:
|
|
secure_pool_data = dict(securePool=True)
|
|
try:
|
|
(retc, r) = request(
|
|
self.api_url + "/storage-systems/%s/storage-pools/%s" % (self.ssid,
|
|
self.pool_detail[
|
|
'id']),
|
|
data=json.dumps(secure_pool_data), headers=self.post_headers, method='POST',
|
|
url_username=self.api_usr, url_password=self.api_pwd,
|
|
validate_certs=self.validate_certs, timeout=120, ignore_errors=True)
|
|
except:
|
|
err = get_exception()
|
|
self.module.exit_json(
|
|
msg="Failed to delete storage pool. Pool id [%s]. Array id [%s]. Error[%s]." % (
|
|
pool_id, self.ssid, str(err)))
|
|
|
|
if int(retc) == 422:
|
|
self.module.fail_json(
|
|
msg="Error in enabling secure pool. One of the drives in the specified storage pool is likely not security capable")
|
|
|
|
elif self.state == 'absent':
|
|
# delete the storage pool
|
|
try:
|
|
remove_vol_opt = ''
|
|
if self.remove_volumes:
|
|
remove_vol_opt = '?delete-volumes=true'
|
|
(rc, resp) = request(
|
|
self.api_url + "/storage-systems/%s/storage-pools/%s%s" % (self.ssid, pool_id,
|
|
remove_vol_opt),
|
|
method='DELETE',
|
|
url_username=self.api_usr, url_password=self.api_pwd, validate_certs=self.validate_certs,
|
|
timeout=120)
|
|
except:
|
|
err = get_exception()
|
|
self.module.exit_json(
|
|
msg="Failed to delete storage pool. Pool id [%s]. Array id [%s]. Error[%s]." % (pool_id,
|
|
self.ssid,
|
|
str(err)))
|
|
|
|
self.module.exit_json(changed=changed, **self.pool_detail)
|
|
|
|
|
|
def main():
|
|
sp = NetAppESeriesStoragePool()
|
|
try:
|
|
sp.apply()
|
|
except Exception:
|
|
e = get_exception()
|
|
sp.debug("Exception in apply(): \n%s" % format_exc(e))
|
|
raise
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|