From 952695be334c2d68b3abc8315822e512a1caf900 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aija=20Jaunt=C4=93va?= <aija.jaunteva@dell.com>
Date: Fri, 5 Feb 2021 04:59:01 -0500
Subject: [PATCH] Add Redfish RAID interface to idrac HW type

Adds MVP support for idrac-redfish to RAID interface. Based on
generic redfish implementation, but requires OEM extension
to check when `Immediate` time becomes available shortly
after IPA starts executing steps.

Does not support foreign disks, convert from non-RAID mode.

Story: 2008602
Task: 41778
Depends-On: https://review.opendev.org/c/x/sushy-oem-idrac/+/776224
Change-Id: Iefb7f882c97e33a176962e4e907163d9e4809445
---
 doc/source/admin/drivers/idrac.rst            |  38 ++--
 driver-requirements.txt                       |   2 +-
 ironic/drivers/drac.py                        |   5 +-
 ironic/drivers/modules/drac/raid.py           | 164 ++++++++++++++++++
 ironic/drivers/modules/redfish/raid.py        |  26 +++
 ironic/tests/unit/db/utils.py                 |   4 +
 .../unit/drivers/modules/drac/test_raid.py    | 162 +++++++++++++++++
 .../unit/drivers/modules/redfish/test_raid.py |  16 ++
 ironic/tests/unit/drivers/test_drac.py        |  12 +-
 ...redfish-raid-support-414aad5e633a160f.yaml |  18 ++
 setup.cfg                                     |   1 +
 11 files changed, 434 insertions(+), 14 deletions(-)
 create mode 100644 releasenotes/notes/idrac-add-redfish-raid-support-414aad5e633a160f.yaml

diff --git a/doc/source/admin/drivers/idrac.rst b/doc/source/admin/drivers/idrac.rst
index d94a577ef8..b5d217554f 100644
--- a/doc/source/admin/drivers/idrac.rst
+++ b/doc/source/admin/drivers/idrac.rst
@@ -55,17 +55,13 @@ Enabling
 
 The iDRAC driver supports WSMAN for the bios, inspect, management, power,
 raid, and vendor interfaces. In addition, it supports Redfish for
-the bios, inspect, management, and power interfaces. The iDRAC driver
+the bios, inspect, management, power, and raid interfaces. The iDRAC driver
 allows you to mix and match WSMAN and Redfish interfaces.
 
 The ``idrac-wsman`` implementation must be enabled to use WSMAN for
 an interface. The ``idrac-redfish`` implementation must be enabled
 to use Redfish for an interface.
 
-.. NOTE::
-   Redfish is supported for only the bios, inspect, management, and power
-   interfaces at the present time.
-
 To enable the ``idrac`` hardware type with the minimum interfaces,
 all using WSMAN, add the following to your ``/etc/ironic/ironic.conf``:
 
@@ -88,7 +84,7 @@ following configuration:
     enabled_inspect_interfaces=idrac-redfish
     enabled_management_interfaces=idrac-redfish
     enabled_power_interfaces=idrac-redfish
-    enabled_raid_interfaces=idrac-wsman
+    enabled_raid_interfaces=idrac-redfish
     enabled_vendor_interfaces=idrac-redfish
 
 Below is the list of supported interface implementations in priority
@@ -106,7 +102,7 @@ Interface            Supported Implementations
 ``management``       ``idrac-wsman``, ``idrac``, ``idrac-redfish``
 ``network``          ``flat``, ``neutron``, ``noop``
 ``power``            ``idrac-wsman``, ``idrac``, ``idrac-redfish``
-``raid``             ``idrac-wsman``, ``idrac``, ``no-raid``
+``raid``             ``idrac-wsman``, ``idrac``, ``idrac-redfish``, ``no-raid``
 ``rescue``           ``no-rescue``, ``agent``
 ``storage``          ``noop``, ``cinder``, ``external``
 ``vendor``           ``idrac-wsman``, ``idrac``, ``idrac-redfish``,
