From 98958cd0a49d987290b84a76ce5d6eb95e067148 Mon Sep 17 00:00:00 2001
From: Bob Fournier <bfournie@redhat.com>
Date: Tue, 17 Nov 2020 08:18:10 -0500
Subject: [PATCH] Add vendor_passthru method for virtual media

Add a vendor_passthru method to eject_vmedia for Redfish and idrac.

Story: 2008363
Task: 41271

Change-Id: Ib5ae16bacfd79f479a9aa8fbf69edc5cfdf73ce3
---
 ironic/drivers/drac.py                        |   3 +-
 .../drivers/modules/drac/vendor_passthru.py   |   8 ++
 ironic/drivers/modules/redfish/boot.py        |  16 +--
 ironic/drivers/modules/redfish/vendor.py      |  92 ++++++++++++++
 ironic/drivers/redfish.py                     |   6 +
 .../unit/drivers/modules/redfish/test_boot.py |  36 +++---
 .../drivers/modules/redfish/test_vendor.py    | 116 ++++++++++++++++++
 ...assthru-eject-vmedia-e4456320ee1c70c1.yaml |   7 ++
 setup.cfg                                     |   2 +
 9 files changed, 259 insertions(+), 27 deletions(-)
 create mode 100644 ironic/drivers/modules/redfish/vendor.py
 create mode 100644 ironic/tests/unit/drivers/modules/redfish/test_vendor.py
 create mode 100644 releasenotes/notes/vendor-passthru-eject-vmedia-e4456320ee1c70c1.yaml

diff --git a/ironic/drivers/drac.py b/ironic/drivers/drac.py
index 9c58ea079e..87d7e7217e 100644
--- a/ironic/drivers/drac.py
+++ b/ironic/drivers/drac.py
@@ -81,4 +81,5 @@ class IDRACHardware(generic.GenericHardware):
     def supported_vendor_interfaces(self):
         """List of supported vendor interfaces."""
         return [vendor_passthru.DracWSManVendorPassthru,
-                vendor_passthru.DracVendorPassthru, noop.NoVendor]
+                vendor_passthru.DracVendorPassthru,
+                vendor_passthru.DracRedfishVendorPassthru, noop.NoVendor]
diff --git a/ironic/drivers/modules/drac/vendor_passthru.py b/ironic/drivers/modules/drac/vendor_passthru.py
index ff43cc95ce..fb25397a93 100644
--- a/ironic/drivers/modules/drac/vendor_passthru.py
+++ b/ironic/drivers/modules/drac/vendor_passthru.py
@@ -24,6 +24,7 @@ from ironic.drivers import base
 from ironic.drivers.modules.drac import bios as drac_bios
 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 vendor as redfish_vendor
 
 LOG = logging.getLogger(__name__)
 
@@ -190,3 +191,10 @@ class DracVendorPassthru(DracWSManVendorPassthru):
         LOG.warning("Vendor passthru interface 'idrac' is deprecated and may "
                     "be removed in a future release. Use 'idrac-wsman' "
                     "instead.")
+
+
+class DracRedfishVendorPassthru(redfish_vendor.RedfishVendorPassthru):
+    """iDRAC Redfish interface for vendor_passthru.
+
+    Use the Redfish implementation for vendor passthru.
+    """
diff --git a/ironic/drivers/modules/redfish/boot.py b/ironic/drivers/modules/redfish/boot.py
index 15c8352d1b..67812acc3e 100644
--- a/ironic/drivers/modules/redfish/boot.py
+++ b/ironic/drivers/modules/redfish/boot.py
@@ -183,7 +183,7 @@ def _insert_vmedia(task, boot_url, boot_device):
         _('No suitable virtual media device found'))
 
 
