403 lines
16 KiB
Python
403 lines
16 KiB
Python
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import json
|
|
import os
|
|
import time
|
|
|
|
import jsonschema
|
|
from jsonschema import exceptions as json_schema_exc
|
|
|
|
from proliantutils import exception
|
|
from proliantutils.hpssa import constants
|
|
from proliantutils.hpssa import disk_allocator
|
|
from proliantutils.hpssa import objects
|
|
|
|
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
RAID_CONFIG_SCHEMA = os.path.join(CURRENT_DIR, "raid_config_schema.json")
|
|
|
|
|
|
def _update_physical_disk_details(raid_config, server):
|
|
"""Adds the physical disk details to the RAID configuration passed."""
|
|
raid_config['physical_disks'] = []
|
|
physical_drives = server.get_physical_drives()
|
|
for physical_drive in physical_drives:
|
|
physical_drive_dict = physical_drive.get_physical_drive_dict()
|
|
raid_config['physical_disks'].append(physical_drive_dict)
|
|
|
|
|
|
def validate(raid_config):
|
|
"""Validates the RAID configuration provided.
|
|
|
|
This method validates the RAID configuration provided against
|
|
a JSON schema.
|
|
|
|
:param raid_config: The RAID configuration to be validated.
|
|
:raises: InvalidInputError, if validation of the input fails.
|
|
"""
|
|
raid_schema_fobj = open(RAID_CONFIG_SCHEMA, 'r')
|
|
raid_config_schema = json.load(raid_schema_fobj)
|
|
try:
|
|
jsonschema.validate(raid_config, raid_config_schema)
|
|
except json_schema_exc.ValidationError as e:
|
|
raise exception.InvalidInputError(e.message)
|
|
|
|
for logical_disk in raid_config['logical_disks']:
|
|
|
|
# If user has provided 'number_of_physical_disks' or
|
|
# 'physical_disks', validate that they have mentioned at least
|
|
# minimum number of physical disks required for that RAID level.
|
|
raid_level = logical_disk['raid_level']
|
|
min_disks_reqd = constants.RAID_LEVEL_MIN_DISKS[raid_level]
|
|
|
|
no_of_disks_specified = None
|
|
if 'number_of_physical_disks' in logical_disk:
|
|
no_of_disks_specified = logical_disk['number_of_physical_disks']
|
|
elif 'physical_disks' in logical_disk:
|
|
no_of_disks_specified = len(logical_disk['physical_disks'])
|
|
|
|
if (no_of_disks_specified and
|
|
no_of_disks_specified < min_disks_reqd):
|
|
msg = ("RAID level %(raid_level)s requires at least %(number)s "
|
|
"disks." % {'raid_level': raid_level,
|
|
'number': min_disks_reqd})
|
|
raise exception.InvalidInputError(msg)
|
|
|
|
|
|
def _select_controllers_by(server, select_condition, msg):
|
|
"""Filters out the hpssa controllers based on the condition.
|
|
|
|
This method updates the server with only the controller which satisfies
|
|
the condition. The controllers which doesn't satisfies the selection
|
|
condition will be removed from the list.
|
|
|
|
:param server: The object containing all the supported hpssa controllers
|
|
details.
|
|
:param select_condition: A lambda function to select the controllers based
|
|
on requirement.
|
|
:param msg: A String which describes the controller selection.
|
|
:raises exception.HPSSAOperationError, if all the controller are in HBA
|
|
mode.
|
|
"""
|
|
all_controllers = server.controllers
|
|
supported_controllers = [c for c in all_controllers if select_condition(c)]
|
|
|
|
if not supported_controllers:
|
|
reason = ("None of the available SSA controllers %(controllers)s "
|
|
"have %(msg)s"
|
|
% {'controllers': ', '.join([c.id for c in all_controllers]),
|
|
'msg': msg})
|
|
raise exception.HPSSAOperationError(reason=reason)
|
|
|
|
server.controllers = supported_controllers
|
|
|
|
|
|
def create_configuration(raid_config):
|
|
"""Create a RAID configuration on this server.
|
|
|
|
This method creates the given RAID configuration on the
|
|
server based on the input passed.
|
|
:param raid_config: The dictionary containing the requested
|
|
RAID configuration. This data structure should be as follows:
|
|
raid_config = {'logical_disks': [{'raid_level': 1, 'size_gb': 100},
|
|
<info-for-logical-disk-2>
|
|
]}
|
|
:returns: the current raid configuration. This is same as raid_config
|
|
with some extra properties like root_device_hint, volume_name,
|
|
controller, physical_disks, etc filled for each logical disk
|
|
after its creation.
|
|
:raises exception.InvalidInputError, if input is invalid.
|
|
:raises exception.HPSSAOperationError, if all the controllers are in HBA
|
|
mode.
|
|
"""
|
|
server = objects.Server()
|
|
|
|
select_controllers = lambda x: not x.properties.get('HBA Mode Enabled',
|
|
False)
|
|
_select_controllers_by(server, select_controllers, 'RAID enabled')
|
|
|
|
validate(raid_config)
|
|
|
|
# Make sure we create the large disks first. This is avoid the
|
|
# situation that we avoid giving large disks to smaller requests.
|
|
# For example, consider this:
|
|
# - two logical disks - LD1(50), LD(100)
|
|
# - have 4 physical disks - PD1(50), PD2(50), PD3(100), PD4(100)
|
|
#
|
|
# In this case, for RAID1 configuration, if we were to consider
|
|
# LD1 first and allocate PD3 and PD4 for it, then allocation would
|
|
# fail. So follow a particular order for allocation.
|
|
#
|
|
# Also make sure we create the MAX logical_disks the last to make sure
|
|
# we allot only the remaining space available.
|
|
logical_disks_sorted = (
|
|
sorted((x for x in raid_config['logical_disks']
|
|
if x['size_gb'] != "MAX"),
|
|
reverse=True,
|
|
key=lambda x: x['size_gb']) +
|
|
[x for x in raid_config['logical_disks'] if x['size_gb'] == "MAX"])
|
|
|
|
if any(logical_disk['share_physical_disks']
|
|
for logical_disk in logical_disks_sorted
|
|
if 'share_physical_disks' in logical_disk):
|
|
logical_disks_sorted = _sort_shared_logical_disks(logical_disks_sorted)
|
|
|
|
# We figure out the new disk created by recording the wwns
|
|
# before and after the create, and then figuring out the
|
|
# newly found wwn from it.
|
|
wwns_before_create = set([x.wwn for x in
|
|
server.get_logical_drives()])
|
|
|
|
for logical_disk in logical_disks_sorted:
|
|
|
|
if 'physical_disks' not in logical_disk:
|
|
disk_allocator.allocate_disks(logical_disk, server,
|
|
raid_config)
|
|
|
|
controller_id = logical_disk['controller']
|
|
|
|
controller = server.get_controller_by_id(controller_id)
|
|
if not controller:
|
|
msg = ("Unable to find controller named '%(controller)s'."
|
|
" The available controllers are '%(ctrl_list)s'." %
|
|
{'controller': controller_id,
|
|
'ctrl_list': ', '.join(
|
|
[c.id for c in server.controllers])})
|
|
raise exception.InvalidInputError(reason=msg)
|
|
|
|
if 'physical_disks' in logical_disk:
|
|
for physical_disk in logical_disk['physical_disks']:
|
|
disk_obj = controller.get_physical_drive_by_id(physical_disk)
|
|
if not disk_obj:
|
|
msg = ("Unable to find physical disk '%(physical_disk)s' "
|
|
"on '%(controller)s'" %
|
|
{'physical_disk': physical_disk,
|
|
'controller': controller_id})
|
|
raise exception.InvalidInputError(msg)
|
|
|
|
controller.create_logical_drive(logical_disk)
|
|
|
|
# Now find the new logical drive created.
|
|
server.refresh()
|
|
wwns_after_create = set([x.wwn for x in
|
|
server.get_logical_drives()])
|
|
|
|
new_wwn = wwns_after_create - wwns_before_create
|
|
|
|
if not new_wwn:
|
|
reason = ("Newly created logical disk with raid_level "
|
|
"'%(raid_level)s' and size %(size_gb)s GB not "
|
|
"found." % {'raid_level': logical_disk['raid_level'],
|
|
'size_gb': logical_disk['size_gb']})
|
|
raise exception.HPSSAOperationError(reason=reason)
|
|
|
|
new_logical_disk = server.get_logical_drive_by_wwn(new_wwn.pop())
|
|
new_log_drive_properties = new_logical_disk.get_logical_drive_dict()
|
|
logical_disk.update(new_log_drive_properties)
|
|
|
|
wwns_before_create = wwns_after_create.copy()
|
|
|
|
_update_physical_disk_details(raid_config, server)
|
|
return raid_config
|
|
|
|
|
|
def _sort_shared_logical_disks(logical_disks):
|
|
"""Sort the logical disks based on the following conditions.
|
|
|
|
When the share_physical_disks is True make sure we create the volume
|
|
which needs more disks first. This avoids the situation of insufficient
|
|
disks for some logical volume request.
|
|
|
|
For example,
|
|
- two logical disk with number of disks - LD1(3), LD2(4)
|
|
- have 4 physical disks
|
|
In this case, if we consider LD1 first then LD2 will fail since not
|
|
enough disks available to create LD2. So follow a order for allocation
|
|
when share_physical_disks is True.
|
|
|
|
Also RAID1 can share only when there is logical volume with only 2 disks.
|
|
So make sure we create RAID 1 first when share_physical_disks is True.
|
|
|
|
And RAID 1+0 can share only when the logical volume with even number of
|
|
disks.
|
|
:param logical_disks: 'logical_disks' to be sorted for shared logical
|
|
disks.
|
|
:returns: the logical disks sorted based the above conditions.
|
|
"""
|
|
is_shared = (lambda x: True if ('share_physical_disks' in x and
|
|
x['share_physical_disks']) else False)
|
|
num_of_disks = (lambda x: x['number_of_physical_disks']
|
|
if 'number_of_physical_disks' in x else
|
|
constants.RAID_LEVEL_MIN_DISKS[x['raid_level']])
|
|
|
|
# Separate logical disks based on share_physical_disks value.
|
|
# 'logical_disks_shared' when share_physical_disks is True and
|
|
# 'logical_disks_nonshared' when share_physical_disks is False
|
|
logical_disks_shared = []
|
|
logical_disks_nonshared = []
|
|
for x in logical_disks:
|
|
target = (logical_disks_shared if is_shared(x)
|
|
else logical_disks_nonshared)
|
|
target.append(x)
|
|
|
|
# Separete logical disks with raid 1 from the 'logical_disks_shared' into
|
|
# 'logical_disks_shared_raid1' and remaining as
|
|
# 'logical_disks_shared_excl_raid1'.
|
|
logical_disks_shared_raid1 = []
|
|
logical_disks_shared_excl_raid1 = []
|
|
for x in logical_disks_shared:
|
|
target = (logical_disks_shared_raid1 if x['raid_level'] == '1'
|
|
else logical_disks_shared_excl_raid1)
|
|
target.append(x)
|
|
|
|
# Sort the 'logical_disks_shared' in reverse order based on
|
|
# 'number_of_physical_disks' attribute, if provided, otherwise minimum
|
|
# disks required to create the logical volume.
|
|
logical_disks_shared = sorted(logical_disks_shared_excl_raid1,
|
|
reverse=True,
|
|
key=num_of_disks)
|
|
|
|
# Move RAID 1+0 to first in 'logical_disks_shared' when number of physical
|
|
# disks needed to create logical volume cannot be shared with odd number of
|
|
# disks and disks higher than that of RAID 1+0.
|
|
check = True
|
|
for x in logical_disks_shared:
|
|
if x['raid_level'] == "1+0":
|
|
x_num = num_of_disks(x)
|
|
for y in logical_disks_shared:
|
|
if y['raid_level'] != "1+0":
|
|
y_num = num_of_disks(y)
|
|
if x_num < y_num:
|
|
check = (True if y_num % 2 == 0 else False)
|
|
if check:
|
|
break
|
|
if not check:
|
|
logical_disks_shared.remove(x)
|
|
logical_disks_shared.insert(0, x)
|
|
check = True
|
|
|
|
# Final 'logical_disks_sorted' list should have non shared logical disks
|
|
# first, followed by shared logical disks with RAID 1, and finally by the
|
|
# shared logical disks sorted based on number of disks and RAID 1+0
|
|
# condition.
|
|
logical_disks_sorted = (logical_disks_nonshared +
|
|
logical_disks_shared_raid1 +
|
|
logical_disks_shared)
|
|
return logical_disks_sorted
|
|
|
|
|
|
def delete_configuration():
|
|
"""Delete a RAID configuration on this server.
|
|
|
|
:returns: the current RAID configuration after deleting all
|
|
the logical disks.
|
|
"""
|
|
server = objects.Server()
|
|
|
|
select_controllers = lambda x: not x.properties.get('HBA Mode Enabled',
|
|
False)
|
|
_select_controllers_by(server, select_controllers, 'RAID enabled')
|
|
|
|
for controller in server.controllers:
|
|
# Trigger delete only if there is some RAID array, otherwise
|
|
# hpssacli/ssacli will fail saying "no logical drives found.".
|
|
if controller.raid_arrays:
|
|
controller.delete_all_logical_drives()
|
|
return get_configuration()
|
|
|
|
|
|
def get_configuration():
|
|
"""Get the current RAID configuration.
|
|
|
|
Get the RAID configuration from the server and return it
|
|
as a dictionary.
|
|
|
|
:returns: A dictionary of the below format.
|
|
raid_config = {
|
|
'logical_disks': [{
|
|
'size_gb': 100,
|
|
'raid_level': 1,
|
|
'physical_disks': [
|
|
'5I:0:1',
|
|
'5I:0:2'],
|
|
'controller': 'Smart array controller'
|
|
},
|
|
]
|
|
}
|
|
"""
|
|
server = objects.Server()
|
|
logical_drives = server.get_logical_drives()
|
|
raid_config = {}
|
|
raid_config['logical_disks'] = []
|
|
|
|
for logical_drive in logical_drives:
|
|
logical_drive_dict = logical_drive.get_logical_drive_dict()
|
|
raid_config['logical_disks'].append(logical_drive_dict)
|
|
|
|
_update_physical_disk_details(raid_config, server)
|
|
return raid_config
|
|
|
|
|
|
def has_erase_completed():
|
|
server = objects.Server()
|
|
drives = server.get_physical_drives()
|
|
if any((drive.erase_status == 'Erase In Progress')
|
|
for drive in drives):
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
|
|
def erase_devices():
|
|
"""Erase all the drives on this server.
|
|
|
|
This method performs sanitize erase on all the supported physical drives
|
|
in this server. This erase cannot be performed on logical drives.
|
|
|
|
:returns: a dictionary of controllers with drives and the erase status.
|
|
:raises exception.HPSSAException, if none of the drives support
|
|
sanitize erase.
|
|
"""
|
|
server = objects.Server()
|
|
|
|
for controller in server.controllers:
|
|
drives = [x for x in controller.unassigned_physical_drives
|
|
if (x.get_physical_drive_dict().get('erase_status', '')
|
|
== 'OK')]
|
|
if drives:
|
|
controller.erase_devices(drives)
|
|
|
|
while not has_erase_completed():
|
|
time.sleep(300)
|
|
|
|
server.refresh()
|
|
|
|
status = {}
|
|
for controller in server.controllers:
|
|
drive_status = {x.id: x.erase_status
|
|
for x in controller.unassigned_physical_drives}
|
|
sanitize_supported = controller.properties.get(
|
|
'Sanitize Erase Supported', 'False')
|
|
if sanitize_supported == 'False':
|
|
msg = ("Drives overwritten with zeros because sanitize erase "
|
|
"is not supported on the controller.")
|
|
else:
|
|
msg = ("Sanitize Erase performed on the disks attached to "
|
|
"the controller.")
|
|
|
|
drive_status.update({'Summary': msg})
|
|
status[controller.id] = drive_status
|
|
|
|
return status
|