@@ -180,7 +176,7 @@ hardware type using Redfish for all interfaces:
         --inspect-interface idrac-redfish \
         --management-interface idrac-redfish \
         --power-interface idrac-redfish \
-        --raid-interface no-raid \
+        --raid-interface idrac-redfish \
         --vendor-interface idrac-redfish
 
 The following command enrolls a bare metal node with the ``idrac``
@@ -283,9 +279,12 @@ RAID Interface
 
 See :doc:`/admin/raid` for more information on Ironic RAID support.
 
-The following properties are supported by the iDRAC WSMAN raid interface
-implementation, ``idrac-wsman``:
+The following properties are supported by the iDRAC WSMAN and Redfish RAID
+interface implementation:
 
+.. NOTE::
+  When using ``idrac-redfish`` for RAID interface iDRAC firmware greater than
+  4.40.00.00 is required.
 
 Mandatory properties
 --------------------
@@ -310,6 +309,11 @@ Optional properties
 Backing physical disk hints
 ---------------------------
 
+.. NOTE::
+  Backing physical disk hints are not widely tested with ``idrac-redfish`` yet
+  and they might not work as desired. This will be addressed in future
+  releases.
+
 See :doc:`/admin/raid` for more information on backing disk hints.
 
 These are machine-independent information. The hints are specified for each
@@ -408,6 +412,20 @@ be used to fetch the information directly from the Dell bare metal:
   physical_disks = client.list_physical_disks()
   print(physical_disks)
 
+Or using ``sushy`` with Redfish:
+
+.. code-block:: python
+
+  import sushy
+
+
+  client = sushy.Sushy('https://192.168.1.1', username='root', password='calvin', verify=False)
+  for s in client.get_system_collection().get_members():
+    print("System: %(id)s" % {'id': s.identity})
+    for c in system1.storage.get_members():
+        print("\tController: %(id)s" % {'id': c.identity})
+        for d in c.drives:
+          print("\t\tDrive: %(id)s" % {'id': d.identity})
 
 Vendor Interface
 ================
diff --git a/driver-requirements.txt b/driver-requirements.txt
index 1bd25844a1..921c2a6490 100644
--- a/driver-requirements.txt
+++ b/driver-requirements.txt
@@ -20,4 +20,4 @@ ansible>=2.7
 python-ibmcclient>=0.2.2,<0.3.0
 
 # Dell EMC iDRAC sushy OEM extension
-sushy-oem-idrac<2.0.0
+sushy-oem-idrac>=2.0.0,<3.0.0
diff --git a/ironic/drivers/drac.py b/ironic/drivers/drac.py
index 87d7e7217e..266b21bee4 100644
--- a/ironic/drivers/drac.py
+++ b/ironic/drivers/drac.py
@@ -74,8 +74,9 @@ class IDRACHardware(generic.GenericHardware):
     @property
     def supported_raid_interfaces(self):
         """List of supported raid interfaces."""
-        return [raid.DracWSManRAID, raid.DracRAID] + super(
-            IDRACHardware, self).supported_raid_interfaces
+        return [raid.DracWSManRAID, raid.DracRAID,
+                raid.DracRedfishRAID] + super(
+                    IDRACHardware, self).supported_raid_interfaces
 
     @property
     def supported_vendor_interfaces(self):
diff --git a/ironic/drivers/modules/drac/raid.py b/ironic/drivers/modules/drac/raid.py
index f7319a140d..2d3e639a84 100644
--- a/ironic/drivers/modules/drac/raid.py
+++ b/ironic/drivers/modules/drac/raid.py
@@ -23,6 +23,7 @@ from ironic_lib import metrics_utils
 from oslo_log import log as logging
 from oslo_utils import importutils
 from oslo_utils import units
+import tenacity
 
 from ironic.common import exception
 from ironic.common.i18n import _