-def _eject_vmedia(task, boot_device=None):
+def eject_vmedia(task, boot_device=None):
     """Eject virtual CDs and DVDs
 
     :param task: A task from TaskManager.
@@ -430,7 +430,7 @@ class RedfishVirtualMediaBoot(base.BootInterface):
                 floppy_ref = image_utils.prepare_floppy_image(
                     task, params=ramdisk_params)
 
-                _eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY)
+                eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY)
                 _insert_vmedia(
                     task, floppy_ref, sushy.VIRTUAL_MEDIA_FLOPPY)
 
@@ -447,7 +447,7 @@ class RedfishVirtualMediaBoot(base.BootInterface):
         iso_ref = image_utils.prepare_deploy_iso(task, ramdisk_params,
                                                  mode, d_info)
 
-        _eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD)
+        eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD)
         _insert_vmedia(task, iso_ref, sushy.VIRTUAL_MEDIA_CD)
 
         boot_mode_utils.sync_boot_mode(task)
@@ -474,12 +474,12 @@ class RedfishVirtualMediaBoot(base.BootInterface):
         LOG.debug("Cleaning up deploy boot for "
                   "%(node)s", {'node': task.node.uuid})
 
-        _eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD)
+        eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD)
         image_utils.cleanup_iso_image(task)
 
         if (config_via_floppy
                 and _has_vmedia_device(task, sushy.VIRTUAL_MEDIA_FLOPPY)):
-            _eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY)
+            eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY)
 
             image_utils.cleanup_floppy_image(task)
 
@@ -533,7 +533,7 @@ class RedfishVirtualMediaBoot(base.BootInterface):
 
         deploy_info = _parse_deploy_info(node)
         iso_ref = image_utils.prepare_boot_iso(task, deploy_info, **params)
-        _eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD)
+        eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD)
         _insert_vmedia(task, iso_ref, sushy.VIRTUAL_MEDIA_CD)
 
         boot_mode_utils.sync_boot_mode(task)
@@ -556,11 +556,11 @@ class RedfishVirtualMediaBoot(base.BootInterface):
         LOG.debug("Cleaning up instance boot for "
                   "%(node)s", {'node': task.node.uuid})
 
-        _eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD)
+        eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD)
         d_info = task.node.driver_info
         config_via_floppy = d_info.get('config_via_floppy')
         if config_via_floppy:
-            _eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY)
+            eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY)
 
         image_utils.cleanup_iso_image(task)
 
diff --git a/ironic/drivers/modules/redfish/vendor.py b/ironic/drivers/modules/redfish/vendor.py
new file mode 100644
index 0000000000..76927c2c73
--- /dev/null
+++ b/ironic/drivers/modules/redfish/vendor.py
@@ -0,0 +1,92 @@
+# Copyright 2015 Hewlett-Packard Development Company, L.P.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+"""
+Vendor Interface for Redfish drivers and its supporting methods.
+"""
+
+from ironic_lib import metrics_utils
+
+from ironic.common import exception
+from ironic.common.i18n import _
+from ironic.drivers import base
+from ironic.drivers.modules.redfish import boot as redfish_boot
+from ironic.drivers.modules.redfish import utils as redfish_utils
+
+METRICS = metrics_utils.get_metrics_logger(__name__)
+
+
+class RedfishVendorPassthru(base.VendorInterface):
+    """Vendor-specific interfaces for Redfish drivers."""
+
+    def get_properties(self):
+        return {}
+
+    @METRICS.timer('RedfishVendorPassthru.validate')
+    def validate(self, task, method, **kwargs):
+        """Validate vendor-specific actions.
+
+        Checks if a valid vendor passthru method was passed and validates
+        the parameters for the vendor passthru method.
+
+        :param task: a TaskManager instance containing the node to act on.
+        :param method: method to be validated.
+        :param kwargs: kwargs containing the vendor passthru method's
+            parameters.
+        :raises: InvalidParameterValue, if any of the parameters have invalid
+            value.
+        """
+        if method == 'eject_vmedia':
+            self._validate_eject_vmedia(task, kwargs)
+            return
+        super(RedfishVendorPassthru, self).validate(task, method, **kwargs)
+
+    def _validate_eject_vmedia(self, task, kwargs):
+        """Verify that the boot_device input is valid."""
+
+        # If a boot device is provided check that it's valid.
+        # It is OK to eject if already ejected
+        boot_device = kwargs.get('boot_device')
+
+        if not boot_device:
+            return
+
+        system = redfish_utils.get_system(task.node)
+
+        for manager in system.managers:
+            for v_media in manager.virtual_media.get_members():
+                if boot_device not in v_media.media_types:
+                    raise exception.InvalidParameterValue(_(
+                        "Boot device %s is not a valid value ") % boot_device)
+
+    @METRICS.timer('RedfishVendorPassthru.eject_vmedia')
+    @base.passthru(['POST'],
+                   description=_("Eject a virtual media device. If no device "
+                                 "is provided than all attached devices will "
+                                 "be ejected. "
+                                 "Optional arguments: "
+                                 "'boot_device' - the boot device to eject, "
+                                 "either 'cd', 'dvd', 'usb', or 'floppy'"))
+    # @task_manager.require_exclusive_lock
+    def eject_vmedia(self, task, **kwargs):
+        """Eject a virtual media device.
+
+        :param task: A TaskManager object.
+        :param kwargs: The arguments sent with vendor passthru. The optional
+            kwargs are::
+            'boot_device': the boot device to eject
+        """
+
+        # If boot_device not provided all vmedia devices will be ejected
+        boot_device = kwargs.get('boot_device')
+        redfish_boot.eject_vmedia(task, boot_device)
diff --git a/ironic/drivers/redfish.py b/ironic/drivers/redfish.py
index feb579af0d..9a00b04970 100644
--- a/ironic/drivers/redfish.py
+++ b/ironic/drivers/redfish.py
@@ -24,6 +24,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 vendor as redfish_vendor
 
 
 class RedfishHardware(generic.GenericHardware):
@@ -57,3 +58,8 @@ class RedfishHardware(generic.GenericHardware):
         # vendors support.
         return [ipxe.iPXEBoot, pxe.PXEBoot,
                 redfish_boot.RedfishVirtualMediaBoot]
+
+    @property
+    def supported_vendor_interfaces(self):
+        """List of supported vendor interfaces."""
+        return [redfish_vendor.RedfishVendorPassthru, noop.NoVendor]
diff --git a/ironic/tests/unit/drivers/modules/redfish/test_boot.py b/ironic/tests/unit/drivers/modules/redfish/test_boot.py
index 540b99cebd..bc350b1b16 100644
--- a/ironic/tests/unit/drivers/modules/redfish/test_boot.py
+++ b/ironic/tests/unit/drivers/modules/redfish/test_boot.py
@@ -326,7 +326,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
     @mock.patch.object(redfish_boot.manager_utils, 'node_set_boot_device',
                        autospec=True)
     @mock.patch.object(image_utils, 'prepare_deploy_iso', autospec=True)
-    @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot, 'eject_vmedia', autospec=True)
     @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True)
     @mock.patch.object(redfish_boot, '_parse_driver_info', autospec=True)
     @mock.patch.object(redfish_boot.manager_utils, 'node_power_action',
@@ -371,7 +371,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
     @mock.patch.object(redfish_boot.manager_utils, 'node_set_boot_device',
                        autospec=True)
     @mock.patch.object(image_utils, 'prepare_deploy_iso', autospec=True)
-    @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot, 'eject_vmedia', autospec=True)
     @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True)
     @mock.patch.object(redfish_boot, '_parse_driver_info', autospec=True)
     @mock.patch.object(redfish_boot.manager_utils, 'node_power_action',
@@ -417,7 +417,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
     @mock.patch.object(image_utils, 'prepare_floppy_image', autospec=True)
     @mock.patch.object(image_utils, 'prepare_deploy_iso', autospec=True)
     @mock.patch.object(redfish_boot, '_has_vmedia_device', autospec=True)
-    @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot, 'eject_vmedia', autospec=True)
     @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True)
     @mock.patch.object(redfish_boot, '_parse_driver_info', autospec=True)
     @mock.patch.object(redfish_boot.manager_utils, 'node_power_action',
@@ -482,7 +482,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
             mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task)
 
     @mock.patch.object(redfish_boot, '_has_vmedia_device', autospec=True)
-    @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot, 'eject_vmedia', autospec=True)
     @mock.patch.object(image_utils, 'cleanup_iso_image', autospec=True)
     @mock.patch.object(image_utils, 'cleanup_floppy_image', autospec=True)
     @mock.patch.object(redfish_boot, '_parse_driver_info', autospec=True)
@@ -517,7 +517,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
     @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
                        'clean_up_instance', autospec=True)
     @mock.patch.object(image_utils, 'prepare_boot_iso', autospec=True)
-    @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot, 'eject_vmedia', autospec=True)
     @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True)
     @mock.patch.object(redfish_boot, '_parse_deploy_info', autospec=True)
     @mock.patch.object(redfish_boot, 'manager_utils', autospec=True)
@@ -569,7 +569,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
     @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
                        'clean_up_instance', autospec=True)
     @mock.patch.object(image_utils, 'prepare_boot_iso', autospec=True)
-    @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot, 'eject_vmedia', autospec=True)
     @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True)
     @mock.patch.object(redfish_boot, '_parse_deploy_info', autospec=True)
     @mock.patch.object(redfish_boot, 'manager_utils', autospec=True)
@@ -617,7 +617,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
     @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
                        'clean_up_instance', autospec=True)
     @mock.patch.object(image_utils, 'prepare_boot_iso', autospec=True)
-    @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot, 'eject_vmedia', autospec=True)
     @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True)
     @mock.patch.object(redfish_boot, '_parse_deploy_info', autospec=True)
     @mock.patch.object(redfish_boot, 'manager_utils', autospec=True)
@@ -663,7 +663,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
     @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
                        'clean_up_instance', autospec=True)
     @mock.patch.object(image_utils, 'prepare_boot_iso', autospec=True)
-    @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot, 'eject_vmedia', autospec=True)
     @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True)
     @mock.patch.object(redfish_boot, '_parse_deploy_info', autospec=True)
     @mock.patch.object(redfish_boot, 'manager_utils', autospec=True)
@@ -700,7 +700,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
 
             mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task)
 
-    @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot, 'eject_vmedia', autospec=True)
     @mock.patch.object(image_utils, 'cleanup_iso_image', autospec=True)
     @mock.patch.object(redfish_boot, 'manager_utils', autospec=True)
     def _test_prepare_instance_local_boot(
@@ -733,7 +733,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
         self.node.save()
         self._test_prepare_instance_local_boot()
 
-    @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot, 'eject_vmedia', autospec=True)
     @mock.patch.object(image_utils, 'cleanup_iso_image', autospec=True)
     def _test_clean_up_instance(self, mock_cleanup_iso_image,
                                 mock__eject_vmedia):
@@ -832,7 +832,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
                 task, 'img-url', sushy.VIRTUAL_MEDIA_CD)
 
     @mock.patch.object(redfish_boot, 'redfish_utils', autospec=True)
-    def test__eject_vmedia_everything(self, mock_redfish_utils):
+    def test_eject_vmedia_everything(self, mock_redfish_utils):
 
         with task_manager.acquire(self.context, self.node.uuid,
                                   shared=True) as task:
@@ -851,13 +851,13 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
             mock_redfish_utils.get_system.return_value.managers = [
                 mock_manager]
 
-            redfish_boot._eject_vmedia(task)
+            redfish_boot.eject_vmedia(task)
 
             mock_vmedia_cd.eject_media.assert_called_once_with()
             mock_vmedia_floppy.eject_media.assert_called_once_with()
 
     @mock.patch.object(redfish_boot, 'redfish_utils', autospec=True)
-    def test__eject_vmedia_specific(self, mock_redfish_utils):
+    def test_eject_vmedia_specific(self, mock_redfish_utils):
 
         with task_manager.acquire(self.context, self.node.uuid,
                                   shared=True) as task:
@@ -876,13 +876,13 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
             mock_redfish_utils.get_system.return_value.managers = [
                 mock_manager]
 
-            redfish_boot._eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD)
+            redfish_boot.eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD)
 
             mock_vmedia_cd.eject_media.assert_called_once_with()
             self.assertFalse(mock_vmedia_floppy.eject_media.call_count)
 
     @mock.patch.object(redfish_boot, 'redfish_utils', autospec=True)
-    def test__eject_vmedia_not_inserted(self, mock_redfish_utils):
+    def test_eject_vmedia_not_inserted(self, mock_redfish_utils):
 
         with task_manager.acquire(self.context, self.node.uuid,
                                   shared=True) as task:
@@ -901,13 +901,13 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
             mock_redfish_utils.get_system.return_value.managers = [
                 mock_manager]
 
-            redfish_boot._eject_vmedia(task)
+            redfish_boot.eject_vmedia(task)
 
             self.assertFalse(mock_vmedia_cd.eject_media.call_count)
             self.assertFalse(mock_vmedia_floppy.eject_media.call_count)
 
     @mock.patch.object(redfish_boot, 'redfish_utils', autospec=True)
-    def test__eject_vmedia_unknown(self, mock_redfish_utils):
+    def test_eject_vmedia_unknown(self, mock_redfish_utils):
 
         with task_manager.acquire(self.context, self.node.uuid,
                                   shared=True) as task:
@@ -923,6 +923,6 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
             mock_redfish_utils.get_system.return_value.managers = [
                 mock_manager]
 
-            redfish_boot._eject_vmedia(task)
+            redfish_boot.eject_vmedia(task)
 
             self.assertFalse(mock_vmedia_cd.eject_media.call_count)
diff --git a/ironic/tests/unit/drivers/modules/redfish/test_vendor.py b/ironic/tests/unit/drivers/modules/redfish/test_vendor.py
new file mode 100644
index 0000000000..156b3defbc
--- /dev/null
+++ b/ironic/tests/unit/drivers/modules/redfish/test_vendor.py
@@ -0,0 +1,116 @@
+# Copyright 2018 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 ironic.common import exception
+from ironic.conductor import task_manager
+from ironic.drivers.modules.redfish import boot as redfish_boot
+from ironic.drivers.modules.redfish import vendor as redfish_vendor
+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()
+
+
+class RedfishVendorPassthruTestCase(db_base.DbTestCase):
+
+    def setUp(self):
+        super(RedfishVendorPassthruTestCase, self).setUp()
+        self.config(enabled_bios_interfaces=['redfish'],
+                    enabled_hardware_types=['redfish'],
+                    enabled_power_interfaces=['redfish'],
+                    enabled_boot_interfaces=['redfish-virtual-media'],
+                    enabled_management_interfaces=['redfish'],
+                    enabled_vendor_interfaces=['redfish'])
+        self.node = obj_utils.create_test_node(
+            self.context, driver='redfish', driver_info=INFO_DICT)
+
+    @mock.patch.object(redfish_boot, 'redfish_utils', autospec=True)
+    def test_eject_vmedia_all(self, mock_redfish_utils):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+
+            mock_vmedia_cd = mock.MagicMock(
+                inserted=True,
+                media_types=[sushy.VIRTUAL_MEDIA_CD])
+            mock_vmedia_floppy = mock.MagicMock(
+                inserted=True,
+                media_types=[sushy.VIRTUAL_MEDIA_FLOPPY])
+
+            mock_manager = mock.MagicMock()
+
+            mock_manager.virtual_media.get_members.return_value = [
+                mock_vmedia_cd, mock_vmedia_floppy]
+
+            mock_redfish_utils.get_system.return_value.managers = [
+                mock_manager]
+
+            task.driver.vendor.eject_vmedia(task)
+
+            mock_vmedia_cd.eject_media.assert_called_once_with()
+            mock_vmedia_floppy.eject_media.assert_called_once_with()
+
+    @mock.patch.object(redfish_boot, 'redfish_utils', autospec=True)
+    def test_eject_vmedia_cd(self, mock_redfish_utils):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+
+            mock_vmedia_cd = mock.MagicMock(
+                inserted=True,
+                media_types=[sushy.VIRTUAL_MEDIA_CD])
+            mock_vmedia_floppy = mock.MagicMock(
+                inserted=True,
+                media_types=[sushy.VIRTUAL_MEDIA_FLOPPY])
+
+            mock_manager = mock.MagicMock()
+
+            mock_manager.virtual_media.get_members.return_value = [
+                mock_vmedia_cd, mock_vmedia_floppy]
+
+            mock_redfish_utils.get_system.return_value.managers = [
+                mock_manager]
+
+            task.driver.vendor.eject_vmedia(task,
+                                            boot_device=sushy.VIRTUAL_MEDIA_CD)
+
+            mock_vmedia_cd.eject_media.assert_called_once_with()
+            mock_vmedia_floppy.eject_media.assert_not_called()
+
+    @mock.patch.object(redfish_vendor, 'redfish_utils', autospec=True)
+    def test_eject_vmedia_invalid_dev(self, mock_redfish_utils):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+
+            mock_vmedia_cd = mock.MagicMock(
+                inserted=True,
+                media_types=[sushy.VIRTUAL_MEDIA_CD])
+
+            mock_manager = mock.MagicMock()
+
+            mock_manager.virtual_media.get_members.return_value = [
+                mock_vmedia_cd]
+
+            mock_redfish_utils.get_system.return_value.managers = [
+                mock_manager]
+
+            kwargs = {'boot_device': 'foo'}
+            self.assertRaises(
+                exception.InvalidParameterValue,
+                task.driver.vendor.validate, task, 'eject_vmedia', **kwargs)
diff --git a/releasenotes/notes/vendor-passthru-eject-vmedia-e4456320ee1c70c1.yaml b/releasenotes/notes/vendor-passthru-eject-vmedia-e4456320ee1c70c1.yaml
new file mode 100644
index 0000000000..95e0696187
--- /dev/null
+++ b/releasenotes/notes/vendor-passthru-eject-vmedia-e4456320ee1c70c1.yaml
@@ -0,0 +1,7 @@
+---
+features:
+  - |
+    Provides a new vendor passthru method for Redfish to eject a virtual_media
+    device.  A specific device can be given (either ``cd``, ``dvd``,
+    ``floppy``, or ``usb``), or if no device is provided then all attached
+    devices will be ejected.
diff --git a/setup.cfg b/setup.cfg
index 75da36990f..cd2888928e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -162,9 +162,11 @@ ironic.hardware.interfaces.vendor =
     ibmc = ironic.drivers.modules.ibmc.vendor:IBMCVendor
     idrac = ironic.drivers.modules.drac.vendor_passthru:DracVendorPassthru
     idrac-wsman = ironic.drivers.modules.drac.vendor_passthru:DracWSManVendorPassthru
+    idrac-redfish = ironic.drivers.modules.drac.vendor_passthru:DracRedfishVendorPassthru
     ilo = ironic.drivers.modules.ilo.vendor:VendorPassthru
     ipmitool = ironic.drivers.modules.ipmitool:VendorPassthru
     no-vendor = ironic.drivers.modules.noop:NoVendor
+    redfish = ironic.drivers.modules.redfish.vendor:RedfishVendorPassthru
 
 ironic.hardware.types =
     fake-hardware = ironic.drivers.fake_hardware:FakeHardware