diff --git a/devstack/files/debs/ironic b/devstack/files/debs/ironic
index 50fa67d7f7..e769a2d7f9 100644
--- a/devstack/files/debs/ironic
+++ b/devstack/files/debs/ironic
@@ -21,6 +21,8 @@ libguestfs-tools
 libvirt-bin # dist:xenial,bionic NOPRIME
 open-iscsi
 openssh-client
+# TODO (etingof) pinning to older version in devstack/lib/ironic
+#ovmf
 pxelinux # dist:xenial,bionic
 python-libguestfs
 qemu
diff --git a/devstack/lib/ironic b/devstack/lib/ironic
index c98e8fe4ee..b8b4dfc119 100644
--- a/devstack/lib/ironic
+++ b/devstack/lib/ironic
@@ -246,6 +246,10 @@ IRONIC_DEPLOY_RAMDISK=${IRONIC_DEPLOY_RAMDISK:-$TOP_DIR/files/ir-deploy-$IRONIC_
 IRONIC_DEPLOY_KERNEL=${IRONIC_DEPLOY_KERNEL:-$TOP_DIR/files/ir-deploy-$IRONIC_DEPLOY_DRIVER.kernel}
 IRONIC_DEPLOY_ISO=${IRONIC_DEPLOY_ISO:-$TOP_DIR/files/ir-deploy-$IRONIC_DEPLOY_DRIVER.iso}
 
+# If present, this file is used to deploy/boot nodes over virtual media
+# (The value must be an absolute path)
+IRONIC_EFIBOOT=${IRONIC_EFIBOOT:-$TOP_DIR/files/ir-deploy-$IRONIC_DEPLOY_DRIVER.efiboot}
+
 # NOTE(jroll) this needs to be updated when stable branches are cut
 IPA_DOWNLOAD_BRANCH=${IPA_DOWNLOAD_BRANCH:-master}
 IPA_DOWNLOAD_BRANCH=$(echo $IPA_DOWNLOAD_BRANCH | tr / -)
@@ -528,6 +532,14 @@ if [[ "$IRONIC_BOOT_MODE" == "uefi" ]]; then
         die $LINENO "Boot mode UEFI only works in Ubuntu or Fedora for now."
     fi
 
+    if is_arch "x86_64"; then
+        if is_ubuntu; then
+            install_package grub-efi
+        elif is_fedora; then
+            install_package grub2 grub2-efi
+        fi
+    fi
+
     if is_ubuntu && [[ -z $IRONIC_GRUB2_FILE ]]; then
         IRONIC_GRUB2_SHIM_FILE=/usr/lib/shim/shimx64.efi
         IRONIC_GRUB2_FILE=/usr/lib/grub/x86_64-efi-signed/grubx64.efi.signed
@@ -2519,6 +2531,63 @@ function build_ipa_dib_ramdisk {
     rm -rf $tempdir
 }
 
+# download EFI boot loader image and upload it to glance
+# this function sets ``IRONIC_EFIBOOT_ID``
+function upload_baremetal_ironic_efiboot {
+    declare -g IRONIC_EFIBOOT_ID
+
+    local efiboot_name
+    efiboot_name=$(basename $IRONIC_EFIBOOT)
+
+    echo_summary "Building and uploading EFI boot image for ironic"
+
+    if [ ! -e "$IRONIC_EFIBOOT" ]; then
+
+        local efiboot_path
+        efiboot_path=$(mktemp -d --tmpdir=${DEST})/$efiboot_name
+
+        local efiboot_mount
+        efiboot_mount=$(mktemp -d --tmpdir=${DEST})
+
+        dd if=/dev/zero \
+            of=$efiboot_path \
+            bs=4096 count=1024
+
+        mkfs.fat -s 4 -r 512 -S 4096 $efiboot_path
+
+        sudo mount $efiboot_path $efiboot_mount
+
+        sudo mkdir -p $efiboot_mount/efi/boot
+
+        sudo grub-mkimage \
+            -C xz \
+            -O x86_64-efi \
+            -p /boot/grub \
+            -o $efiboot_mount/efi/boot/bootx64.efi \
+            boot linux linuxefi search normal configfile \
+            part_gpt btrfs ext2 fat iso9660 loopback \
+            test keystatus gfxmenu regexp probe \
+            efi_gop efi_uga all_video gfxterm font \
+            echo read ls cat png jpeg halt reboot
+
+        sudo umount $efiboot_mount
+
+        # load efiboot into glance
+        IRONIC_EFIBOOT_ID=$(openstack \
+            image create \
+            $efiboot_name \
+            --public --disk-format=raw \
+            --container-format=bare \
+            -f value -c id \
+            < $efiboot_path)
+        die_if_not_set $LINENO IRONIC_EFIBOOT_ID "Failed to load EFI bootloader image into glance"
+
+        mv $efiboot_path $IRONIC_EFIBOOT
+
+        iniset $IRONIC_CONF_FILE conductor bootloader $IRONIC_EFIBOOT_ID
+    fi
+}
+
 # build deploy kernel+ramdisk, then upload them to glance
 # this function sets ``IRONIC_DEPLOY_KERNEL_ID``, ``IRONIC_DEPLOY_RAMDISK_ID``
 function upload_baremetal_ironic_deploy {
@@ -2611,6 +2680,11 @@ function prepare_baremetal_basic_ops {
     fi
 
     upload_baremetal_ironic_deploy
+
+    if [[ "$IRONIC_BOOT_MODE" == "uefi" && is_deployed_by_redfish ]]; then
+        upload_baremetal_ironic_efiboot
+    fi
+
     configure_tftpd
     configure_iptables
 }
diff --git a/ironic/conf/conductor.py b/ironic/conf/conductor.py
index 03c235c769..1dd21355b8 100644
--- a/ironic/conf/conductor.py
+++ b/ironic/conf/conductor.py
@@ -240,6 +240,13 @@ opts = [
                mutable=True,
                help=_('Glance ID, http:// or file:// URL of the initramfs of '
                       'the default rescue image.')),
+    cfg.StrOpt('bootloader',
+               mutable=True,
+               help=_('Glance ID, http:// or file:// URL of the EFI system '
+                      'partition image containing EFI boot loader. This image '
+                      'will be used by ironic when building UEFI-bootable ISO '
+                      'out of kernel and ramdisk. Required for UEFI boot from '
+                      'partition images.')),
 ]
 
 
diff --git a/ironic/conf/redfish.py b/ironic/conf/redfish.py
index a49f2e5d1b..20cd3af5e0 100644
--- a/ironic/conf/redfish.py
+++ b/ironic/conf/redfish.py
@@ -43,7 +43,18 @@ opts = [
                         ('auto', _('Try HTTP session authentication first, '
                                    'fall back to basic HTTP authentication'))],
                default='auto',
-               help=_('Redfish HTTP client authentication method.'))
+               help=_('Redfish HTTP client authentication method.')),
+    cfg.StrOpt('swift_container',
+               default='ironic_redfish_container',
+               help=_('The Swift container to store Redfish driver data.')),
+    cfg.IntOpt('swift_object_expiry_timeout',
+               default=900,
+               help=_('Amount of time in seconds for Swift objects to '
+                      'auto-expire.')),
+    cfg.StrOpt('kernel_append_params',
+               default='nofb nomodeset vga=normal',
+               help=_('Additional kernel parameters for baremetal '
+                      'Virtual Media boot.')),
 ]
 
 