@@ -34,9 +35,12 @@ from ironic.drivers import base
 from ironic.drivers.modules import deploy_utils
 from ironic.drivers.modules.drac import common as drac_common
 from ironic.drivers.modules.drac import job as drac_job
+from ironic.drivers.modules.redfish import raid as redfish_raid
+from ironic.drivers.modules.redfish import utils as redfish_utils
 
 drac_exceptions = importutils.try_import('dracclient.exceptions')
 drac_constants = importutils.try_import('dracclient.constants')
+sushy = importutils.try_import('sushy')
 
 LOG = logging.getLogger(__name__)
 
@@ -1160,6 +1164,166 @@ def _get_disk_free_size_mb(disk, pending_delete):
     return disk.size_mb if pending_delete else disk.free_size_mb
 
 
+def _wait_till_realtime_ready(task):
+    """Waits till real time operations are ready to be executed.
+
+    Useful for RAID operations where almost all controllers support
+    real time configuration, but controllers might not be ready for
+    it by the time IPA starts executing steps. It can take minute or
+    bit more to be ready for real time configuration.
+
+    :param task: TaskManager object containing the node.
+    :raises RedfishError: If can't find OEM extension or it fails to
+        execute
+    """
+    try:
+        _retry_till_realtime_ready(task)
+    except tenacity.RetryError:
+        LOG.debug('Retries exceeded while waiting for real-time ready '
+                  'for node %(node)s. Will proceed with out real-time '
+                  'ready state', {'node': task.node.uuid})
+
+
+@tenacity.retry(
+    stop=(tenacity.stop_after_attempt(30)),
+    wait=tenacity.wait_fixed(10),
+    retry=tenacity.retry_if_result(lambda result: not result))
+def _retry_till_realtime_ready(task):
+    """Retries till real time operations are ready to be executed.
+
+    :param task: TaskManager object containing the node.
+    :raises RedfishError: If can't find OEM extension or it fails to
+        execute
+    :raises RetryError: If retries exceeded and still not ready for real-time
+    """
+    return _is_realtime_ready(task)
+
+
+def _is_realtime_ready(task):
+    """Gets is real time ready status
+
+    Uses sushy-oem-idrac extension.
+
+    :param task: TaskManager object containing the node.
+    :returns: True, if real time operations are ready, otherwise False.
+    :raises RedfishError: If can't find OEM extension or it fails to
+        execute
+    """
+    system = redfish_utils.get_system(task.node)
+    for manager in system.managers:
+        try:
+            manager_oem = manager.get_oem_extension('Dell')
+        except sushy.exceptions.OEMExtensionNotFoundError as e:
+            error_msg = (_("Search for Sushy OEM extension Python package "
+                           "'sushy-oem-idrac' failed for node %(node)s. "
+                           "Ensure it is installed. Error: %(error)s") %
+                         {'node': task.node.uuid, 'error': e})
+            LOG.error(error_msg)
+            raise exception.RedfishError(error=error_msg)
+
+        try:
+            return manager_oem.lifecycle_service.is_realtime_ready()
+        except sushy.exceptions.SushyError as e:
+            LOG.debug("Failed to get real time ready status with system "
+                      "%(system)s manager %(manager)s for node %(node)s. Will "
+                      "try next manager, if available. Error: %(error)s",
+                      {'system': system.uuid if system.uuid else
+                       system.identity,
+                       'manager': manager.uuid if manager.uuid else
+                       manager.identity,
+                       'node': task.node.uuid,
+                       'error': e})
+            continue
+        break
+
+    else:
+        error_msg = (_("iDRAC Redfish get real time ready status failed for "
+                       "node %(node)s, because system %(system)s has no "
+                       "manager%(no_manager)s.") %
+                     {'node': task.node.uuid,
+                      'system': system.uuid if system.uuid else
+                      system.identity,
+                      'no_manager': '' if not system.managers else
+                      ' which could'})
+        LOG.error(error_msg)
+        raise exception.RedfishError(error=error_msg)
+
+
+class DracRedfishRAID(redfish_raid.RedfishRAID):
+    """iDRAC Redfish interface for RAID related actions.
+
+    Includes iDRAC specific adjustments for RAID related actions.
+    """
+
+    @base.clean_step(priority=0, abortable=False, argsinfo={
+        'create_root_volume': {
+            'description': (
+                'This specifies whether to create the root volume. '
+                'Defaults to `True`.'
+            ),
+            'required': False
+        },
+        'create_nonroot_volumes': {
+            'description': (
+                'This specifies whether to create the non-root volumes. '
+                'Defaults to `True`.'
+            ),
+            'required': False
+        },
+        'delete_existing': {
+            'description': (
+                'Setting this to `True` indicates to delete existing RAID '
+                'configuration prior to creating the new configuration. '
+                'Default value is `False`.'
+            ),
+            'required': False,
+        }
+    })
+    def create_configuration(self, task, create_root_volume=True,
+                             create_nonroot_volumes=True,
+                             delete_existing=False):
+        """Create RAID configuration on the node.
+
+        This method creates the RAID configuration as read from
+        node.target_raid_config.  This method
+        by default will create all logical disks.
+
+        :param task: TaskManager object containing the node.
+        :param create_root_volume: Setting this to False indicates
+            not to create root volume that is specified in the node's
+            target_raid_config. Default value is True.
+        :param create_nonroot_volumes: Setting this to False indicates
+            not to create non-root volumes (all except the root volume) in
+            the node's target_raid_config.  Default value is True.
+        :param delete_existing: Setting this to True indicates to delete RAID
+            configuration prior to creating the new configuration. Default is
+            False.
+        :returns: states.CLEANWAIT if RAID configuration is in progress
+            asynchronously or None if it is complete.
+        :raises: RedfishError if there is an error creating the configuration
+        """
+        _wait_till_realtime_ready(task)
+        return super(DracRedfishRAID, self).create_configuration(
+            task, create_root_volume, create_nonroot_volumes,
+            delete_existing)
+
+    @base.clean_step(priority=0)
+    @base.deploy_step(priority=0)
+    def delete_configuration(self, task):
+        """Delete RAID configuration on the node.
+
+        :param task: TaskManager object containing the node.
+        :returns: states.CLEANWAIT (cleaning) or states.DEPLOYWAIT (deployment)
+            if deletion is in progress asynchronously or None if it is
+            complete.
+        """
+        _wait_till_realtime_ready(task)
+        return super(DracRedfishRAID, self).delete_configuration(task)
+
+    def _validate_vendor(self, task):
+        pass  # for now assume idrac-redfish is used with iDRAC BMC, thus pass
+
+
 class DracWSManRAID(base.RAIDInterface):
 
     def get_properties(self):
