Merge "Add Redfish RAID management to Ironic"
This commit is contained in:
commit
e3dff622bb
@ -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…
x
Reference in New Issue
Block a user