diff --git a/ironic/drivers/modules/redfish/boot.py b/ironic/drivers/modules/redfish/boot.py
new file mode 100644
index 0000000000..263649939d
--- /dev/null
+++ b/ironic/drivers/modules/redfish/boot.py
@@ -0,0 +1,818 @@
+# Copyright 2019 Red Hat, Inc.
+# 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.
+import tempfile
+
+from oslo_log import log
+from oslo_utils import importutils
+
+from ironic.common import boot_devices
+from ironic.common import exception
+from ironic.common.glance_service import service_utils
+from ironic.common.i18n import _
+from ironic.common import images
+from ironic.common import states
+from ironic.common import swift
+from ironic.conductor import utils as manager_utils
+from ironic.conf import CONF
+from ironic.drivers import base
+from ironic.drivers.modules import boot_mode_utils
+from ironic.drivers.modules import deploy_utils
+from ironic.drivers.modules.redfish import utils as redfish_utils
+
+LOG = log.getLogger(__name__)
+
+REQUIRED_PROPERTIES = {
+    'deploy_kernel': _("URL or Glance UUID of the deployment kernel. "
+                       "Required."),
+    'deploy_ramdisk': _("URL or Glance UUID of the ramdisk that is "
+                        "mounted at boot time. Required.")
+}
+
+OPTIONAL_PROPERTIES = {
+    'config_via_floppy': _("Boolean value to indicate whether or not the "
+                           "driver should use virtual media Floppy device "
+                           "for passing configuration information to the "
+                           "ramdisk. Defaults to False. Optional."),
+    'bootloader': _("URL or Glance UUID  of the EFI system partition "
+                    "image containing EFI boot loader. This image will be "
+                    "used by ironic when building UEFI-bootable ISO "
+                    "out of kernel and ramdisk. Required for UEFI "
+                    "boot from partition images.")
+}
+
+RESCUE_PROPERTIES = {
+    'rescue_kernel': _('URL or Glance UUID of the rescue kernel. This value '
+                       'is required for rescue mode.'),
+    'rescue_ramdisk': _('URL or Glance UUID of the rescue ramdisk with agent '
+                        'that is used at node rescue time. This value is '
+                        'required for rescue mode.'),
+}
+
+COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
+COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
+COMMON_PROPERTIES.update(RESCUE_PROPERTIES)
+
+KERNEL_RAMDISK_LABELS = {
+    'deploy': REQUIRED_PROPERTIES,
+    'rescue': RESCUE_PROPERTIES
+}
+
+sushy = importutils.try_import('sushy')
+
+
+class RedfishVirtualMediaBoot(base.BootInterface):
+    """Virtual media boot interface over Redfish.
+
+    Virtual Media allows booting the system from the "virtual"
+    CD/DVD drive containing the user image that BMC "inserts"
+    into the drive.
+
+    The CD/DVD images must be in ISO format and (depending on
+    BMC implementation) could be pulled over HTTP, served as
+    iSCSI targets or NFS volumes.
+
+    The baseline boot workflow looks like this:
+
+    1. Pull kernel, ramdisk and ESP (FAT partition image with EFI boot
+       loader) images (ESP is only needed for UEFI boot)
+    2. Create bootable ISO out of images (#1), push it to Glance and
+       pass to the BMC as Swift temporary URL
+    3. Optionally create floppy image with desired system configuration data,
+       push it to Glance and pass to the BMC as Swift temporary URL
+    4. Insert CD/DVD and (optionally) floppy images and set proper boot mode
+
+    For building deploy or rescue ISO, redfish boot interface uses
+    `deploy_kernel`/`deploy_ramdisk` or `rescue_kernel`/`rescue_ramdisk`
+    properties from `[instance_info]` or `[driver_info]`.
+
+    For building boot (user) ISO, redfish boot interface seeks `kernel_id`
+    and `ramdisk_id` properties in the Glance image metadata found in
+    `[instance_info]image_source` node property.
+    """
+
+    capabilities = ['iscsi_volume_boot', 'ramdisk_boot']
+
+    def __init__(self):
+        """Initialize the Redfish virtual media boot interface.
+
+        :raises: DriverLoadError if the driver can't be loaded due to
+            missing dependencies
+        """
+        super(RedfishVirtualMediaBoot, self).__init__()
+        if not sushy:
+            raise exception.DriverLoadError(
+                driver='redfish',
+                reason=_('Unable to import the sushy library'))
+
+    @staticmethod
+    def _parse_driver_info(node):
+        """Gets the driver specific Node deployment info.
+
+        This method validates whether the 'driver_info' property of the
+        supplied node contains the required or optional information properly
+        for this driver to deploy images to the node.
+
+        :param node: a target node of the deployment
+        :returns: the driver_info values of the node.
+        :raises: MissingParameterValue, if any of the required parameters are
+            missing.
+        :raises: InvalidParameterValue, if any of the parameters have invalid
+            value.
+        """
+        d_info = node.driver_info
+
+        mode = deploy_utils.rescue_or_deploy_mode(node)
+        params_to_check = KERNEL_RAMDISK_LABELS[mode]
+
+        deploy_info = {option: d_info.get(option)
+                       for option in params_to_check}
+
+        if not any(deploy_info.values()):
+            # NOTE(dtantsur): avoid situation when e.g. deploy_kernel comes
+            # from driver_info but deploy_ramdisk comes from configuration,
+            # since it's a sign of a potential operator's mistake.
+            deploy_info = {k: getattr(CONF.conductor, k)
+                           for k in params_to_check}
+
+        error_msg = _("Error validating Redfish virtual media. Some "
+                      "parameters were missing in node's driver_info")
+
+        deploy_utils.check_for_missing_params(deploy_info, error_msg)
+
+        deploy_info.update(
+            {option: d_info.get(option, getattr(CONF.conductor, option, None))
+             for option in OPTIONAL_PROPERTIES})
+
+        deploy_info.update(redfish_utils.parse_driver_info(node))
+
+        return deploy_info
+
+    @staticmethod
+    def _parse_instance_info(node):
+        """Gets the instance specific Node deployment info.
+
+        This method validates whether the 'instance_info' property of the
+        supplied node contains the required or optional information properly
+        for this driver to deploy images to the node.
+
+        :param node: a target node of the deployment
+        :returns:  the instance_info values of the node.
+        :raises: InvalidParameterValue, if any of the parameters have invalid
+            value.
+        """
+        deploy_info = node.instance_info.copy()
+
+        # NOTE(etingof): this method is currently no-op, here for completeness
+        return deploy_info
+
+    @classmethod
+    def _parse_deploy_info(cls, node):
+        """Gets the instance and driver specific Node deployment info.
+
+        This method validates whether the 'instance_info' and 'driver_info'
+        property of the supplied node contains the required information for
+        this driver to deploy images to the node.
+
+        :param node: a target node of the deployment
+        :returns: a dict with the instance_info and driver_info values.
+        :raises: MissingParameterValue, if any of the required parameters are
+            missing.
+        :raises: InvalidParameterValue, if any of the parameters have invalid
+            value.
+        """
+        deploy_info = {}
+        deploy_info.update(deploy_utils.get_image_instance_info(node))
+        deploy_info.update(cls._parse_driver_info(node))
+        deploy_info.update(cls._parse_instance_info(node))
+
+        return deploy_info
+
+    @staticmethod
+    def _delete_from_swift(task, container, object_name):
+        LOG.debug("Cleaning up image %(name)s from Swift container "
+                  "%(container)s for node "
+                  "%(node)s", {'node': task.node.uuid,
+                               'name': object_name,
+                               'container': container})
+
+        swift_api = swift.SwiftAPI()
+
+        try:
+            swift_api.delete_object(container, object_name)
+
+        except exception.SwiftOperationError as e:
+            LOG.warning("Failed to clean up image %(image)s for node "
+                        "%(node)s. Error: %(error)s.",
+                        {'node': task.node.uuid, 'image': object_name,
+                         'error': e})
+
+    @staticmethod
+    def _get_floppy_image_name(node):
+        """Returns the floppy image name for a given node.
+
+        :param node: the node for which image name is to be provided.
+        """
+        return "image-%s" % node.uuid
+
+    @classmethod
+    def _cleanup_floppy_image(cls, task):
+        """Deletes the floppy image if it was created for the node.
+
+        :param task: an ironic node object.
+        """
+        floppy_object_name = cls._get_floppy_image_name(task.node)
+
+        cls._delete_from_swift(
+            task, CONF.redfish.swift_container, floppy_object_name)
+
+    @classmethod
+    def _prepare_floppy_image(cls, task, params=None):
+        """Prepares the floppy image for passing the parameters.
+
+        This method prepares a temporary VFAT filesystem image and adds
+        a file into the image which contains parameters to be passed to
+        the ramdisk. Then this method uploads built image to Swift
+        '[redfish]swift_container', setting it to auto expire after
+        '[redfish]swift_object_expiry_timeout' seconds. Finally, a
+        temporary Swift URL is returned addressing Swift object just
+        created.
+
+        :param task: a TaskManager instance containing the node to act on.
+        :param params: a dictionary containing 'parameter name'->'value'
+            mapping to be passed to deploy or rescue image via floppy image.
+        :raises: ImageCreationFailed, if it failed while creating the floppy
+            image.
+        :raises: SwiftOperationError, if any operation with Swift fails.
+        :returns: image URL for the floppy image.
+        """
+        object_name = cls._get_floppy_image_name(task.node)
+
+        container = CONF.redfish.swift_container
+        timeout = CONF.redfish.swift_object_expiry_timeout
+
+        object_headers = {'X-Delete-After': str(timeout)}
+        swift_api = swift.SwiftAPI()
+
+        LOG.debug("Trying to create floppy image for node "
+                  "%(node)s", {'node': task.node.uuid})
+
+        with tempfile.NamedTemporaryFile(
+                dir=CONF.tempdir, suffix='.img') as vfat_image_tmpfile_obj:
+
+            vfat_image_tmpfile = vfat_image_tmpfile_obj.name
+            images.create_vfat_image(vfat_image_tmpfile, parameters=params)
+
+            swift_api.create_object(container, object_name, vfat_image_tmpfile,
+                                    object_headers=object_headers)
+
+        image_url = swift_api.get_temp_url(container, object_name, timeout)
+
+        LOG.debug("Created floppy image %(name)s in Swift for node %(node)s, "
+                  "exposed as temporary URL "
+                  "%(url)s", {'node': task.node.uuid,
+                              'name': object_name,
+                              'url': image_url})
+
+        return image_url
+
+    @staticmethod
+    def _get_iso_image_name(node):
+        """Returns the boot iso image name for a given node.
+
+        :param node: the node for which image name is to be provided.
+        """
+        return "boot-%s" % node.uuid
+
+    @classmethod
+    def _cleanup_iso_image(cls, task):
+        """Deletes the ISO if it was created for the instance.
+
+        :param task: an ironic node object.
+        """
+        iso_object_name = cls._get_iso_image_name(task.node)
+
+        cls._delete_from_swift(
+            task, CONF.redfish.swift_container, iso_object_name)
+
+    @classmethod
+    def _prepare_iso_image(cls, task, kernel_href, ramdisk_href,
+                           bootloader_href=None, root_uuid=None, params=None):
+        """Prepare an ISO to boot the node.
+
+        Build bootable ISO out of `kernel_href` and `ramdisk_href` (and
+        `bootloader` if it's UEFI boot), then push built image up to Swift and
+        return a temporary URL.
+
+        :param task: a TaskManager instance containing the node to act on.
+        :param kernel_href: URL or Glance UUID of the kernel to use
+        :param ramdisk_href: URL or Glance UUID of the ramdisk to use
+        :param bootloader_href: URL or Glance UUID of the EFI bootloader
+             image to use when creating UEFI bootbable ISO
+        :param root_uuid: optional uuid of the root partition.
+        :param params: a dictionary containing 'parameter name'->'value'
+            mapping to be passed to kernel command line.
+        :returns: bootable ISO HTTP URL.
+        :raises: MissingParameterValue, if any of the required parameters are
+            missing.
+        :raises: InvalidParameterValue, if any of the parameters have invalid
+            value.
+        :raises: ImageCreationFailed, if creating ISO image failed.
+        """
+        if not kernel_href or not ramdisk_href:
+            raise exception.InvalidParameterValue(_(
+                "Unable to find kernel or ramdisk for "
+                "building ISO for %(node)s") %
+                {'node': task.node.uuid})
+
+        if deploy_utils.get_boot_option(task.node) == "ramdisk":
+            i_info = task.node.instance_info
+            kernel_params = "root=/dev/ram0 text "
+            kernel_params += i_info.get("ramdisk_kernel_arguments", "")
+        else:
+            kernel_params = CONF.redfish.kernel_append_params
+
+        if params:
+            kernel_params = ' '.join(
+                (kernel_params, ' '.join(
+                    '%s=%s' % kv for kv in params.items())))
+
+        boot_mode = boot_mode_utils.get_boot_mode_for_deploy(task.node)
+
+        LOG.debug("Trying to create %(boot_mode)s ISO image for node %(node)s "
+                  "with kernel %(kernel_href)s, ramdisk %(ramdisk_href)s, "
+                  "bootloader %(bootloader_href)s and kernel params %(params)s"
+                  "", {'node': task.node.uuid,
+                       'boot_mode': boot_mode,
+                       'kernel_href': kernel_href,
+                       'ramdisk_href': ramdisk_href,
+                       'bootloader_href': bootloader_href,
+                       'params': kernel_params})
+
+        with tempfile.NamedTemporaryFile(
+                dir=CONF.tempdir, suffix='.iso') as fileobj:
+            boot_iso_tmp_file = fileobj.name
+            images.create_boot_iso(
+                task.context, boot_iso_tmp_file,
+                kernel_href, ramdisk_href,
+                esp_image_href=bootloader_href,
+                root_uuid=root_uuid,
+                kernel_params=kernel_params,
+                boot_mode=boot_mode)
+
+            iso_object_name = cls._get_iso_image_name(task.node)
+
+            container = CONF.redfish.swift_container
+
+            timeout = CONF.redfish.swift_object_expiry_timeout
+
+            object_headers = {'X-Delete-After': str(timeout)}
+
+            swift_api = swift.SwiftAPI()
+
+            swift_api.create_object(container, iso_object_name,
+                                    boot_iso_tmp_file,
+                                    object_headers=object_headers)
+
+            boot_iso_url = swift_api.get_temp_url(
+                container, iso_object_name, timeout)
+
+        LOG.debug("Created ISO %(name)s in Swift for node %(node)s, exposed "
+                  "as temporary URL %(url)s", {'node': task.node.uuid,
+                                               'name': iso_object_name,
+                                               'url': boot_iso_url})
+
+        return boot_iso_url
+
+    @classmethod
+    def _prepare_deploy_iso(cls, task, params, mode):
+        """Prepare deploy or rescue ISO image
+
+        Build bootable ISO out of
+        `[driver_info]/deploy_kernel`/`[driver_info]/deploy_ramdisk` or
+        `[driver_info]/rescue_kernel`/`[driver_info]/rescue_ramdisk`
+        and `[driver_info]/bootloader`, then push built image up to Glance
+        and return temporary Swift URL to the image.
+
+        :param task: a TaskManager instance containing the node to act on.
+        :param params: a dictionary containing 'parameter name'->'value'
+            mapping to be passed to kernel command line.
+        :param mode: either 'deploy' or 'rescue'.
+        :returns: bootable ISO HTTP URL.
+        :raises: MissingParameterValue, if any of the required parameters are
+            missing.
+        :raises: InvalidParameterValue, if any of the parameters have invalid
+            value.
+        :raises: ImageCreationFailed, if creating ISO image failed.
+        """
+        node = task.node
+
+        d_info = cls._parse_driver_info(node)
+
+        kernel_href = d_info.get('%s_kernel' % mode)
+        ramdisk_href = d_info.get('%s_ramdisk' % mode)
+        bootloader_href = d_info.get('bootloader')
+
+        return cls._prepare_iso_image(
+            task, kernel_href, ramdisk_href, bootloader_href, params=params)
+
+    @classmethod
+    def _prepare_boot_iso(cls, task, root_uuid=None):
+        """Prepare boot ISO image
+
+        Build bootable ISO out of `[instance_info]/kernel`,
+        `[instance_info]/ramdisk` and `[driver_info]/bootloader` if present.
+        Otherwise, read `kernel_id` and `ramdisk_id` from
+        `[instance_info]/image_source` Glance image metadata.
+
+        Push produced ISO image up to Glance and return temporary Swift
+        URL to the image.
+
+        :param task: a TaskManager instance containing the node to act on.
+        :returns: bootable ISO HTTP URL.
+        :raises: MissingParameterValue, if any of the required parameters are
+            missing.
+        :raises: InvalidParameterValue, if any of the parameters have invalid
+            value.
+        :raises: ImageCreationFailed, if creating ISO image failed.
+        """
+        node = task.node
+
+        d_info = cls._parse_deploy_info(node)
+
+        kernel_href = node.instance_info.get('kernel')
+        ramdisk_href = node.instance_info.get('ramdisk')
+
+        if not kernel_href or not ramdisk_href:
+
+            image_href = d_info['image_source']
+
+            image_properties = (
+                images.get_image_properties(
+                    task.context, image_href, ['kernel_id', 'ramdisk_id']))
+
+            if not kernel_href:
+                kernel_href = image_properties.get('kernel_id')
+
+            if not ramdisk_href:
+                ramdisk_href = image_properties.get('ramdisk_id')
+
+        if not kernel_href or not ramdisk_href:
+            raise exception.InvalidParameterValue(_(
+                "Unable to find kernel or ramdisk for "
+                "to generate boot ISO for %(node)s") %
+                {'node': task.node.uuid})
+
+        bootloader_href = d_info.get('bootloader')
+
+        return cls._prepare_iso_image(
+            task, kernel_href, ramdisk_href, bootloader_href,
+            root_uuid=root_uuid)
+
+    def get_properties(self):
+        """Return the properties of the interface.
+
+        :returns: dictionary of <property name>:<property description> entries.
+        """
+        return REQUIRED_PROPERTIES
+
+    @classmethod
+    def _validate_driver_info(cls, task):
+        """Validate the prerequisites for virtual media based boot.
+
+        This method validates whether the 'driver_info' property of the
+        supplied node contains the required information for this driver.
+
+        :param task: a TaskManager instance containing the node to act on.
+        :raises: InvalidParameterValue if any parameters are incorrect
+        :raises: MissingParameterValue if some mandatory information
+            is missing on the node
+        """
+        node = task.node
+
+        cls._parse_driver_info(node)
+
+    @classmethod
+    def _validate_instance_info(cls, task):
+        """Validate instance image information for the task's node.
+
+        This method validates whether the 'instance_info' property of the
+        supplied node contains the required information for this driver.
+
+        :param task: a TaskManager instance containing the node to act on.
+        :raises: InvalidParameterValue if any parameters are incorrect
+        :raises: MissingParameterValue if some mandatory information
+            is missing on the node
+        """
+        node = task.node
+
+        d_info = cls._parse_deploy_info(node)
+
+        if node.driver_internal_info.get('is_whole_disk_image'):
+            props = []
+
+        elif service_utils.is_glance_image(d_info['image_source']):
+            props = ['kernel_id', 'ramdisk_id']
+
+        else:
+            props = ['kernel', 'ramdisk']
+
+        deploy_utils.validate_image_properties(task.context, d_info, props)
+
+    def validate(self, task):
+        """Validate the deployment information for the task's node.
+
+        This method validates whether the 'driver_info' and/or 'instance_info'
+        properties of the task's node contains the required information for
+        this interface to function.
+
+        :param task: A TaskManager instance containing the node to act on.
+        :raises: InvalidParameterValue on malformed parameter(s)
+        :raises: MissingParameterValue on missing parameter(s)
+        """
+        self._validate_driver_info(task)
+
+        if task.driver.storage.should_write_image(task):
+            self._validate_instance_info(task)
+
+    def prepare_ramdisk(self, task, ramdisk_params):
+        """Prepares the boot of deploy or rescue ramdisk over virtual media.
+
+        This method prepares the boot of the deploy or rescue ramdisk after
+        reading relevant information from the node's driver_info and
+        instance_info.
+
+        :param task: A task from TaskManager.
+        :param ramdisk_params: the parameters to be passed to the ramdisk.
+        :returns: None
+        :raises: MissingParameterValue, if some information is missing in
+            node's driver_info or instance_info.
+        :raises: InvalidParameterValue, if some information provided is
+            invalid.
+        :raises: IronicException, if some power or set boot boot device
+            operation failed on the node.
+        """
+        node = task.node
+        # NOTE(TheJulia): If this method is being called by something
+        # aside from deployment, clean and rescue, such as conductor takeover,
+        # we should treat this as a no-op and move on otherwise we would
+        # modify the state of the node due to virtual media operations.
+        if node.provision_state not in (states.DEPLOYING,
+                                        states.CLEANING,
+                                        states.RESCUING):
+            return
+
+        manager_utils.node_power_action(task, states.POWER_OFF)
+
+        d_info = self._parse_driver_info(node)
+
+        config_via_floppy = d_info.get('config_via_floppy')
+
+        deploy_nic_mac = deploy_utils.get_single_nic_with_vif_port_id(task)
+        ramdisk_params['BOOTIF'] = deploy_nic_mac
+
+        if config_via_floppy:
+
+            if self._has_vmedia_device(task, sushy.VIRTUAL_MEDIA_FLOPPY):
+                # NOTE (etingof): IPA will read the diskette only if
+                # we tell it to
+                ramdisk_params['boot_method'] = 'vmedia'
+
+                floppy_ref = self._prepare_floppy_image(
+                    task, params=ramdisk_params)
+
+                self._eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY)
+                self._insert_vmedia(
+                    task, floppy_ref, sushy.VIRTUAL_MEDIA_FLOPPY)
+
+                LOG.debug('Inserted virtual floppy with configuration for '
+                          'node %(node)s', {'node': task.node.uuid})
+
+            else:
+                LOG.warning('Config via floppy is requested, but '
+                            'Floppy drive is not available on node '
+                            '%(node)s', {'node': task.node.uuid})
+
+        mode = deploy_utils.rescue_or_deploy_mode(node)
+
+        iso_ref = self._prepare_deploy_iso(task, ramdisk_params, mode)
+
+        self._eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD)
+        self._insert_vmedia(task, iso_ref, sushy.VIRTUAL_MEDIA_CD)
+
+        boot_mode_utils.sync_boot_mode(task)
+
+        manager_utils.node_set_boot_device(task, boot_devices.CDROM)
+
+        LOG.debug("Node %(node)s is set to one time boot from "
+                  "%(device)s", {'node': task.node.uuid,
+                                 'device': boot_devices.CDROM})
+
+    def clean_up_ramdisk(self, task):
+        """Cleans up the boot of ironic ramdisk.
+
+        This method cleans up the environment that was setup for booting the
+        deploy ramdisk.
+
+        :param task: A task from TaskManager.
+        :returns: None
+        """
+        node = task.node
+
+        d_info = self._parse_driver_info(node)
+
+        config_via_floppy = d_info.get('config_via_floppy')
+
+        LOG.debug("Cleaning up deploy boot for "
+                  "%(node)s", {'node': task.node.uuid})
+
+        self._eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD)
+        self._cleanup_iso_image(task)
+
+        if (config_via_floppy and
+                self._has_vmedia_device(task, sushy.VIRTUAL_MEDIA_FLOPPY)):
+            self._eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY)
+            self._cleanup_floppy_image(task)
+
+    def prepare_instance(self, task):
+        """Prepares the boot of instance over virtual media.
+
+        This method prepares the boot of the instance after reading
+        relevant information from the node's instance_info.
+
+        The internal logic is as follows:
+
+        - If `boot_option` requested for this deploy is 'local', then set the
+          node to boot from disk.
+        - Unless `boot_option` requested for this deploy is 'ramdisk', pass
+          root disk/partition ID to virtual media boot image
+        - Otherwise build boot image, insert it into virtual media device
+          and set node to boot from CD.
+
+        :param task: a task from TaskManager.
+        :returns: None
+        :raises: InstanceDeployFailure, if its try to boot iSCSI volume in
+                 'BIOS' boot mode.
+        """
+        node = task.node
+
+        boot_option = deploy_utils.get_boot_option(node)
+
+        self.clean_up_instance(task)
+        iwdi = node.driver_internal_info.get('is_whole_disk_image')
+        if boot_option == "local" or iwdi:
+            manager_utils.node_set_boot_device(
+                task, boot_devices.DISK, persistent=True)
+
+            LOG.debug("Node %(node)s is set to permanently boot from local "
+                      "%(device)s", {'node': task.node.uuid,
+                                     'device': boot_devices.DISK})
+            return
+
+        params = {}
+
+        if boot_option != 'ramdisk':
+            root_uuid = node.driver_internal_info.get('root_uuid_or_disk_id')
+
+            if not root_uuid and task.driver.storage.should_write_image(task):
+                LOG.warning(
+                    "The UUID of the root partition could not be found for "
+                    "node %s. Booting instance from disk anyway.", node.uuid)
+
+                manager_utils.node_set_boot_device(
+                    task, boot_devices.DISK, persistent=True)
+
+                return
+
+            params.update(root_uuid=root_uuid)
+
+        iso_ref = self._prepare_boot_iso(task, **params)
+
+        self._eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD)
+        self._insert_vmedia(task, iso_ref, sushy.VIRTUAL_MEDIA_CD)
+
+        boot_mode_utils.sync_boot_mode(task)
+
+        manager_utils.node_set_boot_device(
+            task, boot_devices.CDROM, persistent=True)
+
+        LOG.debug("Node %(node)s is set to permanently boot from "
+                  "%(device)s", {'node': task.node.uuid,
+                                 'device': boot_devices.CDROM})
+
+    def clean_up_instance(self, task):
+        """Cleans up the boot of instance.
+
+        This method cleans up the environment that was setup for booting
+        the instance.
+
+        :param task: A task from TaskManager.
+        :returns: None
+        """
+        LOG.debug("Cleaning up instance boot for "
+                  "%(node)s", {'node': task.node.uuid})
+
+        self._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:
+            self._eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY)
+
+        self._cleanup_iso_image(task)
+
+    @staticmethod
+    def _insert_vmedia(task, boot_url, boot_device):
+        """Insert bootable ISO image into virtual CD or DVD
+
+        :param task: A task from TaskManager.
+        :param boot_url: URL to a bootable ISO image
+        :param boot_device: sushy boot device e.g. `VIRTUAL_MEDIA_CD`,
+            `VIRTUAL_MEDIA_DVD` or `VIRTUAL_MEDIA_FLOPPY`
+        :raises: InvalidParameterValue, if no suitable virtual CD or DVD is
+            found on the node.
+        """
+        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:
+                    continue
+
+                if v_media.inserted:
+                    if v_media.image == boot_url:
+                        LOG.debug("Boot media %(boot_url)s is already "
+                                  "inserted into %(boot_device)s for node "
+                                  "%(node)s", {'node': task.node.uuid,
+                                               'boot_url': boot_url,
+                                               'boot_device': boot_device})
+                        return
+
+                    continue
+
+                v_media.insert_media(boot_url, inserted=True,
+                                     write_protected=True)
+
+                LOG.info("Inserted boot media %(boot_url)s into "
+                         "%(boot_device)s for node "
+                         "%(node)s", {'node': task.node.uuid,
+                                      'boot_url': boot_url,
+                                      'boot_device': boot_device})
+                return
+
+        raise exception.InvalidParameterValue(
+            _('No suitable virtual media device found'))
+
+    @staticmethod
+    def _eject_vmedia(task, boot_device=None):
+        """Eject virtual CDs and DVDs
+
+        :param task: A task from TaskManager.
+        :param boot_device: sushy boot device e.g. `VIRTUAL_MEDIA_CD`,
+            `VIRTUAL_MEDIA_DVD` or `VIRTUAL_MEDIA_FLOPPY` or `None` to
+            eject everything (default).
+        :raises: InvalidParameterValue, if no suitable virtual CD or DVD is
+            found on the node.
+        """
+        system = redfish_utils.get_system(task.node)
+
+        for manager in system.managers:
+            for v_media in manager.virtual_media.get_members():
+                if boot_device and boot_device not in v_media.media_types:
+                    continue
+
+                inserted = v_media.inserted
+
+                if inserted:
+                    v_media.eject_media()
+
+                LOG.info("Boot media is%(already)s ejected from "
+                         "%(boot_device)s for node %(node)s"
+                         "", {'node': task.node.uuid,
+                              'already': '' if inserted else ' already',
+                              'boot_device': v_media.name})
+
+    @staticmethod
+    def _has_vmedia_device(task, boot_device):
+        """Indicate if device exists at any of the managers
+
+        :param task: A task from TaskManager.
+        :param boot_device: sushy boot device e.g. `VIRTUAL_MEDIA_CD`,
+            `VIRTUAL_MEDIA_DVD` or `VIRTUAL_MEDIA_FLOPPY`.
+        """
+        system = redfish_utils.get_system(task.node)
+
+        for manager in system.managers:
+            for v_media in manager.virtual_media.get_members():
+                if boot_device in v_media.media_types:
+                    return True
diff --git a/ironic/drivers/redfish.py b/ironic/drivers/redfish.py
index 1610a2613e..fe082cfe4a 100644
--- a/ironic/drivers/redfish.py
+++ b/ironic/drivers/redfish.py
@@ -15,8 +15,11 @@
 
 from ironic.drivers import generic
 from ironic.drivers.modules import inspector
