Add Redfish RAID management to Ironic
The Redfish hardware type does not currently implement the RAID hardware interface. This patch implements the Redfish RAID interface, allowing operators to specify the desired RAID configuration on Ironic Redfish nodes. Story: 2003514 Task: 24789 Depends-On: https://review.opendev.org/c/openstack/sushy/+/774532 Change-Id: Icf5ca865e0c1e168b96659229df622698bea1503
This commit is contained in:
parent
5857fa802d
commit
d7f4da2376
@ -11,7 +11,7 @@ python-dracclient>=5.1.0,<6.0.0
|
||||
python-xclarityclient>=0.1.6
|
||||
|
||||
# The Redfish hardware type uses the Sushy library
|
||||
sushy>=3.6.0
|
||||
sushy>=3.7.0
|
||||
|
||||
# Ansible-deploy interface
|
||||
ansible>=2.7
|
||||
|
@ -90,6 +90,16 @@ opts = [
|
||||
default=60,
|
||||
help=_('Number of seconds to wait between checking for '
|
||||
'failed firmware update tasks')),
|
||||
cfg.IntOpt('raid_config_status_interval',
|
||||
min=0,
|
||||
default=60,
|
||||
help=_('Number of seconds to wait between checking for '
|
||||
'completed raid config tasks')),
|
||||
cfg.IntOpt('raid_config_fail_interval',
|
||||
min=0,
|
||||
default=60,
|
||||
help=_('Number of seconds to wait between checking for '
|
||||
'failed raid config tasks')),
|
||||
]
|
||||
|
||||
|
||||
|
1119
ironic/drivers/modules/redfish/raid.py
Normal file
1119
ironic/drivers/modules/redfish/raid.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -285,6 +285,24 @@ def get_system(node):
|
||||
raise exception.RedfishError(error=e)
|
||||
|
||||
|
||||
def get_task_monitor(node, uri):
|
||||
"""Get a TaskMonitor for a node.
|
||||
|
||||
:param node: an Ironic node object
|
||||
:param uri: the URI of a TaskMonitor
|
||||
:raises: RedfishConnectionError when it fails to connect to Redfish
|
||||
:raises: RedfishError when the TaskMonitor is not available in Redfish
|
||||
"""
|
||||
|
||||
try:
|
||||
return _get_connection(node, lambda conn: conn.get_task_monitor(uri))
|
||||
except sushy.exceptions.ResourceNotFoundError as e:
|
||||
LOG.error('The Redfish TaskMonitor "%(uri)s" was not found for '
|
||||
'node %(node)s. Error %(error)s',
|
||||
{'uri': uri, 'node': node.uuid, 'error': e})
|
||||
raise exception.RedfishError(error=e)
|
||||
|
||||
|
||||
def _get_connection(node, lambda_fun, *args):
|
||||
"""Get a Redfish connection to a node.
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
# under the License.
|
||||
|
||||
from ironic.drivers import generic
|
||||
from ironic.drivers.modules import agent
|
||||
from ironic.drivers.modules import inspector
|
||||
from ironic.drivers.modules import ipxe
|
||||
from ironic.drivers.modules import noop
|
||||
@ -24,6 +25,7 @@ from ironic.drivers.modules.redfish import boot as redfish_boot
|
||||
from ironic.drivers.modules.redfish import inspect as redfish_inspect
|
||||
from ironic.drivers.modules.redfish import management as redfish_mgmt
|
||||
from ironic.drivers.modules.redfish import power as redfish_power
|
||||
from ironic.drivers.modules.redfish import raid as redfish_raid
|
||||
from ironic.drivers.modules.redfish import vendor as redfish_vendor
|
||||
|
||||
|
||||
@ -63,3 +65,8 @@ class RedfishHardware(generic.GenericHardware):
|
||||
def supported_vendor_interfaces(self):
|
||||
"""List of supported vendor interfaces."""
|
||||
return [redfish_vendor.RedfishVendorPassthru, noop.NoVendor]
|
||||
|
||||
@property
|
||||
def supported_raid_interfaces(self):
|
||||
"""List of supported raid interfaces."""
|
||||
return [redfish_raid.RedfishRAID, noop.NoRAID, agent.AgentRAID]
|
||||
|
846
ironic/tests/unit/drivers/modules/redfish/test_raid.py
Normal file
846
ironic/tests/unit/drivers/modules/redfish/test_raid.py
Normal file
@ -0,0 +1,846 @@
|
||||
# Copyright 2021 DMTF. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from oslo_utils import importutils
|
||||
from oslo_utils import units
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common import states
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.conductor import utils as manager_utils
|
||||
from ironic.drivers.modules import deploy_utils
|
||||
from ironic.drivers.modules.redfish import boot as redfish_boot
|
||||
from ironic.drivers.modules.redfish import raid as redfish_raid
|
||||
from ironic.drivers.modules.redfish import utils as redfish_utils
|
||||
from ironic.tests.unit.db import base as db_base
|
||||
from ironic.tests.unit.db import utils as db_utils
|
||||
from ironic.tests.unit.objects import utils as obj_utils
|
||||
|
||||
sushy = importutils.try_import('sushy')
|
||||
|
||||
INFO_DICT = db_utils.get_test_redfish_info()
|
||||
|
||||
|
||||
def _mock_drive(identity, block_size_bytes=None, capacity_bytes=None,
|
||||
media_type=None, name=None, protocol=None):
|
||||
return mock.MagicMock(
|
||||
_path='/redfish/v1/Systems/1/Storage/1/Drives/' + identity,
|
||||
identity=identity,
|
||||
block_size_bytes=block_size_bytes,
|
||||
capacity_bytes=capacity_bytes,
|
||||
media_type=media_type,
|
||||
name=name,
|
||||
protocol=protocol
|
||||
)
|
||||
|
||||
|
||||
def _mock_volume(identity, volume_type=None, raid_type=None):
|
||||
volume = mock.MagicMock(
|
||||
_path='/redfish/v1/Systems/1/Storage/1/Volumes/' + identity,
|
||||
identity=identity,
|
||||
volume_type=volume_type,
|
||||
raid_type=raid_type
|
||||
)
|
||||
# Mocking Immediate that does not return anything
|
||||
volume.delete.return_value = None
|
||||
return volume
|
||||
|
||||
|
||||
@mock.patch('oslo_utils.eventletutils.EventletEvent.wait',
|
||||
lambda *args, **kwargs: None)
|
||||
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
|
||||
class RedfishRAIDTestCase(db_base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(RedfishRAIDTestCase, self).setUp()
|
||||
self.config(enabled_hardware_types=['redfish'],
|
||||
enabled_power_interfaces=['redfish'],
|
||||
enabled_boot_interfaces=['redfish-virtual-media'],
|
||||
enabled_management_interfaces=['redfish'],
|
||||
enabled_inspect_interfaces=['redfish'],
|
||||
enabled_bios_interfaces=['redfish'],
|
||||
enabled_raid_interfaces=['redfish']
|
||||
)
|
||||
self.node = obj_utils.create_test_node(
|
||||
self.context, driver='redfish', driver_info=INFO_DICT)
|
||||
self.mock_storage = mock.MagicMock()
|
||||
self.drive_id1 = '35D38F11ACEF7BD3'
|
||||
self.drive_id2 = '3F5A8C54207B7233'
|
||||
self.drive_id3 = '32ADF365C6C1B7BD'
|
||||
self.drive_id4 = '3D58ECBC375FD9F2'
|
||||
mock_drives = []
|
||||
for i in [self.drive_id1, self.drive_id2, self.drive_id3,
|
||||
self.drive_id4]:
|
||||
mock_drives.append(_mock_drive(
|
||||
identity=i, block_size_bytes=512, capacity_bytes=899527000000,
|
||||
media_type='HDD', name='Drive', protocol='SAS'))
|
||||
self.mock_storage.drives = mock_drives
|
||||
mock_identifier = mock.Mock()
|
||||
mock_identifier.durable_name = '345C59DBD970859C'
|
||||
mock_controller = mock.Mock()
|
||||
mock_controller.identifiers = [mock_identifier]
|
||||
self.mock_storage.storage_controllers = [mock_controller]
|
||||
mock_volumes = mock.MagicMock()
|
||||
self.mock_storage.volumes = mock_volumes
|
||||
self.free_space_bytes = {d: d.capacity_bytes for d in
|
||||
mock_drives}
|
||||
self.physical_disks = mock_drives
|
||||
|
||||
@mock.patch.object(redfish_raid, 'sushy', None)
|
||||
def test_loading_error(self, mock_get_system):
|
||||
self.assertRaisesRegex(
|
||||
exception.DriverLoadError,
|
||||
'Unable to import the sushy library',
|
||||
redfish_raid.RedfishRAID)
|
||||
|
||||
def test__max_volume_size_bytes_raid0(self, mock_get_system):
|
||||
spans = redfish_raid._calculate_spans('0', 3)
|
||||
max_size = redfish_raid._max_volume_size_bytes(
|
||||
'0', self.physical_disks[0:3], self.free_space_bytes,
|
||||
spans_count=spans)
|
||||
self.assertEqual(2698380312576, max_size)
|
||||
|
||||
def test__max_volume_size_bytes_raid1(self, mock_get_system):
|
||||
spans = redfish_raid._calculate_spans('1', 2)
|
||||
max_size = redfish_raid._max_volume_size_bytes(
|
||||
'1', self.physical_disks[0:2], self.free_space_bytes,
|
||||
spans_count=spans)
|
||||
self.assertEqual(899460104192, max_size)
|
||||
|
||||
def test__max_volume_size_bytes_raid5(self, mock_get_system):
|
||||
spans = redfish_raid._calculate_spans('5', 3)
|
||||
max_size = redfish_raid._max_volume_size_bytes(
|
||||
'5', self.physical_disks[0:3], self.free_space_bytes,
|
||||
spans_count=spans)
|
||||
self.assertEqual(1798920208384, max_size)
|
||||
|
||||
def test__max_volume_size_bytes_raid6(self, mock_get_system):
|
||||
spans = redfish_raid._calculate_spans('6', 4)
|
||||
max_size = redfish_raid._max_volume_size_bytes(
|
||||
'6', self.physical_disks[0:4], self.free_space_bytes,
|
||||
spans_count=spans)
|
||||
self.assertEqual(1798920208384, max_size)
|
||||
|
||||
def test__volume_usage_per_disk_bytes_raid5(self, mock_get_system):
|
||||
logical_disk = {
|
||||
'size_gb': 100,
|
||||
'raid_level': '5',
|
||||
'controller': 'Smart Array P822 in Slot 3',
|
||||
'physical_disks': [
|
||||
'35D38F11ACEF7BD3',
|
||||
'3F5A8C54207B7233',
|
||||
'32ADF365C6C1B7BD'
|
||||
],
|
||||
'is_root_volume': True
|
||||
}
|
||||
logical_disk['size_bytes'] = logical_disk['size_gb'] * units.Gi
|
||||
del logical_disk['size_gb']
|
||||
spans = redfish_raid._calculate_spans('5', 3)
|
||||
usage_bytes = redfish_raid._volume_usage_per_disk_bytes(
|
||||
logical_disk, self.physical_disks[0:3], spans_count=spans)
|
||||
self.assertEqual(53687091200, usage_bytes)
|
||||
|
||||
def test__volume_usage_per_disk_bytes_raid10(self, mock_get_system):
|
||||
logical_disk = {
|
||||
'size_gb': 50,
|
||||
'raid_level': '1+0',
|
||||
'controller': 'RAID.Integrated.1-1',
|
||||
'volume_name': 'root_volume',
|
||||
'is_root_volume': True,
|
||||
'physical_disks': [
|
||||
'35D38F11ACEF7BD3',
|
||||
'3F5A8C54207B7233',
|
||||
'32ADF365C6C1B7BD',
|
||||
'3D58ECBC375FD9F2'
|
||||
]
|
||||
}
|
||||
logical_disk['size_bytes'] = logical_disk['size_gb'] * units.Gi
|
||||
del logical_disk['size_gb']
|
||||
spans = redfish_raid._calculate_spans('1+0', 4)
|
||||
usage_bytes = redfish_raid._volume_usage_per_disk_bytes(
|
||||
logical_disk, self.physical_disks[0:4], spans_count=spans)
|
||||
self.assertEqual(26843545600, usage_bytes)
|
||||
|
||||
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, 'prepare_ramdisk',
|
||||
spec_set=True, autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
|
||||
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'get_async_step_return_state',
|
||||
autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'set_async_step_flags', autospec=True)
|
||||
def test_create_config_case_1a(
|
||||
self,
|
||||
mock_set_async_step_flags,
|
||||
mock_get_async_step_return_state,
|
||||
mock_node_power_action,
|
||||
mock_build_agent_options,
|
||||
mock_prepare_ramdisk,
|
||||
mock_get_system):
|
||||
target_raid_config = {
|
||||
'logical_disks': [
|
||||
{
|
||||
'size_gb': 'MAX',
|
||||
'raid_level': '5',
|
||||
'is_root_volume': True
|
||||
}
|
||||
]
|
||||
}
|
||||
self.node.target_raid_config = target_raid_config
|
||||
self.node.save()
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
self.assertRaisesRegex(
|
||||
exception.InvalidParameterValue,
|
||||
"'physical_disks' is missing from logical_disk while "
|
||||
"'size_gb'='MAX' was requested",
|
||||
task.driver.raid.create_configuration, task)
|
||||
|
||||
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, 'prepare_ramdisk',
|
||||
spec_set=True, autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
|
||||
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'get_async_step_return_state',
|
||||
autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'set_async_step_flags', autospec=True)
|
||||
def test_create_config_case_1b(
|
||||
self,
|
||||
mock_set_async_step_flags,
|
||||
mock_get_async_step_return_state,
|
||||
mock_node_power_action,
|
||||
mock_build_agent_options,
|
||||
mock_prepare_ramdisk,
|
||||
mock_get_system):
|
||||
target_raid_config = {
|
||||
'logical_disks': [
|
||||
{
|
||||
'size_gb': 100,
|
||||
'raid_level': '5',
|
||||
'is_root_volume': True
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_get_system.return_value.storage.get_members.return_value = [
|
||||
self.mock_storage]
|
||||
self.node.target_raid_config = target_raid_config
|
||||
self.node.save()
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
task.driver.raid.create_configuration(task)
|
||||
pre = '/redfish/v1/Systems/1/Storage/1/Drives/'
|
||||
expected_payload = {
|
||||
'Encrypted': False,
|
||||
'VolumeType': 'StripedWithParity',
|
||||
'RAIDType': 'RAID5',
|
||||
'CapacityBytes': 107374182400,
|
||||
'Links': {
|
||||
'Drives': [
|
||||
{'@odata.id': pre + self.drive_id1},
|
||||
{'@odata.id': pre + self.drive_id2},
|
||||
{'@odata.id': pre + self.drive_id3}
|
||||
]
|
||||
}
|
||||
}
|
||||
self.mock_storage.volumes.create.assert_called_once_with(
|
||||
expected_payload, apply_time=None
|
||||
)
|
||||
|
||||
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, 'prepare_ramdisk',
|
||||
spec_set=True, autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
|
||||
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'get_async_step_return_state',
|
||||
autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'set_async_step_flags', autospec=True)
|
||||
def test_create_config_case_1b_apply_time_immediate(
|
||||
self,
|
||||
mock_set_async_step_flags,
|
||||
mock_get_async_step_return_state,
|
||||
mock_node_power_action,
|
||||
mock_build_agent_options,
|
||||
mock_prepare_ramdisk,
|
||||
mock_get_system):
|
||||
target_raid_config = {
|
||||
'logical_disks': [
|
||||
{
|
||||
'size_gb': 100,
|
||||
'raid_level': '5',
|
||||
'is_root_volume': True
|
||||
}
|
||||
]
|
||||
}
|
||||
volumes = mock.MagicMock()
|
||||
op_apply_time_support = mock.MagicMock()
|
||||
op_apply_time_support.mapped_supported_values = [
|
||||
sushy.APPLY_TIME_IMMEDIATE, sushy.APPLY_TIME_ON_RESET]
|
||||
volumes.operation_apply_time_support = op_apply_time_support
|
||||
self.mock_storage.volumes = volumes
|
||||
mock_get_system.return_value.storage.get_members.return_value = [
|
||||
self.mock_storage]
|
||||
resource = mock.MagicMock(spec=['resource_name'])
|
||||
resource.resource_name = 'volume'
|
||||
volumes.create.return_value = resource
|
||||
self.node.target_raid_config = target_raid_config
|
||||
self.node.save()
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
task.driver.raid.create_configuration(task)
|
||||
pre = '/redfish/v1/Systems/1/Storage/1/Drives/'
|
||||
expected_payload = {
|
||||
'Encrypted': False,
|
||||
'VolumeType': 'StripedWithParity',
|
||||
'RAIDType': 'RAID5',
|
||||
'CapacityBytes': 107374182400,
|
||||
'Links': {
|
||||
'Drives': [
|
||||
{'@odata.id': pre + self.drive_id1},
|
||||
{'@odata.id': pre + self.drive_id2},
|
||||
{'@odata.id': pre + self.drive_id3}
|
||||
]
|
||||
}
|
||||
}
|
||||
self.mock_storage.volumes.create.assert_called_once_with(
|
||||
expected_payload, apply_time=sushy.APPLY_TIME_IMMEDIATE)
|
||||
mock_set_async_step_flags.assert_called_once_with(
|
||||
task.node, reboot=False, skip_current_step=True, polling=True)
|
||||
self.assertEqual(mock_get_async_step_return_state.call_count, 0)
|
||||
self.assertEqual(mock_node_power_action.call_count, 0)
|
||||
self.assertEqual(mock_build_agent_options.call_count, 0)
|
||||
self.assertEqual(mock_prepare_ramdisk.call_count, 0)
|
||||
|
||||
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, 'prepare_ramdisk',
|
||||
spec_set=True, autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
|
||||
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'get_async_step_return_state',
|
||||
autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'set_async_step_flags', autospec=True)
|
||||
def test_create_config_case_1b_apply_time_on_reset(
|
||||
self,
|
||||
mock_set_async_step_flags,
|
||||
mock_get_async_step_return_state,
|
||||
mock_node_power_action,
|
||||
mock_build_agent_options,
|
||||
mock_prepare_ramdisk,
|
||||
mock_get_system):
|
||||
target_raid_config = {
|
||||
'logical_disks': [
|
||||
{
|
||||
'size_gb': 100,
|
||||
'raid_level': '5',
|
||||
'is_root_volume': True
|
||||
}
|
||||
]
|
||||
}
|
||||
volumes = mock.MagicMock()
|
||||
op_apply_time_support = mock.MagicMock()
|
||||
op_apply_time_support.mapped_supported_values = [
|
||||
sushy.APPLY_TIME_ON_RESET]
|
||||
volumes.operation_apply_time_support = op_apply_time_support
|
||||
self.mock_storage.volumes = volumes
|
||||
mock_get_system.return_value.storage.get_members.return_value = [
|
||||
self.mock_storage]
|
||||
task_mon = mock.MagicMock()
|
||||
task_mon.task_monitor_uri = '/TaskService/123'
|
||||
volumes.create.return_value = task_mon
|
||||
self.node.target_raid_config = target_raid_config
|
||||
self.node.save()
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
task.driver.raid.create_configuration(task)
|
||||
pre = '/redfish/v1/Systems/1/Storage/1/Drives/'
|
||||
expected_payload = {
|
||||
'Encrypted': False,
|
||||
'VolumeType': 'StripedWithParity',
|
||||
'RAIDType': 'RAID5',
|
||||
'CapacityBytes': 107374182400,
|
||||
'Links': {
|
||||
'Drives': [
|
||||
{'@odata.id': pre + self.drive_id1},
|
||||
{'@odata.id': pre + self.drive_id2},
|
||||
{'@odata.id': pre + self.drive_id3}
|
||||
]
|
||||
}
|
||||
}
|
||||
self.mock_storage.volumes.create.assert_called_once_with(
|
||||
expected_payload, apply_time=sushy.APPLY_TIME_ON_RESET)
|
||||
mock_set_async_step_flags.assert_called_once_with(
|
||||
task.node, reboot=True, skip_current_step=True, polling=True)
|
||||
mock_get_async_step_return_state.assert_called_once_with(
|
||||
task.node)
|
||||
mock_node_power_action.assert_called_once_with(task, states.REBOOT)
|
||||
mock_build_agent_options.assert_called_once_with(task.node)
|
||||
self.assertEqual(mock_prepare_ramdisk.call_count, 1)
|
||||
|
||||
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, 'prepare_ramdisk',
|
||||
spec_set=True, autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
|
||||
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'get_async_step_return_state',
|
||||
autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'set_async_step_flags', autospec=True)
|
||||
def test_create_config_case_2(
|
||||
self,
|
||||
mock_set_async_step_flags,
|
||||
mock_get_async_step_return_state,
|
||||
mock_node_power_action,
|
||||
mock_build_agent_options,
|
||||
mock_prepare_ramdisk,
|
||||
mock_get_system):
|
||||
# TODO(bdodd): update mock_storage to allow this to pass w/o Exception
|
||||
target_raid_config = {
|
||||
'logical_disks': [
|
||||
{
|
||||
'size_gb': 100,
|
||||
'raid_level': '5',
|
||||
'is_root_volume': True,
|
||||
'disk_type': 'ssd'
|
||||
},
|
||||
{
|
||||
'size_gb': 500,
|
||||
'raid_level': '1',
|
||||
'disk_type': 'hdd'
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_get_system.return_value.storage.get_members.return_value = [
|
||||
self.mock_storage]
|
||||
self.node.target_raid_config = target_raid_config
|
||||
self.node.save()
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
self.assertRaisesRegex(
|
||||
exception.RedfishError,
|
||||
'failed to find matching physical disks for all logical disks',
|
||||
task.driver.raid.create_configuration, task)
|
||||
|
||||
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, 'prepare_ramdisk',
|
||||
spec_set=True, autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
|
||||
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'get_async_step_return_state',
|
||||
autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'set_async_step_flags', autospec=True)
|
||||
def test_create_config_case_3(
|
||||
self,
|
||||
mock_set_async_step_flags,
|
||||
mock_get_async_step_return_state,
|
||||
mock_node_power_action,
|
||||
mock_build_agent_options,
|
||||
mock_prepare_ramdisk,
|
||||
mock_get_system):
|
||||
target_raid_config = {
|
||||
'logical_disks': [
|
||||
{
|
||||
'size_gb': 100,
|
||||
'raid_level': '5',
|
||||
'controller': 'Smart Array P822 in Slot 3',
|
||||
# 'physical_disks': ['6I:1:5', '6I:1:6', '6I:1:7'],
|
||||
'physical_disks': [
|
||||
'35D38F11ACEF7BD3',
|
||||
'3F5A8C54207B7233',
|
||||
'32ADF365C6C1B7BD'
|
||||
],
|
||||
'is_root_volume': True
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_get_system.return_value.storage.get_members.return_value = [
|
||||
self.mock_storage]
|
||||
self.node.target_raid_config = target_raid_config
|
||||
self.node.save()
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
task.driver.raid.create_configuration(task)
|
||||
pre = '/redfish/v1/Systems/1/Storage/1/Drives/'
|
||||
expected_payload = {
|
||||
'Encrypted': False,
|
||||
'VolumeType': 'StripedWithParity',
|
||||
'RAIDType': 'RAID5',
|
||||
'CapacityBytes': 107374182400,
|
||||
'Links': {
|
||||
'Drives': [
|
||||
{'@odata.id': pre + self.drive_id1},
|
||||
{'@odata.id': pre + self.drive_id2},
|
||||
{'@odata.id': pre + self.drive_id3}
|
||||
]
|
||||
}
|
||||
}
|
||||
self.mock_storage.volumes.create.assert_called_once_with(
|
||||
expected_payload, apply_time=None
|
||||
)
|
||||
|
||||
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, 'prepare_ramdisk',
|
||||
spec_set=True, autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
|
||||
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'get_async_step_return_state',
|
||||
autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'set_async_step_flags', autospec=True)
|
||||
def test_create_config_case_4(
|
||||
self,
|
||||
mock_set_async_step_flags,
|
||||
mock_get_async_step_return_state,
|
||||
mock_node_power_action,
|
||||
mock_build_agent_options,
|
||||
mock_prepare_ramdisk,
|
||||
mock_get_system):
|
||||
# TODO(bdodd): update self.mock_storage to add more drives to satisfy
|
||||
# both logical disks
|
||||
target_raid_config = {
|
||||
'logical_disks': [
|
||||
{
|
||||
'size_gb': 50,
|
||||
'raid_level': '1+0',
|
||||
'controller': 'RAID.Integrated.1-1',
|
||||
'volume_name': 'root_volume',
|
||||
'is_root_volume': True,
|
||||
# 'physical_disks': [
|
||||
# 'Disk.Bay.0:Encl.Int.0-1:RAID.Integrated.1-1',
|
||||
# 'Disk.Bay.1:Encl.Int.0-1:RAID.Integrated.1-1'
|
||||
# ]
|
||||
'physical_disks': [
|
||||
'35D38F11ACEF7BD3',
|
||||
'3F5A8C54207B7233',
|
||||
'32ADF365C6C1B7BD',
|
||||
'3D58ECBC375FD9F2'
|
||||
]
|
||||
},
|
||||
{
|
||||
'size_gb': 100,
|
||||
'raid_level': '5',
|
||||
'controller': 'RAID.Integrated.1-1',
|
||||
'volume_name': 'data_volume',
|
||||
# 'physical_disks': [
|
||||
# 'Disk.Bay.2:Encl.Int.0-1:RAID.Integrated.1-1',
|
||||
# 'Disk.Bay.3:Encl.Int.0-1:RAID.Integrated.1-1',
|
||||
# 'Disk.Bay.4:Encl.Int.0-1:RAID.Integrated.1-1'
|
||||
# ]
|
||||
'physical_disks': [
|
||||
'3F5A8C54207B7233',
|
||||
'32ADF365C6C1B7BD',
|
||||
'3D58ECBC375FD9F2'
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_get_system.return_value.storage.get_members.return_value = [
|
||||
self.mock_storage]
|
||||
self.node.target_raid_config = target_raid_config
|
||||
self.node.save()
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
task.driver.raid.create_configuration(task)
|
||||
pre = '/redfish/v1/Systems/1/Storage/1/Drives/'
|
||||
expected_payload1 = {
|
||||
'Encrypted': False,
|
||||
'VolumeType': 'SpannedMirrors',
|
||||
'RAIDType': 'RAID10',
|
||||
'CapacityBytes': 53687091200,
|
||||
'Links': {
|
||||
'Drives': [
|
||||
{'@odata.id': pre + self.drive_id1},
|
||||
{'@odata.id': pre + self.drive_id2},
|
||||
{'@odata.id': pre + self.drive_id3},
|
||||
{'@odata.id': pre + self.drive_id4}
|
||||
]
|
||||
}
|
||||
}
|
||||
expected_payload2 = {
|
||||
'Encrypted': False,
|
||||
'VolumeType': 'StripedWithParity',
|
||||
'RAIDType': 'RAID5',
|
||||
'CapacityBytes': 107374182400,
|
||||
'Links': {
|
||||
'Drives': [
|
||||
{'@odata.id': pre + self.drive_id2},
|
||||
{'@odata.id': pre + self.drive_id3},
|
||||
{'@odata.id': pre + self.drive_id4}
|
||||
]
|
||||
}
|
||||
}
|
||||
self.assertEqual(
|
||||
self.mock_storage.volumes.create.call_count, 2)
|
||||
self.mock_storage.volumes.create.assert_any_call(
|
||||
expected_payload1, apply_time=None
|
||||
)
|
||||
self.mock_storage.volumes.create.assert_any_call(
|
||||
expected_payload2, apply_time=None
|
||||
)
|
||||
|
||||
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, 'prepare_ramdisk',
|
||||
spec_set=True, autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
|
||||
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'get_async_step_return_state',
|
||||
autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'set_async_step_flags', autospec=True)
|
||||
def test_create_config_case_5a(
|
||||
self,
|
||||
mock_set_async_step_flags,
|
||||
mock_get_async_step_return_state,
|
||||
mock_node_power_action,
|
||||
mock_build_agent_options,
|
||||
mock_prepare_ramdisk,
|
||||
mock_get_system):
|
||||
target_raid_config = {
|
||||
'logical_disks': [
|
||||
{
|
||||
'size_gb': 100,
|
||||
'raid_level': '1',
|
||||
'controller': 'software'
|
||||
},
|
||||
{
|
||||
'size_gb': 'MAX',
|
||||
'raid_level': '0',
|
||||
'controller': 'software'
|
||||
}
|
||||
]
|
||||
}
|
||||
self.node.target_raid_config = target_raid_config
|
||||
self.node.save()
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
self.assertRaisesRegex(
|
||||
exception.InvalidParameterValue,
|
||||
"'physical_disks' is missing from logical_disk while "
|
||||
"'size_gb'='MAX' was requested",
|
||||
task.driver.raid.create_configuration, task)
|
||||
|
||||
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, 'prepare_ramdisk',
|
||||
spec_set=True, autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
|
||||
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'get_async_step_return_state',
|
||||
autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'set_async_step_flags', autospec=True)
|
||||
def test_create_config_case_5b(
|
||||
self,
|
||||
mock_set_async_step_flags,
|
||||
mock_get_async_step_return_state,
|
||||
mock_node_power_action,
|
||||
mock_build_agent_options,
|
||||
mock_prepare_ramdisk,
|
||||
mock_get_system):
|
||||
target_raid_config = {
|
||||
'logical_disks': [
|
||||
{
|
||||
'size_gb': 100,
|
||||
'raid_level': '1',
|
||||
'controller': 'software'
|
||||
},
|
||||
{
|
||||
'size_gb': 500,
|
||||
'raid_level': '0',
|
||||
'controller': 'software'
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_get_system.return_value.storage.get_members.return_value = [
|
||||
self.mock_storage]
|
||||
self.node.target_raid_config = target_raid_config
|
||||
self.node.save()
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
task.driver.raid.create_configuration(task)
|
||||
pre = '/redfish/v1/Systems/1/Storage/1/Drives/'
|
||||
expected_payload1 = {
|
||||
'Encrypted': False,
|
||||
'VolumeType': 'Mirrored',
|
||||
'RAIDType': 'RAID1',
|
||||
'CapacityBytes': 107374182400,
|
||||
'Links': {
|
||||
'Drives': [
|
||||
{'@odata.id': pre + self.drive_id1},
|
||||
{'@odata.id': pre + self.drive_id2}
|
||||
]
|
||||
}
|
||||
}
|
||||
expected_payload2 = {
|
||||
'Encrypted': False,
|
||||
'VolumeType': 'NonRedundant',
|
||||
'RAIDType': 'RAID0',
|
||||
'CapacityBytes': 536870912000,
|
||||
'Links': {
|
||||
'Drives': [
|
||||
{'@odata.id': pre + self.drive_id3}
|
||||
]
|
||||
}
|
||||
}
|
||||
self.assertEqual(
|
||||
self.mock_storage.volumes.create.call_count, 2)
|
||||
self.mock_storage.volumes.create.assert_any_call(
|
||||
expected_payload1, apply_time=None
|
||||
)
|
||||
self.mock_storage.volumes.create.assert_any_call(
|
||||
expected_payload2, apply_time=None
|
||||
)
|
||||
|
||||
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, 'prepare_ramdisk',
|
||||
spec_set=True, autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
|
||||
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'get_async_step_return_state',
|
||||
autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'set_async_step_flags', autospec=True)
|
||||
def test_create_config_case_6(
|
||||
self,
|
||||
mock_set_async_step_flags,
|
||||
mock_get_async_step_return_state,
|
||||
mock_node_power_action,
|
||||
mock_build_agent_options,
|
||||
mock_prepare_ramdisk,
|
||||
mock_get_system):
|
||||
target_raid_config = {
|
||||
'logical_disks': [
|
||||
{
|
||||
'size_gb': 'MAX',
|
||||
'raid_level': '0',
|
||||
'controller': 'software',
|
||||
'physical_disks': [
|
||||
{'size': '> 100'},
|
||||
{'size': '> 100'}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_get_system.return_value.storage.get_members.return_value = [
|
||||
self.mock_storage]
|
||||
self.node.target_raid_config = target_raid_config
|
||||
self.node.save()
|
||||
# TODO(bdodd): update when impl can handle disk size evaluation
|
||||
# (see _calculate_volume_props())
|
||||
"""
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
task.driver.raid.create_configuration(task)
|
||||
"""
|
||||
|
||||
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, 'prepare_ramdisk',
|
||||
spec_set=True, autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
|
||||
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'get_async_step_return_state',
|
||||
autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'set_async_step_flags', autospec=True)
|
||||
def test_delete_config_immediate(
|
||||
self,
|
||||
mock_set_async_step_flags,
|
||||
mock_get_async_step_return_state,
|
||||
mock_node_power_action,
|
||||
mock_build_agent_options,
|
||||
mock_prepare_ramdisk,
|
||||
mock_get_system):
|
||||
mock_volumes = []
|
||||
for i in ["1", "2"]:
|
||||
mock_volumes.append(_mock_volume(
|
||||
i, volume_type='Mirrored', raid_type='RAID1'))
|
||||
op_apply_time_support = mock.MagicMock()
|
||||
op_apply_time_support.mapped_supported_values = [
|
||||
sushy.APPLY_TIME_IMMEDIATE, sushy.APPLY_TIME_ON_RESET]
|
||||
self.mock_storage.volumes.operation_apply_time_support = (
|
||||
op_apply_time_support)
|
||||
self.mock_storage.volumes.get_members.return_value = mock_volumes
|
||||
mock_get_system.return_value.storage.get_members.return_value = [
|
||||
self.mock_storage]
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
task.driver.raid.delete_configuration(task)
|
||||
self.assertEqual(mock_volumes[0].delete.call_count, 1)
|
||||
self.assertEqual(mock_volumes[1].delete.call_count, 1)
|
||||
mock_set_async_step_flags.assert_called_once_with(
|
||||
task.node, reboot=False, skip_current_step=True, polling=True)
|
||||
self.assertEqual(mock_get_async_step_return_state.call_count, 0)
|
||||
self.assertEqual(mock_node_power_action.call_count, 0)
|
||||
self.assertEqual(mock_build_agent_options.call_count, 0)
|
||||
self.assertEqual(mock_prepare_ramdisk.call_count, 0)
|
||||
|
||||
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, 'prepare_ramdisk',
|
||||
spec_set=True, autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
|
||||
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'get_async_step_return_state',
|
||||
autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'set_async_step_flags', autospec=True)
|
||||
def test_delete_config_on_reset(
|
||||
self,
|
||||
mock_set_async_step_flags,
|
||||
mock_get_async_step_return_state,
|
||||
mock_node_power_action,
|
||||
mock_build_agent_options,
|
||||
mock_prepare_ramdisk,
|
||||
mock_get_system):
|
||||
mock_volumes = []
|
||||
for i in ["1", "2"]:
|
||||
mock_volumes.append(_mock_volume(
|
||||
i, volume_type='Mirrored', raid_type='RAID1'))
|
||||
op_apply_time_support = mock.MagicMock()
|
||||
op_apply_time_support.mapped_supported_values = [
|
||||
sushy.APPLY_TIME_ON_RESET]
|
||||
self.mock_storage.volumes.operation_apply_time_support = (
|
||||
op_apply_time_support)
|
||||
self.mock_storage.volumes.get_members.return_value = mock_volumes
|
||||
mock_get_system.return_value.storage.get_members.return_value = [
|
||||
self.mock_storage]
|
||||
task_mon = mock.MagicMock()
|
||||
task_mon.task_monitor_uri = '/TaskService/123'
|
||||
mock_volumes[0].delete.return_value = task_mon
|
||||
mock_volumes[1].delete.return_value = task_mon
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
task.driver.raid.delete_configuration(task)
|
||||
self.assertEqual(mock_volumes[0].delete.call_count, 1)
|
||||
self.assertEqual(mock_volumes[1].delete.call_count, 1)
|
||||
mock_set_async_step_flags.assert_called_once_with(
|
||||
task.node, reboot=True, skip_current_step=True, polling=True)
|
||||
mock_get_async_step_return_state.assert_called_once_with(
|
||||
task.node)
|
||||
mock_node_power_action.assert_called_once_with(task, states.REBOOT)
|
||||
mock_build_agent_options.assert_called_once_with(task.node)
|
||||
self.assertEqual(mock_prepare_ramdisk.call_count, 1)
|
||||
|
||||
def test_volume_create_error_handler(self, mock_get_system):
|
||||
volume_collection = self.mock_storage.volumes
|
||||
sushy_error = sushy.exceptions.SushyError()
|
||||
volume_collection.create.side_effect = sushy_error
|
||||
mock_get_system.return_value.storage.get_members.return_value = [
|
||||
self.mock_storage]
|
||||
mock_error_handler = mock.Mock()
|
||||
drive_id = '35D38F11ACEF7BD3'
|
||||
physical_disks = [drive_id]
|
||||
capacity_bytes = 53739520000
|
||||
pre = '/redfish/v1/Systems/1/Storage/1/Drives/'
|
||||
expected_payload = {
|
||||
'Encrypted': False,
|
||||
'VolumeType': 'Mirrored',
|
||||
'RAIDType': 'RAID1',
|
||||
'CapacityBytes': capacity_bytes,
|
||||
'Links': {
|
||||
'Drives': [
|
||||
{
|
||||
'@odata.id': pre + drive_id
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
redfish_raid.create_virtual_disk(
|
||||
task, None, physical_disks, '1', capacity_bytes,
|
||||
error_handler=mock_error_handler)
|
||||
self.assertEqual(mock_error_handler.call_count, 1)
|
||||
mock_error_handler.assert_called_once_with(
|
||||
task, sushy_error, volume_collection, expected_payload
|
||||
)
|
@ -226,6 +226,25 @@ class RedfishUtilsTestCase(db_base.DbTestCase):
|
||||
self.assertEqual(fake_conn.get_system.call_count,
|
||||
redfish_utils.CONF.redfish.connection_attempts)
|
||||
|
||||
def test_get_task_monitor(self):
|
||||
redfish_utils._get_connection = mock.Mock()
|
||||
fake_monitor = mock.Mock()
|
||||
redfish_utils._get_connection.return_value = fake_monitor
|
||||
uri = '/redfish/v1/TaskMonitor/FAKEMONITOR'
|
||||
|
||||
response = redfish_utils.get_task_monitor(self.node, uri)
|
||||
|
||||
self.assertEqual(fake_monitor, response)
|
||||
|
||||
def test_get_task_monitor_error(self):
|
||||
redfish_utils._get_connection = mock.Mock()
|
||||
uri = '/redfish/v1/TaskMonitor/FAKEMONITOR'
|
||||
redfish_utils._get_connection.side_effect =\
|
||||
sushy.exceptions.ResourceNotFoundError('GET', uri, mock.Mock())
|
||||
|
||||
self.assertRaises(exception.RedfishError,
|
||||
redfish_utils.get_task_monitor, self.node, uri)
|
||||
|
||||
@mock.patch.object(sushy, 'Sushy', autospec=True)
|
||||
@mock.patch('ironic.drivers.modules.redfish.utils.'
|
||||
'SessionCache._sessions', {})
|
||||
|
@ -156,12 +156,14 @@ SUSHY_SPEC = (
|
||||
'VIRTUAL_MEDIA_CD',
|
||||
'VIRTUAL_MEDIA_FLOPPY',
|
||||
'VIRTUAL_MEDIA_USBSTICK',
|
||||
'APPLY_TIME_IMMEDIATE',
|
||||
'APPLY_TIME_ON_RESET',
|
||||
'TASK_STATE_COMPLETED',
|
||||
'HEALTH_OK',
|
||||
'HEALTH_WARNING',
|
||||
'SECURE_BOOT_RESET_KEYS_TO_DEFAULT',
|
||||
'SECURE_BOOT_RESET_KEYS_DELETE_ALL',
|
||||
'VOLUME_TYPE_RAW_DEVICE'
|
||||
)
|
||||
|
||||
SUSHY_AUTH_SPEC = (
|
||||
|
@ -218,12 +218,14 @@ if not sushy:
|
||||
VIRTUAL_MEDIA_CD='cd',
|
||||
VIRTUAL_MEDIA_FLOPPY='floppy',
|
||||
VIRTUAL_MEDIA_USBSTICK='usb',
|
||||
APPLY_TIME_IMMEDIATE='immediate',
|
||||
APPLY_TIME_ON_RESET='on reset',
|
||||
TASK_STATE_COMPLETED='completed',
|
||||
HEALTH_OK='ok',
|
||||
HEALTH_WARNING='warning',
|
||||
SECURE_BOOT_RESET_KEYS_TO_DEFAULT="ResetAllKeysToDefault",
|
||||
SECURE_BOOT_RESET_KEYS_DELETE_ALL="DeleteAllKeys",
|
||||
VOLUME_TYPE_RAW_DEVICE='rawdevice'
|
||||
)
|
||||
|
||||
sys.modules['sushy'] = sushy
|
||||
|
@ -0,0 +1,8 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Adds a ``redfish`` native ``raid_interface`` to the ``redfish`` hardware type.
|
||||
- |
|
||||
Note that common RAID cases have been tested, but cases that are more complex or
|
||||
rely on vendor-specific implementation details may not work as desired due to
|
||||
capability limitations.
|
@ -145,6 +145,7 @@ ironic.hardware.interfaces.raid =
|
||||
ilo5 = ironic.drivers.modules.ilo.raid:Ilo5RAID
|
||||
irmc = ironic.drivers.modules.irmc.raid:IRMCRAID
|
||||
no-raid = ironic.drivers.modules.noop:NoRAID
|
||||
redfish = ironic.drivers.modules.redfish.raid:RedfishRAID
|
||||
|
||||
ironic.hardware.interfaces.rescue =
|
||||
agent = ironic.drivers.modules.agent:AgentRescue
|
||||
|
Loading…
Reference in New Issue
Block a user