diff --git a/ironic/drivers/modules/redfish/raid.py b/ironic/drivers/modules/redfish/raid.py
index 7e14735e2b..f9b84bb860 100644
--- a/ironic/drivers/modules/redfish/raid.py
+++ b/ironic/drivers/modules/redfish/raid.py
@@ -687,6 +687,32 @@ class RedfishRAID(base.RAIDInterface):
         """
         return redfish_utils.COMMON_PROPERTIES.copy()
 
+    def _validate_vendor(self, task):
+        vendor = task.node.properties.get('vendor')
+        if not vendor:
+            return
+
+        if 'dell' in vendor.lower().split():
+            raise exception.InvalidParameterValue(
+                _("The %(iface)s raid interface is not suitable for node "
+                  "%(node)s with vendor %(vendor)s, use idrac-redfish instead")
+                % {'iface': task.node.raid_interface,
+                   'node': task.node.uuid, 'vendor': vendor})
+
+    def validate(self, task):
+        """Validates the RAID Interface.
+
+        This method validates the properties defined by Ironic for RAID
+        configuration. Driver implementations of this interface can override
+        this method for doing more validations (such as BMC's credentials).
+
+        :param task: A TaskManager instance.
+        :raises: InvalidParameterValue, if the RAID configuration is invalid.
+        :raises: MissingParameterValue, if some parameters are missing.
+        """
+        self._validate_vendor(task)
+        super(RedfishRAID, self).validate(task)
+
     @base.deploy_step(priority=0,
                       argsinfo=base.RAID_APPLY_CONFIGURATION_ARGSINFO)
     def apply_configuration(self, task, raid_config, create_root_volume=True,
diff --git a/ironic/tests/unit/db/utils.py b/ironic/tests/unit/db/utils.py
index 4368490549..bf025d6afb 100644
--- a/ironic/tests/unit/db/utils.py
+++ b/ironic/tests/unit/db/utils.py
@@ -92,6 +92,10 @@ def get_test_drac_info():
         "drac_protocol": "https",
         "drac_username": "admin",
         "drac_password": "fake",
+        "redfish_address": "1.2.3.4",
+        "redfish_system_id": "/redfish/v1/Systems/System.Embedded.1",
+        "redfish_username": "admin",
+        "redfish_password": "fake"
     }
 
 
diff --git a/ironic/tests/unit/drivers/modules/drac/test_raid.py b/ironic/tests/unit/drivers/modules/drac/test_raid.py
index 48f38a09b5..3dd48f1b0e 100644
--- a/ironic/tests/unit/drivers/modules/drac/test_raid.py
+++ b/ironic/tests/unit/drivers/modules/drac/test_raid.py
@@ -20,6 +20,8 @@ from unittest import mock
 
 from dracclient import constants
 from dracclient import exceptions as drac_exceptions
+from oslo_utils import importutils
+import tenacity
 
 from ironic.common import exception
 from ironic.common import states
@@ -28,9 +30,13 @@ from ironic.drivers import base
 from ironic.drivers.modules.drac import common as drac_common
 from ironic.drivers.modules.drac import job as drac_job
 from ironic.drivers.modules.drac import raid as drac_raid
+from ironic.drivers.modules.redfish import raid as redfish_raid
+from ironic.drivers.modules.redfish import utils as redfish_utils
 from ironic.tests.unit.drivers.modules.drac import utils as test_utils
 from ironic.tests.unit.objects import utils as obj_utils
 
+sushy = importutils.try_import('sushy')
+
 INFO_DICT = test_utils.INFO_DICT
 
 
@@ -2239,3 +2245,159 @@ class DracRaidInterfaceTestCase(test_utils.BaseDracTest):
             mock_apply_configuration.assert_called_once_with(
                 task.driver.raid, task,
                 self.target_raid_configuration, False, True, False)
+
+
+class DracRedfishRAIDTestCase(test_utils.BaseDracTest):
+
+    def setUp(self):
+        super(DracRedfishRAIDTestCase, self).setUp()
+        self.node = obj_utils.create_test_node(self.context,
+                                               driver='idrac',
+                                               driver_info=INFO_DICT)
+        self.raid = drac_raid.DracRedfishRAID()
+
+    @mock.patch.object(drac_raid, '_wait_till_realtime_ready', autospec=True)
+    @mock.patch.object(redfish_raid.RedfishRAID, 'create_configuration',
+                       autospec=True)
+    def test_create_configuration(self, mock_redfish_create, mock_wait):
+        task = mock.Mock(node=self.node, context=self.context)
+
+        self.raid.create_configuration(task)
+
+        mock_wait.assert_called_once_with(task)
+        mock_redfish_create.assert_called_once_with(
+            self.raid, task, True, True, False)
+
+    @mock.patch.object(drac_raid, '_wait_till_realtime_ready', autospec=True)
+    @mock.patch.object(redfish_raid.RedfishRAID, 'delete_configuration',
+                       autospec=True)
+    def test_delete_configuration(self, mock_redfish_delete, mock_wait):
+        task = mock.Mock(node=self.node, context=self.context)
+
+        self.raid.delete_configuration(task)
+
+        mock_wait.assert_called_once_with(task)
+        mock_redfish_delete.assert_called_once_with(self.raid, task)
+
+    @mock.patch.object(drac_raid, '_retry_till_realtime_ready', autospec=True)
+    def test__wait_till_realtime_ready(self, mock_ready):
+        task = mock.Mock(node=self.node, context=self.context)
+        drac_raid._wait_till_realtime_ready(task)
+        mock_ready.assert_called_once_with(task)
+
+    @mock.patch.object(drac_raid, 'LOG', autospec=True)
+    @mock.patch.object(drac_raid, '_retry_till_realtime_ready', autospec=True)
+    def test__wait_till_realtime_ready_retryerror(self, mock_ready, mock_log):
+        task = mock.Mock(node=self.node, context=self.context)
+        mock_ready.side_effect = tenacity.RetryError(3)
+        drac_raid._wait_till_realtime_ready(task)
+        mock_ready.assert_called_once_with(task)
+        self.assertEqual(mock_log.debug.call_count, 1)
+
+    @mock.patch.object(drac_raid, '_is_realtime_ready', autospec=True)
+    def test__retry_till_realtime_ready_retry_exceeded(self, mock_ready):
+        drac_raid._retry_till_realtime_ready.retry.sleep = mock.Mock()
+        drac_raid._retry_till_realtime_ready.retry.stop =\
+            tenacity.stop_after_attempt(3)
+        task = mock.Mock(node=self.node, context=self.context)
+        mock_ready.return_value = False
+
+        self.assertRaises(
+            tenacity.RetryError,
+            drac_raid._retry_till_realtime_ready, task)
+
+        self.assertEqual(3, mock_ready.call_count)
+
+    @mock.patch.object(drac_raid, '_is_realtime_ready', autospec=True)
+    def test__retry_till_realtime_ready_retry_fails(self, mock_ready):
+        drac_raid._retry_till_realtime_ready.retry.sleep = mock.Mock()
+        drac_raid._retry_till_realtime_ready.retry.stop =\
+            tenacity.stop_after_attempt(3)
+        task = mock.Mock(node=self.node, context=self.context)
+        mock_ready.side_effect = [False, exception.RedfishError]
+
+        self.assertRaises(
+            exception.RedfishError,
+            drac_raid._retry_till_realtime_ready, task)
+
+        self.assertEqual(2, mock_ready.call_count)
+
+    @mock.patch.object(drac_raid, '_is_realtime_ready', autospec=True)
+    def test__retry_till_realtime_ready(self, mock_ready):
+        drac_raid._retry_till_realtime_ready.retry.sleep = mock.Mock()
+        task = mock.Mock(node=self.node, context=self.context)
+        mock_ready.side_effect = [False, True]
+
+        is_ready = drac_raid._retry_till_realtime_ready(task)
+
+        self.assertTrue(is_ready)
+        self.assertEqual(2, mock_ready.call_count)
+
+    @mock.patch.object(redfish_utils, 'get_system', autospec=True)
+    def test__is_realtime_ready_no_managers(self, mock_get_system):
+        task = mock.Mock(node=self.node, context=self.context)
+        fake_system = mock.Mock(managers=[])
+        mock_get_system.return_value = fake_system
+        self.assertRaises(exception.RedfishError,
+                          drac_raid._is_realtime_ready, task)
+
+    @mock.patch.object(drac_raid, 'LOG', autospec=True)
+    @mock.patch.object(redfish_utils, 'get_system', autospec=True)
+    def test__is_realtime_ready_oem_not_found(self, mock_get_system, mock_log):
+        task = mock.Mock(node=self.node, context=self.context)
+        fake_manager1 = mock.Mock()
+        fake_manager1.get_oem_extension.side_effect = (
+            sushy.exceptions.OEMExtensionNotFoundError)
+        fake_system = mock.Mock(managers=[fake_manager1])
+        mock_get_system.return_value = fake_system
+        self.assertRaises(exception.RedfishError,
+                          drac_raid._is_realtime_ready, task)
+        self.assertEqual(mock_log.error.call_count, 1)
+
+    @mock.patch.object(drac_raid, 'LOG', autospec=True)
+    @mock.patch.object(redfish_utils, 'get_system', autospec=True)
+    def test__is_realtime_ready_all_managers_fail(self, mock_get_system,
+                                                  mock_log):
+        task = mock.Mock(node=self.node, context=self.context)
+        fake_manager_oem1 = mock.Mock()
+        fake_manager_oem1.lifecycle_service.is_realtime_ready.side_effect = (
+            sushy.exceptions.SushyError)
+        fake_manager1 = mock.Mock()
+        fake_manager1.get_oem_extension.return_value = fake_manager_oem1
+        fake_manager_oem2 = mock.Mock()
+        fake_manager_oem2.lifecycle_service.is_realtime_ready.side_effect = (
+            sushy.exceptions.SushyError)
+        fake_manager2 = mock.Mock()
+        fake_manager2.get_oem_extension.return_value = fake_manager_oem2
+        fake_system = mock.Mock(managers=[fake_manager1, fake_manager2])
+        mock_get_system.return_value = fake_system
+        self.assertRaises(exception.RedfishError,
+                          drac_raid._is_realtime_ready, task)
+        self.assertEqual(mock_log.debug.call_count, 2)
+
+    @mock.patch.object(drac_raid, 'LOG', autospec=True)
+    @mock.patch.object(redfish_utils, 'get_system', autospec=True)
+    def test__is_realtime_ready(self, mock_get_system, mock_log):
+        task = mock.Mock(node=self.node, context=self.context)
+        fake_manager_oem1 = mock.Mock()
+        fake_manager_oem1.lifecycle_service.is_realtime_ready.side_effect = (
+            sushy.exceptions.SushyError)
+        fake_manager1 = mock.Mock()
+        fake_manager1.get_oem_extension.return_value = fake_manager_oem1
+        fake_manager_oem2 = mock.Mock()
+        fake_manager_oem2.lifecycle_service.is_realtime_ready.return_value = (
+            True)
+        fake_manager2 = mock.Mock()
+        fake_manager2.get_oem_extension.return_value = fake_manager_oem2
+        fake_system = mock.Mock(managers=[fake_manager1, fake_manager2])
+        mock_get_system.return_value = fake_system
+
+        is_ready = drac_raid._is_realtime_ready(task)
+
+        self.assertTrue(is_ready)
+        self.assertEqual(mock_log.debug.call_count, 1)
+
+    def test_validate_correct_vendor(self):
+        task = mock.Mock(node=self.node, context=self.context)
+        self.node.properties['vendor'] = 'Dell Inc.'
+        self.raid.validate(task)
diff --git a/ironic/tests/unit/drivers/modules/redfish/test_raid.py b/ironic/tests/unit/drivers/modules/redfish/test_raid.py
index 3e46c388d9..28e57acdf5 100644
--- a/ironic/tests/unit/drivers/modules/redfish/test_raid.py
+++ b/ironic/tests/unit/drivers/modules/redfish/test_raid.py
@@ -844,3 +844,19 @@ class RedfishRAIDTestCase(db_base.DbTestCase):
             mock_error_handler.assert_called_once_with(
                 task, sushy_error, volume_collection, expected_payload
             )
+
+    def test_validate(self, mock_get_system):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            task.node.properties['vendor'] = "Supported vendor"
+
+            task.driver.raid.validate(task)
+
+    def test_validate_unsupported_vendor(self, mock_get_system):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            task.node.properties['vendor'] = "Dell Inc."
+
+            self.assertRaisesRegex(exception.InvalidParameterValue,
+                                   "with vendor Dell.Inc.",
+                                   task.driver.raid.validate, task)
diff --git a/ironic/tests/unit/drivers/test_drac.py b/ironic/tests/unit/drivers/test_drac.py
index 554c04eaec..6af1c2de67 100644
--- a/ironic/tests/unit/drivers/test_drac.py
+++ b/ironic/tests/unit/drivers/test_drac.py
@@ -43,7 +43,8 @@ class IDRACHardwareTestCase(db_base.DbTestCase):
                         'no-inspect'],
                     enabled_network_interfaces=['flat', 'neutron', 'noop'],
                     enabled_raid_interfaces=[
-                        'idrac', 'idrac-wsman', 'no-raid', 'agent'],
+                        'idrac', 'idrac-wsman', 'idrac-redfish', 'no-raid',
+                        'agent'],
                     enabled_vendor_interfaces=[
                         'idrac', 'idrac-wsman', 'no-vendor'],
                     enabled_bios_interfaces=[
@@ -113,6 +114,15 @@ class IDRACHardwareTestCase(db_base.DbTestCase):
             with task_manager.acquire(self.context, node.id) as task:
                 self._validate_interfaces(task.driver, raid=impl)
 
+    def test_override_with_redfish_raid(self):
+        node = obj_utils.create_test_node(self.context,
+                                          uuid=uuidutils.generate_uuid(),
+                                          driver='idrac',
+                                          raid_interface='idrac-redfish')
+        with task_manager.acquire(self.context, node.id) as task:
+            self._validate_interfaces(task.driver,
+                                      raid=drac.raid.DracRedfishRAID)
+
     def test_override_no_vendor(self):
         node = obj_utils.create_test_node(self.context, driver='idrac',
                                           vendor_interface='no-vendor')
diff --git a/releasenotes/notes/idrac-add-redfish-raid-support-414aad5e633a160f.yaml b/releasenotes/notes/idrac-add-redfish-raid-support-414aad5e633a160f.yaml
new file mode 100644
index 0000000000..141150f738
--- /dev/null
+++ b/releasenotes/notes/idrac-add-redfish-raid-support-414aad5e633a160f.yaml
@@ -0,0 +1,18 @@
+---
+features:
+  - |
+    Adds basic support for managing RAID configuration via the Redfish
+    out-of-band (OOB) management protocol to the ``idrac`` hardware type by
+    adding new interface named ``idrac-redfish``.
+
+    iDRAC firmware greater than 4.40.00.00 is required. Compared to
+    ``idrac-wsman`` implementation does not yet support foreign disks and
+    converting from non-RAID mode.
+
+    Backing physical disk hints are not widely tested with ``idrac-redfish``
+    yet and they might not work as desired. Backing physical disks
+    (``controller``, ``physical_disks``) with ``size_gb="MAX"`` are tested.
+
+    The ``idrac`` hardware type now supports ``idrac-wsman``, ``idrac``,
+    ``idrac-redfish``, and ``no-raid`` interfaces in given priority
+    order.
diff --git a/setup.cfg b/setup.cfg
index 827fc565ad..4e581d8331 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -141,6 +141,7 @@ ironic.hardware.interfaces.raid =
     fake = ironic.drivers.modules.fake:FakeRAID
     ibmc = ironic.drivers.modules.ibmc.raid:IbmcRAID
     idrac = ironic.drivers.modules.drac.raid:DracRAID
+    idrac-redfish = ironic.drivers.modules.drac.raid:DracRedfishRAID
     idrac-wsman = ironic.drivers.modules.drac.raid:DracWSManRAID
     ilo5 = ironic.drivers.modules.ilo.raid:Ilo5RAID
     irmc = ironic.drivers.modules.irmc.raid:IRMCRAID