+from ironic.drivers.modules import ipxe
 from ironic.drivers.modules import noop
+from ironic.drivers.modules import pxe
 from ironic.drivers.modules.redfish import bios as redfish_bios
+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
@@ -45,3 +48,9 @@ class RedfishHardware(generic.GenericHardware):
         """List of supported power interfaces."""
         return [redfish_inspect.RedfishInspect, inspector.Inspector,
                 noop.NoInspect]
+
+    @property
+    def supported_boot_interfaces(self):
+        """List of supported boot interfaces."""
+        return [redfish_boot.RedfishVirtualMediaBoot,
+                ipxe.iPXEBoot, pxe.PXEBoot]
diff --git a/ironic/tests/unit/drivers/modules/redfish/test_bios.py b/ironic/tests/unit/drivers/modules/redfish/test_bios.py
index 228d148667..98c2aa149d 100644
--- a/ironic/tests/unit/drivers/modules/redfish/test_bios.py
+++ b/ironic/tests/unit/drivers/modules/redfish/test_bios.py
@@ -20,8 +20,8 @@ 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 import pxe as pxe_boot
 from ironic.drivers.modules.redfish import bios as redfish_bios
+from ironic.drivers.modules.redfish import boot as redfish_boot
 from ironic.drivers.modules.redfish import utils as redfish_utils
 from ironic import objects
 from ironic.tests.unit.db import base as db_base
@@ -50,6 +50,7 @@ class RedfishBiosTestCase(db_base.DbTestCase):
         self.config(enabled_bios_interfaces=['redfish'],
                     enabled_hardware_types=['redfish'],
                     enabled_power_interfaces=['redfish'],
+                    enabled_boot_interfaces=['redfish-virtual-media'],
                     enabled_management_interfaces=['redfish'])
         self.node = obj_utils.create_test_node(
             self.context, driver='redfish', driver_info=INFO_DICT)
@@ -160,7 +161,7 @@ class RedfishBiosTestCase(db_base.DbTestCase):
             mock_setting_list.delete.assert_called_once_with(
                 task.context, task.node.id, delete_names)
 
-    @mock.patch.object(pxe_boot.PXEBoot, 'prepare_ramdisk',
+    @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(redfish_utils, 'get_system', autospec=True)
diff --git a/ironic/tests/unit/drivers/modules/redfish/test_boot.py b/ironic/tests/unit/drivers/modules/redfish/test_boot.py
new file mode 100644
index 0000000000..7e0d24766b
--- /dev/null
+++ b/ironic/tests/unit/drivers/modules/redfish/test_boot.py
@@ -0,0 +1,867 @@
+# Copyright 2019 Red Hat, Inc.
+# 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.
+
+import mock
+from oslo_utils import importutils
+
+from ironic.common import boot_devices
+from ironic.common import exception
+from ironic.common import images
+from ironic.common import states
+from ironic.conductor import task_manager
+from ironic.drivers.modules import boot_mode_utils
+from ironic.drivers.modules import deploy_utils
+from ironic.drivers.modules.redfish import boot as redfish_boot
+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()
+
+
+@mock.patch('eventlet.greenthread.sleep', lambda _t: None)
+class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
+
+    def setUp(self):
+        super(RedfishVirtualMediaBootTestCase, 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'])
+        self.node = obj_utils.create_test_node(
+            self.context, driver='redfish', driver_info=INFO_DICT)
+
+    @mock.patch.object(redfish_boot, 'sushy', None)
+    def test_loading_error(self):
+        self.assertRaisesRegex(
+            exception.DriverLoadError,
+            'Unable to import the sushy library',
+            redfish_boot.RedfishVirtualMediaBoot)
+
+    def test_parse_driver_info_deploy(self):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            task.node.driver_info.update(
+                {'deploy_kernel': 'kernel',
+                 'deploy_ramdisk': 'ramdisk',
+                 'bootloader': 'bootloader'}
+            )
+
+            actual_driver_info = task.driver.boot._parse_driver_info(task.node)
+
+            self.assertIn('kernel', actual_driver_info['deploy_kernel'])
+            self.assertIn('ramdisk', actual_driver_info['deploy_ramdisk'])
+            self.assertIn('bootloader', actual_driver_info['bootloader'])
+
+    def test_parse_driver_info_rescue(self):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            task.node.provision_state = states.RESCUING
+            task.node.driver_info.update(
+                {'rescue_kernel': 'kernel',
+                 'rescue_ramdisk': 'ramdisk',
+                 'bootloader': 'bootloader'}
+            )
+
+            actual_driver_info = task.driver.boot._parse_driver_info(task.node)
+
+            self.assertIn('kernel', actual_driver_info['rescue_kernel'])
+            self.assertIn('ramdisk', actual_driver_info['rescue_ramdisk'])
+            self.assertIn('bootloader', actual_driver_info['bootloader'])
+
+    def test_parse_driver_info_exc(self):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            self.assertRaises(exception.MissingParameterValue,
+                              task.driver.boot._parse_driver_info,
+                              task.node)
+
+    def _test_parse_driver_info_from_conf(self, mode='deploy'):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            if mode == 'rescue':
+                task.node.provision_state = states.RESCUING
+
+            expected = {
+                '%s_ramdisk' % mode: 'glance://%s_ramdisk_uuid' % mode,
+                '%s_kernel' % mode: 'glance://%s_kernel_uuid' % mode
+            }
+
+            self.config(group='conductor', **expected)
+
+            image_info = task.driver.boot._parse_driver_info(task.node)
+
+            for key, value in expected.items():
+                self.assertEqual(value, image_info[key])
+
+    def test_parse_driver_info_from_conf_deploy(self):
+        self._test_parse_driver_info_from_conf()
+
+    def test_parse_driver_info_from_conf_rescue(self):
+        self._test_parse_driver_info_from_conf(mode='rescue')
+
+    def _test_parse_driver_info_mixed_source(self, mode='deploy'):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            if mode == 'rescue':
+                task.node.provision_state = states.RESCUING
+
+            kernel_config = {
+                '%s_kernel' % mode: 'glance://%s_kernel_uuid' % mode
+            }
+
+            ramdisk_config = {
+                '%s_ramdisk' % mode: 'glance://%s_ramdisk_uuid' % mode,
+            }
+
+            self.config(group='conductor', **kernel_config)
+
+            task.node.driver_info.update(ramdisk_config)
+
+            self.assertRaises(exception.MissingParameterValue,
+                              task.driver.boot._parse_driver_info, task.node)
+
+    def test_parse_driver_info_mixed_source_deploy(self):
+        self._test_parse_driver_info_mixed_source()
+
+    def test_parse_driver_info_mixed_source_rescue(self):
+        self._test_parse_driver_info_mixed_source(mode='rescue')
+
+    def test_parse_deploy_info(self):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            task.node.driver_info.update(
+                {'deploy_kernel': 'kernel',
+                 'deploy_ramdisk': 'ramdisk',
+                 'bootloader': 'bootloader'}
+            )
+
+            task.node.instance_info.update(
+                {'image_source': 'http://boot/iso',
+                 'kernel': 'http://kernel/img',
+                 'ramdisk': 'http://ramdisk/img'})
+
+            actual_instance_info = task.driver.boot._parse_deploy_info(
+                task.node)
+
+            self.assertEqual(
+                'http://boot/iso', actual_instance_info['image_source'])
+            self.assertEqual(
+                'http://kernel/img', actual_instance_info['kernel'])
+            self.assertEqual(
+                'http://ramdisk/img', actual_instance_info['ramdisk'])
+
+    def test_parse_deploy_info_exc(self):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            self.assertRaises(exception.MissingParameterValue,
+                              task.driver.boot._parse_deploy_info,
+                              task.node)
+
+    @mock.patch.object(redfish_boot, 'swift', autospec=True)
+    def test__cleanup_floppy_image(self, mock_swift):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            task.driver.boot._cleanup_floppy_image(task)
+
+            mock_swift.SwiftAPI.assert_called_once_with()
+            mock_swift_api = mock_swift.SwiftAPI.return_value
+
+            mock_swift_api.delete_object.assert_called_once_with(
+                'ironic_redfish_container', 'image-%s' % task.node.uuid
+            )
+
+    @mock.patch.object(redfish_boot, 'swift', autospec=True)
+    @mock.patch.object(images, 'create_vfat_image', autospec=True)
+    def test__prepare_floppy_image(self, mock_create_vfat_image, mock_swift):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            task.driver.boot._prepare_floppy_image(task)
+
+            mock_create_vfat_image.assert_called_once_with(
+                mock.ANY, parameters=mock.ANY)
+
+            mock_swift.SwiftAPI.assert_called_once_with()
+            mock_swift_api = mock_swift.SwiftAPI.return_value
+
+            mock_swift_api.create_object.assert_called_once_with(
+                mock.ANY, mock.ANY, mock.ANY, mock.ANY)
+
+            mock_swift_api.get_temp_url.assert_called_once_with(
+                mock.ANY, mock.ANY, mock.ANY)
+
+    @mock.patch.object(redfish_boot, 'swift', autospec=True)
+    def test__cleanup_iso_image(self, mock_swift):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            task.driver.boot._cleanup_iso_image(task)
+
+            mock_swift.SwiftAPI.assert_called_once_with()
+            mock_swift_api = mock_swift.SwiftAPI.return_value
+
+            mock_swift_api.delete_object.assert_called_once_with(
+                'ironic_redfish_container', 'boot-%s' % task.node.uuid
+            )
+
+    @mock.patch.object(redfish_boot, 'swift', autospec=True)
+    @mock.patch.object(images, 'create_boot_iso', autospec=True)
+    def test__prepare_iso_image_uefi(self, mock_create_boot_iso, mock_swift):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            task.node.instance_info.update(deploy_boot_mode='uefi')
+
+            mock_swift_api = mock_swift.SwiftAPI.return_value
+            mock_swift_api.get_temp_url.return_value = 'https://a.b/c.f?e=f'
+
+            url = task.driver.boot._prepare_iso_image(
+                task, 'http://kernel/img', 'http://ramdisk/img',
+                'http://bootloader/img', root_uuid=task.node.uuid)
+
+            self.assertTrue(url)
+
+            mock_create_boot_iso.assert_called_once_with(
+                mock.ANY, mock.ANY, 'http://kernel/img', 'http://ramdisk/img',
+                boot_mode='uefi', esp_image_href='http://bootloader/img',
+                kernel_params='nofb nomodeset vga=normal',
+                root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123')
+
+            mock_swift.SwiftAPI.assert_called_once_with()
+
+            mock_swift_api.create_object.assert_called_once_with(
+                mock.ANY, mock.ANY, mock.ANY, mock.ANY)
+
+            mock_swift_api.get_temp_url.assert_called_once_with(
+                mock.ANY, mock.ANY, mock.ANY)
+
+    @mock.patch.object(redfish_boot, 'swift', autospec=True)
+    @mock.patch.object(images, 'create_boot_iso', autospec=True)
+    def test__prepare_iso_image_bios(self, mock_create_boot_iso, mock_swift):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+
+            mock_swift_api = mock_swift.SwiftAPI.return_value
+            mock_swift_api.get_temp_url.return_value = 'https://a.b/c.f?e=f'
+
+            url = task.driver.boot._prepare_iso_image(
+                task, 'http://kernel/img', 'http://ramdisk/img',
+                bootloader_href=None, root_uuid=task.node.uuid)
+
+            self.assertTrue(url)
+
+            mock_create_boot_iso.assert_called_once_with(
+                mock.ANY, mock.ANY, 'http://kernel/img', 'http://ramdisk/img',
+                boot_mode=None, esp_image_href=None,
+                kernel_params='nofb nomodeset vga=normal',
+                root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123')
+
+            mock_swift.SwiftAPI.assert_called_once_with()
+
+            mock_swift_api.create_object.assert_called_once_with(
+                mock.ANY, mock.ANY, mock.ANY, mock.ANY)
+
+            mock_swift_api.get_temp_url.assert_called_once_with(
+                mock.ANY, mock.ANY, mock.ANY)
+
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_prepare_iso_image', autospec=True)
+    def test__prepare_deploy_iso(self, mock__prepare_iso_image):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+
+            task.node.driver_info.update(
+                {'deploy_kernel': 'kernel',
+                 'deploy_ramdisk': 'ramdisk',
+                 'bootloader': 'bootloader'}
+            )
+
+            task.node.instance_info.update(deploy_boot_mode='uefi')
+
+            task.driver.boot._prepare_deploy_iso(task, {}, 'deploy')
+
+            mock__prepare_iso_image.assert_called_once_with(
+                mock.ANY, 'kernel', 'ramdisk', 'bootloader', params={})
+
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_prepare_iso_image', autospec=True)
+    @mock.patch.object(images, 'create_boot_iso', autospec=True)
+    def test__prepare_boot_iso(self, mock_create_boot_iso,
+                               mock__prepare_iso_image):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            task.node.driver_info.update(
+                {'deploy_kernel': 'kernel',
+                 'deploy_ramdisk': 'ramdisk',
+                 'bootloader': 'bootloader'}
+            )
+
+            task.node.instance_info.update(
+                {'image_source': 'http://boot/iso',
+                 'kernel': 'http://kernel/img',
+                 'ramdisk': 'http://ramdisk/img'})
+
+            task.driver.boot._prepare_boot_iso(
+                task, root_uuid=task.node.uuid)
+
+            mock__prepare_iso_image.assert_called_once_with(
+                mock.ANY, 'http://kernel/img', 'http://ramdisk/img',
+                'bootloader', root_uuid=task.node.uuid)
+
+    @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True)
+    @mock.patch.object(deploy_utils, 'validate_image_properties',
+                       autospec=True)
+    @mock.patch.object(boot_mode_utils, 'get_boot_mode_for_deploy',
+                       autospec=True)
+    def test_validate_uefi_boot(self, mock_get_boot_mode,
+                                mock_validate_image_properties,
+                                mock_parse_driver_info):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            task.node.instance_info.update(
+                {'kernel': 'kernel',
+                 'ramdisk': 'ramdisk',
+                 'image_source': 'http://image/source'}
+            )
+
+            task.node.driver_info.update(
+                {'deploy_kernel': 'kernel',
+                 'deploy_ramdisk': 'ramdisk',
+                 'bootloader': 'bootloader'}
+            )
+
+            mock_get_boot_mode.return_value = 'uefi'
+
+            task.driver.boot.validate(task)
+
+            mock_validate_image_properties.assert_called_once_with(
+                mock.ANY, mock.ANY, mock.ANY)
+
+    @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True)
+    @mock.patch.object(deploy_utils, 'validate_image_properties',
+                       autospec=True)
+    @mock.patch.object(boot_mode_utils, 'get_boot_mode_for_deploy',
+                       autospec=True)
+    def test_validate_bios_boot(self, mock_get_boot_mode,
+                                mock_validate_image_properties,
+                                mock_parse_driver_info):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            task.node.instance_info.update(
+                {'kernel': 'kernel',
+                 'ramdisk': 'ramdisk',
+                 'image_source': 'http://image/source'}
+            )
+
+            task.node.driver_info.update(
+                {'deploy_kernel': 'kernel',
+                 'deploy_ramdisk': 'ramdisk',
+                 'bootloader': 'bootloader'}
+            )
+
+            mock_get_boot_mode.return_value = 'bios'
+
+            task.driver.boot.validate(task)
+
+            mock_validate_image_properties.assert_called_once_with(
+                mock.ANY, mock.ANY, mock.ANY)
+
+    @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True)
+    @mock.patch.object(deploy_utils, 'validate_image_properties',
+                       autospec=True)
+    def test_validate_missing(self, mock_validate_image_properties,
+                              mock_parse_driver_info):
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            self.assertRaises(exception.MissingParameterValue,
+                              task.driver.boot.validate, task)
+
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_prepare_deploy_iso', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_eject_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_insert_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_parse_driver_info', autospec=True)
+    @mock.patch.object(redfish_boot, 'manager_utils', autospec=True)
+    @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True)
+    def test_prepare_ramdisk_with_params(
+            self, mock_boot_mode_utils, mock_manager_utils,
+            mock__parse_driver_info, mock__insert_vmedia, mock__eject_vmedia,
+            mock__prepare_deploy_iso):
+
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            task.node.provision_state = states.DEPLOYING
+
+            mock__parse_driver_info.return_value = {}
+            mock__prepare_deploy_iso.return_value = 'image-url'
+
+            task.driver.boot.prepare_ramdisk(task, {})
+
+            mock_manager_utils.node_power_action.assert_called_once_with(
+                task, states.POWER_OFF)
+
+            mock__eject_vmedia.assert_called_once_with(
+                task, sushy.VIRTUAL_MEDIA_CD)
+
+            mock__insert_vmedia.assert_called_once_with(
+                task, 'image-url', sushy.VIRTUAL_MEDIA_CD)
+
+            expected_params = {
+                'BOOTIF': None,
+            }
+
+            mock__prepare_deploy_iso.assert_called_once_with(
+                task, expected_params, 'deploy')
+
+            mock_manager_utils.node_set_boot_device.assert_called_once_with(
+                task, boot_devices.CDROM)
+
+            mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task)
+
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_prepare_floppy_image', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_prepare_deploy_iso', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_has_vmedia_device', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_eject_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_insert_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_parse_driver_info', autospec=True)
+    @mock.patch.object(redfish_boot, 'manager_utils', autospec=True)
+    @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True)
+    def test_prepare_ramdisk_with_floppy(
+            self, mock_boot_mode_utils, mock_manager_utils,
+            mock__parse_driver_info, mock__insert_vmedia, mock__eject_vmedia,
+            mock__has_vmedia_device, mock__prepare_deploy_iso,
+            mock__prepare_floppy_image):
+
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            task.node.provision_state = states.DEPLOYING
+
+            mock__parse_driver_info.return_value = {
+                'config_via_floppy': True
+            }
+
+            mock__has_vmedia_device.return_value = True
+            mock__prepare_floppy_image.return_value = 'floppy-image-url'
+            mock__prepare_deploy_iso.return_value = 'cd-image-url'
+
+            task.driver.boot.prepare_ramdisk(task, {})
+
+            mock_manager_utils.node_power_action.assert_called_once_with(
+                task, states.POWER_OFF)
+
+            mock__has_vmedia_device.assert_called_once_with(
+                task, sushy.VIRTUAL_MEDIA_FLOPPY)
+
+            eject_calls = [
+                mock.call(task, sushy.VIRTUAL_MEDIA_FLOPPY),
+                mock.call(task, sushy.VIRTUAL_MEDIA_CD)
+            ]
+
+            mock__eject_vmedia.assert_has_calls(eject_calls)
+
+            insert_calls = [
+                mock.call(task, 'floppy-image-url',
+                          sushy.VIRTUAL_MEDIA_FLOPPY),
+                mock.call(task, 'cd-image-url',
+                          sushy.VIRTUAL_MEDIA_CD),
+            ]
+
+            mock__insert_vmedia.assert_has_calls(insert_calls)
+
+            expected_params = {
+                'BOOTIF': None,
+                'boot_method': 'vmedia',
+            }
+
+            mock__prepare_deploy_iso.assert_called_once_with(
+                task, expected_params, 'deploy')
+
+            mock_manager_utils.node_set_boot_device.assert_called_once_with(
+                task, boot_devices.CDROM)
+
+            mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task)
+
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_has_vmedia_device', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_eject_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_cleanup_iso_image', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_cleanup_floppy_image', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_parse_driver_info', autospec=True)
+    def test_clean_up_ramdisk(
+            self, mock__parse_driver_info, mock__cleanup_floppy_image,
+            mock__cleanup_iso_image, mock__eject_vmedia,
+            mock__has_vmedia_device):
+
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            task.node.provision_state = states.DEPLOYING
+
+            mock__parse_driver_info.return_value = {'config_via_floppy': True}
+            mock__has_vmedia_device.return_value = True
+
+            task.driver.boot.clean_up_ramdisk(task)
+
+            mock__cleanup_iso_image.assert_called_once_with(task)
+
+            mock__cleanup_floppy_image.assert_called_once_with(task)
+
+            mock__has_vmedia_device.assert_called_once_with(
+                task, sushy.VIRTUAL_MEDIA_FLOPPY)
+
+            eject_calls = [
+                mock.call(task, sushy.VIRTUAL_MEDIA_CD),
+                mock.call(task, sushy.VIRTUAL_MEDIA_FLOPPY)
+            ]
+
+            mock__eject_vmedia.assert_has_calls(eject_calls)
+
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       'clean_up_instance', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_prepare_boot_iso', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_eject_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_insert_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_parse_driver_info', autospec=True)
+    @mock.patch.object(redfish_boot, 'manager_utils', autospec=True)
+    @mock.patch.object(redfish_boot, 'deploy_utils', autospec=True)
+    @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True)
+    def test_prepare_instance_normal_boot(
+            self, mock_boot_mode_utils, mock_deploy_utils, mock_manager_utils,
+            mock__parse_driver_info, mock__insert_vmedia, mock__eject_vmedia,
+            mock__prepare_boot_iso, mock_clean_up_instance):
+
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            task.node.provision_state = states.DEPLOYING
+            task.node.driver_internal_info[
+                'root_uuid_or_disk_id'] = self.node.uuid
+
+            mock_deploy_utils.get_boot_option.return_value = 'net'
+
+            mock__parse_driver_info.return_value = {}
+            mock__prepare_boot_iso.return_value = 'image-url'
+
+            task.driver.boot.prepare_instance(task)
+
+            expected_params = {
+                'root_uuid': self.node.uuid
+            }
+
+            mock__prepare_boot_iso.assert_called_once_with(
+                task, **expected_params)
+
+            mock__eject_vmedia.assert_called_once_with(
+                task, sushy.VIRTUAL_MEDIA_CD)
+
+            mock__insert_vmedia.assert_called_once_with(
+                task, 'image-url', sushy.VIRTUAL_MEDIA_CD)
+
+            mock_manager_utils.node_set_boot_device.assert_called_once_with(
+                task, boot_devices.CDROM, persistent=True)
+
+            mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task)
+
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       'clean_up_instance', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_prepare_boot_iso', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_eject_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_insert_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_parse_driver_info', autospec=True)
+    @mock.patch.object(redfish_boot, 'manager_utils', autospec=True)
+    @mock.patch.object(redfish_boot, 'deploy_utils', autospec=True)
+    @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True)
+    def test_prepare_instance_ramdisk_boot(
+            self, mock_boot_mode_utils, mock_deploy_utils, mock_manager_utils,
+            mock__parse_driver_info, mock__insert_vmedia, mock__eject_vmedia,
+            mock__prepare_boot_iso, mock_clean_up_instance):
+
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            task.node.provision_state = states.DEPLOYING
+            task.node.driver_internal_info[
+                'root_uuid_or_disk_id'] = self.node.uuid
+
+            mock_deploy_utils.get_boot_option.return_value = 'ramdisk'
+
+            mock__prepare_boot_iso.return_value = 'image-url'
+
+            task.driver.boot.prepare_instance(task)
+
+            mock__prepare_boot_iso.assert_called_once_with(task)
+
+            mock__eject_vmedia.assert_called_once_with(
+                task, sushy.VIRTUAL_MEDIA_CD)
+
+            mock__insert_vmedia.assert_called_once_with(
+                task, 'image-url', sushy.VIRTUAL_MEDIA_CD)
+
+            mock_manager_utils.node_set_boot_device.assert_called_once_with(
+                task, boot_devices.CDROM, persistent=True)
+
+            mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task)
+
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_eject_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_cleanup_iso_image', autospec=True)
+    @mock.patch.object(redfish_boot, 'manager_utils', autospec=True)
+    def _test_prepare_instance_local_boot(
+            self, mock_manager_utils,
+            mock__cleanup_iso_image, mock__eject_vmedia):
+
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            task.node.provision_state = states.DEPLOYING
+            task.node.driver_internal_info[
+                'root_uuid_or_disk_id'] = self.node.uuid
+
+            task.driver.boot.prepare_instance(task)
+
+            mock_manager_utils.node_set_boot_device.assert_called_once_with(
+                task, boot_devices.DISK, persistent=True)
+            mock__cleanup_iso_image.assert_called_once_with(task)
+            mock__eject_vmedia.assert_called_once_with(
+                task, sushy.VIRTUAL_MEDIA_CD)
+
+    def test_prepare_instance_local_whole_disk_image(self):
+        self.node.driver_internal_info = {'is_whole_disk_image': True}
+        self.node.save()
+        self._test_prepare_instance_local_boot()
+
+    def test_prepare_instance_local_boot_option(self):
+        instance_info = self.node.instance_info
+        instance_info['capabilities'] = '{"boot_option": "local"}'
+        self.node.instance_info = instance_info
+        self.node.save()
+        self._test_prepare_instance_local_boot()
+
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_eject_vmedia', autospec=True)
+    @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
+                       '_cleanup_iso_image', autospec=True)
+    def _test_clean_up_instance(self, mock__cleanup_iso_image,
+                                mock__eject_vmedia):
+
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+
+            task.driver.boot.clean_up_instance(task)
+
+            mock__cleanup_iso_image.assert_called_once_with(task)
+            eject_calls = [mock.call(task, sushy.VIRTUAL_MEDIA_CD)]
+            if task.node.driver_info.get('config_via_floppy'):
+                eject_calls.append(mock.call(task, sushy.VIRTUAL_MEDIA_FLOPPY))
+
+            mock__eject_vmedia.assert_has_calls(eject_calls)
+
+    def test_clean_up_instance_only_cdrom(self):
+        self._test_clean_up_instance()
+
+    def test_clean_up_instance_cdrom_and_floppy(self):
+        driver_info = self.node.driver_info
+        driver_info['config_via_floppy'] = True
+        self.node.driver_info = driver_info
+        self.node.save()
+        self._test_clean_up_instance()
+
+    @mock.patch.object(redfish_boot, 'redfish_utils', autospec=True)
+    def test__insert_vmedia_anew(self, mock_redfish_utils):
+
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            mock_vmedia_cd = mock.MagicMock(
+                inserted=False,
+                media_types=[sushy.VIRTUAL_MEDIA_CD])
+            mock_vmedia_floppy = mock.MagicMock(
+                inserted=False,
+                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.boot._insert_vmedia(
+                task, 'img-url', sushy.VIRTUAL_MEDIA_CD)
+
+            mock_vmedia_cd.insert_media.assert_called_once_with(
+                'img-url', inserted=True, write_protected=True)
+
+            self.assertFalse(mock_vmedia_floppy.insert_media.call_count)
+
+    @mock.patch.object(redfish_boot, 'redfish_utils', autospec=True)
+    def test__insert_vmedia_already_inserted(self, mock_redfish_utils):
+
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            mock_vmedia_cd = mock.MagicMock(
+                inserted=True,
+                image='img-url',
+                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]
+
+            task.driver.boot._insert_vmedia(
+                task, 'img-url', sushy.VIRTUAL_MEDIA_CD)
+
+            self.assertFalse(mock_vmedia_cd.insert_media.call_count)
+
+    @mock.patch.object(redfish_boot, 'redfish_utils', autospec=True)
+    def test__insert_vmedia_bad_device(self, mock_redfish_utils):
+
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            mock_vmedia_floppy = mock.MagicMock(
+                inserted=False,
+                media_types=[sushy.VIRTUAL_MEDIA_FLOPPY])
+            mock_manager = mock.MagicMock()
+
+            mock_manager.virtual_media.get_members.return_value = [
+                mock_vmedia_floppy]
+
+            mock_redfish_utils.get_system.return_value.managers = [
+                mock_manager]
+
+            self.assertRaises(
+                exception.InvalidParameterValue,
+                task.driver.boot._insert_vmedia,
+                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):
+
+        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.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):
+
+        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.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):
+
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            mock_vmedia_cd = mock.MagicMock(
+                inserted=False,
+                media_types=[sushy.VIRTUAL_MEDIA_CD])
+            mock_vmedia_floppy = mock.MagicMock(
+                inserted=False,
+                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.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):
+
+        with task_manager.acquire(self.context, self.node.uuid,
+                                  shared=True) as task:
+            mock_vmedia_cd = mock.MagicMock(
+                inserted=False,
+                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]
+
+            task.driver.boot._eject_vmedia(task)
+
+            self.assertFalse(mock_vmedia_cd.eject_media.call_count)
diff --git a/ironic/tests/unit/drivers/modules/redfish/test_inspect.py b/ironic/tests/unit/drivers/modules/redfish/test_inspect.py
index c5aef3d672..776a68bae3 100644
--- a/ironic/tests/unit/drivers/modules/redfish/test_inspect.py
+++ b/ironic/tests/unit/drivers/modules/redfish/test_inspect.py
@@ -40,6 +40,7 @@ class RedfishInspectTestCase(db_base.DbTestCase):
         super(RedfishInspectTestCase, 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'])
         self.node = obj_utils.create_test_node(
diff --git a/ironic/tests/unit/drivers/modules/redfish/test_management.py b/ironic/tests/unit/drivers/modules/redfish/test_management.py
index 1a057ec286..a4664f08f4 100644
--- a/ironic/tests/unit/drivers/modules/redfish/test_management.py
+++ b/ironic/tests/unit/drivers/modules/redfish/test_management.py
@@ -37,6 +37,7 @@ class RedfishManagementTestCase(db_base.DbTestCase):
         super(RedfishManagementTestCase, 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'])
diff --git a/ironic/tests/unit/drivers/modules/redfish/test_power.py b/ironic/tests/unit/drivers/modules/redfish/test_power.py
index 96903328e8..b84833acfd 100644
--- a/ironic/tests/unit/drivers/modules/redfish/test_power.py
+++ b/ironic/tests/unit/drivers/modules/redfish/test_power.py
@@ -37,6 +37,7 @@ class RedfishPowerTestCase(db_base.DbTestCase):
         super(RedfishPowerTestCase, 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'])
diff --git a/ironic/tests/unit/drivers/modules/redfish/test_utils.py b/ironic/tests/unit/drivers/modules/redfish/test_utils.py
index 9614c17405..ef56d96c77 100644
--- a/ironic/tests/unit/drivers/modules/redfish/test_utils.py
+++ b/ironic/tests/unit/drivers/modules/redfish/test_utils.py
@@ -40,6 +40,7 @@ class RedfishUtilsTestCase(db_base.DbTestCase):
         # Default configurations
         self.config(enabled_hardware_types=['redfish'],
                     enabled_power_interfaces=['redfish'],
+                    enabled_boot_interfaces=['redfish-virtual-media'],
                     enabled_management_interfaces=['redfish'])
         # Redfish specific configurations
         self.config(connection_attempts=1, group='redfish')
diff --git a/ironic/tests/unit/drivers/test_redfish.py b/ironic/tests/unit/drivers/test_redfish.py
index 1b4e44585a..f2675b5c6d 100644
--- a/ironic/tests/unit/drivers/test_redfish.py
+++ b/ironic/tests/unit/drivers/test_redfish.py
@@ -16,7 +16,7 @@
 from ironic.conductor import task_manager
 from ironic.drivers.modules import iscsi_deploy
 from ironic.drivers.modules import noop
-from ironic.drivers.modules import pxe
+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
@@ -30,6 +30,7 @@ class RedfishHardwareTestCase(db_base.DbTestCase):
         super(RedfishHardwareTestCase, 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'])
@@ -43,7 +44,8 @@ class RedfishHardwareTestCase(db_base.DbTestCase):
                                   redfish_mgmt.RedfishManagement)
             self.assertIsInstance(task.driver.power,
                                   redfish_power.RedfishPower)
-            self.assertIsInstance(task.driver.boot, pxe.PXEBoot)
+            self.assertIsInstance(task.driver.boot,
+                                  redfish_boot.RedfishVirtualMediaBoot)
             self.assertIsInstance(task.driver.deploy, iscsi_deploy.ISCSIDeploy)
             self.assertIsInstance(task.driver.console, noop.NoConsole)
             self.assertIsInstance(task.driver.raid, noop.NoRAID)
diff --git a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py
index db8adc5296..a895730944 100644
--- a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py
+++ b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py
@@ -136,6 +136,8 @@ SUSHY_SPEC = (
     'STATE_ENABLED',
     'STATE_DISABLED',
     'STATE_ABSENT',
+    'VIRTUAL_MEDIA_CD',
+    'VIRTUAL_MEDIA_FLOPPY',
 )
 
 SUSHY_AUTH_SPEC = (
diff --git a/ironic/tests/unit/drivers/third_party_driver_mocks.py b/ironic/tests/unit/drivers/third_party_driver_mocks.py
index 9c351b8a72..53a2b17ae4 100644
--- a/ironic/tests/unit/drivers/third_party_driver_mocks.py
+++ b/ironic/tests/unit/drivers/third_party_driver_mocks.py
@@ -199,6 +199,8 @@ if not sushy:
         STATE_ENABLED='enabled',
         STATE_DISABLED='disabled',
         STATE_ABSENT='absent',
+        VIRTUAL_MEDIA_CD='cd',
+        VIRTUAL_MEDIA_FLOPPY='floppy',
     )
 
     sys.modules['sushy'] = sushy
diff --git a/releasenotes/notes/add-redfish-boot-interface-e7e05bdd2c894d80.yaml b/releasenotes/notes/add-redfish-boot-interface-e7e05bdd2c894d80.yaml
new file mode 100644
index 0000000000..eb1f1388dd
--- /dev/null
+++ b/releasenotes/notes/add-redfish-boot-interface-e7e05bdd2c894d80.yaml
@@ -0,0 +1,10 @@
+---
+features:
+  - |
+    Adds virtual media boot interface to ``redfish`` hardware type supporting
+    virtual media boot. The ``redfish-virtual-media`` boot interface operates
+    on the same kernel/ramdisk as, for example, PXE boot interface does, however
+    ``redfish-virtual-media`` boot interface can additionally require EFI
+    system partition image (ESP) when performing UEFI boot. New configuration
+    option ``bootloader`` or ``[driver_info]/bootloader`` property can be used
+    to convey ESP location to ironic.
diff --git a/setup.cfg b/setup.cfg
index dfae78963b..5ce50f7173 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -69,6 +69,7 @@ ironic.hardware.interfaces.boot =
     irmc-pxe = ironic.drivers.modules.irmc.boot:IRMCPXEBoot
     irmc-virtual-media = ironic.drivers.modules.irmc.boot:IRMCVirtualMediaBoot
     pxe = ironic.drivers.modules.pxe:PXEBoot
+    redfish-virtual-media = ironic.drivers.modules.redfish.boot:RedfishVirtualMediaBoot
 
 ironic.hardware.interfaces.console =
     fake = ironic.drivers.modules.fake:FakeConsole