Serve virtual media boot images from ironic conductor
Add the option to ironic to place bootable ISO images into local HTTP server's document root so that BMC could boot from there. This is an alternative to Swift-based boot controllable via the ``[redfish]use_swift`` ironic configuration option. Change-Id: I390fbb3a27f1b7d0170caff84f54d4ca40e6bcb2 Story: 2006569 Task: 36674
This commit is contained in:
parent
aba282a7fc
commit
199a218f5c
@ -44,13 +44,22 @@ opts = [
|
||||
'fall back to basic HTTP authentication'))],
|
||||
default='auto',
|
||||
help=_('Redfish HTTP client authentication method.')),
|
||||
cfg.BoolOpt('use_swift',
|
||||
default=True,
|
||||
help=_('Upload generated ISO images for virtual media boot to '
|
||||
'Swift, then pass temporary URL to BMC for booting the '
|
||||
'node. If set to false, images are are placed on the '
|
||||
'ironic-conductor node and served over its '
|
||||
'local HTTP server.')),
|
||||
cfg.StrOpt('swift_container',
|
||||
default='ironic_redfish_container',
|
||||
help=_('The Swift container to store Redfish driver data.')),
|
||||
help=_('The Swift container to store Redfish driver data. '
|
||||
'Applies only when `use_swift` is enabled.')),
|
||||
cfg.IntOpt('swift_object_expiry_timeout',
|
||||
default=900,
|
||||
help=_('Amount of time in seconds for Swift objects to '
|
||||
'auto-expire.')),
|
||||
'auto-expire. Applies only when `use_swift` is '
|
||||
'enabled.')),
|
||||
cfg.StrOpt('kernel_append_params',
|
||||
default='nofb nomodeset vga=normal',
|
||||
help=_('Additional kernel parameters for baremetal '
|
||||
|
@ -12,8 +12,12 @@
|
||||
# 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 os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from ironic_lib import utils as ironic_utils
|
||||
from oslo_log import log
|
||||
from oslo_utils import importutils
|
||||
from six.moves.urllib import parse as urlparse
|
||||
@ -103,6 +107,8 @@ class RedfishVirtualMediaBoot(base.BootInterface):
|
||||
`[instance_info]image_source` node property.
|
||||
"""
|
||||
|
||||
IMAGE_SUBDIR = 'redfish'
|
||||
|
||||
capabilities = ['iscsi_volume_boot', 'ramdisk_boot']
|
||||
|
||||
def __init__(self):
|
||||
@ -200,25 +206,6 @@ class RedfishVirtualMediaBoot(base.BootInterface):
|
||||
|
||||
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 _append_filename_param(url, filename):
|
||||
"""Append 'filename=<file>' parameter to given URL.
|
||||
@ -249,6 +236,94 @@ class RedfishVirtualMediaBoot(base.BootInterface):
|
||||
|
||||
return urlparse.urlunparse(parsed_url)
|
||||
|
||||
@classmethod
|
||||
def _publish_image(cls, image_file, object_name):
|
||||
"""Make image file downloadable.
|
||||
|
||||
Depending on ironic settings, pushes given file into Swift or copies
|
||||
it over to local HTTP server's document root and returns publicly
|
||||
accessible URL leading to the given file.
|
||||
|
||||
:param image_file: path to file to publish
|
||||
:param object_name: name of the published file
|
||||
:return: a URL to download published file
|
||||
"""
|
||||
|
||||
if CONF.redfish.use_swift:
|
||||
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, object_name, image_file,
|
||||
object_headers=object_headers)
|
||||
|
||||
image_url = swift_api.get_temp_url(container, object_name, timeout)
|
||||
|
||||
else:
|
||||
public_dir = os.path.join(CONF.deploy.http_root, cls.IMAGE_SUBDIR)
|
||||
|
||||
if not os.path.exists(public_dir):
|
||||
os.mkdir(public_dir, 0x755)
|
||||
|
||||
published_file = os.path.join(public_dir, object_name)
|
||||
|
||||
try:
|
||||
os.link(image_file, published_file)
|
||||
|
||||
except OSError as exc:
|
||||
LOG.debug(
|
||||
"Could not hardlink image file %(image)s to public "
|
||||
"location %(public)s (will copy it over): "
|
||||
"%(error)s", {'image': image_file,
|
||||
'public': published_file,
|
||||
'error': exc})
|
||||
|
||||
shutil.copyfile(image_file, published_file)
|
||||
|
||||
image_url = urlparse.urljoin(
|
||||
CONF.deploy.http_url, cls.IMAGE_SUBDIR, object_name)
|
||||
|
||||
image_url = cls._append_filename_param(
|
||||
image_url, os.path.basename(image_file))
|
||||
|
||||
return image_url
|
||||
|
||||
@classmethod
|
||||
def _unpublish_image(cls, object_name):
|
||||
"""Withdraw the image previously made downloadable.
|
||||
|
||||
Depending on ironic settings, removes previously published file
|
||||
from where it has been published - Swift or local HTTP server's
|
||||
document root.
|
||||
|
||||
:param object_name: name of the published file (optional)
|
||||
"""
|
||||
if CONF.redfish.use_swift:
|
||||
container = CONF.redfish.swift_container
|
||||
|
||||
swift_api = swift.SwiftAPI()
|
||||
|
||||
LOG.debug("Cleaning up image %(name)s from Swift container "
|
||||
"%(container)s", {'name': object_name,
|
||||
'container': container})
|
||||
|
||||
try:
|
||||
swift_api.delete_object(container, object_name)
|
||||
|
||||
except exception.SwiftOperationError as exc:
|
||||
LOG.warning("Failed to clean up image %(image)s. Error: "
|
||||
"%(error)s.", {'image': object_name,
|
||||
'error': exc})
|
||||
|
||||
else:
|
||||
published_file = os.path.join(
|
||||
CONF.deploy.http_root, cls.IMAGE_SUBDIR, object_name)
|
||||
|
||||
ironic_utils.unlink_without_raise(published_file)
|
||||
|
||||
@staticmethod
|
||||
def _get_floppy_image_name(node):
|
||||
"""Returns the floppy image name for a given node.
|
||||
@ -265,8 +340,7 @@ class RedfishVirtualMediaBoot(base.BootInterface):
|
||||
"""
|
||||
floppy_object_name = cls._get_floppy_image_name(task.node)
|
||||
|
||||
cls._delete_from_swift(
|
||||
task, CONF.redfish.swift_container, floppy_object_name)
|
||||
cls._unpublish_image(floppy_object_name)
|
||||
|
||||
@classmethod
|
||||
def _prepare_floppy_image(cls, task, params=None):
|
||||
@ -290,12 +364,6 @@ class RedfishVirtualMediaBoot(base.BootInterface):
|
||||
"""
|
||||
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})
|
||||
|
||||
@ -305,12 +373,7 @@ class RedfishVirtualMediaBoot(base.BootInterface):
|
||||
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)
|
||||
|
||||
image_url = cls._append_filename_param(image_url, 'bootme.img')
|
||||
image_url = cls._publish_image(vfat_image_tmpfile, object_name)
|
||||
|
||||
LOG.debug("Created floppy image %(name)s in Swift for node %(node)s, "
|
||||
"exposed as temporary URL "
|
||||
@ -336,8 +399,7 @@ class RedfishVirtualMediaBoot(base.BootInterface):
|
||||
"""
|
||||
iso_object_name = cls._get_iso_image_name(task.node)
|
||||
|
||||
cls._delete_from_swift(
|
||||
task, CONF.redfish.swift_container, iso_object_name)
|
||||
cls._unpublish_image(iso_object_name)
|
||||
|
||||
@classmethod
|
||||
def _prepare_iso_image(cls, task, kernel_href, ramdisk_href,
|
||||
@ -406,30 +468,14 @@ class RedfishVirtualMediaBoot(base.BootInterface):
|
||||
|
||||
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)
|
||||
|
||||
boot_iso_url = cls._append_filename_param(
|
||||
boot_iso_url, 'bootme.iso')
|
||||
image_url = cls._publish_image(boot_iso_tmp_file, iso_object_name)
|
||||
|
||||
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})
|
||||
'url': image_url})
|
||||
|
||||
return boot_iso_url
|
||||
return image_url
|
||||
|
||||
@classmethod
|
||||
def _prepare_deploy_iso(cls, task, params, mode):
|
||||
|
@ -13,6 +13,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import os
|
||||
|
||||
import mock
|
||||
from oslo_utils import importutils
|
||||
|
||||
@ -200,35 +202,19 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
|
||||
self.assertEqual(expected, res)
|
||||
|
||||
@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):
|
||||
def test__publish_image_swift(self, 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_floppy_image(task)
|
||||
url = task.driver.boot._publish_image('file.iso', 'boot.iso')
|
||||
|
||||
self.assertIn('filename=bootme.img', url)
|
||||
self.assertEqual(
|
||||
'https://a.b/c.f?e=f&filename=file.iso', url)
|
||||
|
||||
mock_swift.SwiftAPI.assert_called_once_with()
|
||||
|
||||
mock_create_vfat_image.assert_called_once_with(
|
||||
mock.ANY, parameters=mock.ANY)
|
||||
|
||||
mock_swift_api.create_object.assert_called_once_with(
|
||||
mock.ANY, mock.ANY, mock.ANY, mock.ANY)
|
||||
|
||||
@ -236,33 +222,143 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
|
||||
mock.ANY, mock.ANY, mock.ANY)
|
||||
|
||||
@mock.patch.object(redfish_boot, 'swift', autospec=True)
|
||||
def test__cleanup_iso_image(self, mock_swift):
|
||||
def test__unpublish_image_swift(self, mock_swift):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
task.driver.boot._cleanup_iso_image(task)
|
||||
object_name = 'image-%s' % task.node.uuid
|
||||
|
||||
task.driver.boot._unpublish_image(object_name)
|
||||
|
||||
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
|
||||
)
|
||||
'ironic_redfish_container', object_name)
|
||||
|
||||
@mock.patch.object(redfish_boot, 'swift', autospec=True)
|
||||
@mock.patch.object(redfish_boot, 'shutil', autospec=True)
|
||||
@mock.patch.object(os, 'link', autospec=True)
|
||||
@mock.patch.object(os, 'mkdir', autospec=True)
|
||||
def test__publish_image_local_link(
|
||||
self, mock_mkdir, mock_link, mock_shutil):
|
||||
self.config(use_swift=False, group='redfish')
|
||||
self.config(http_url='http://localhost', group='deploy')
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
|
||||
url = task.driver.boot._publish_image('file.iso', 'boot.iso')
|
||||
|
||||
self.assertEqual(
|
||||
'http://localhost/redfish?filename=file.iso', url)
|
||||
|
||||
mock_mkdir.assert_called_once_with('/httpboot/redfish', 0x755)
|
||||
mock_link.assert_called_once_with(
|
||||
'file.iso', '/httpboot/redfish/boot.iso')
|
||||
|
||||
@mock.patch.object(redfish_boot, 'shutil', autospec=True)
|
||||
@mock.patch.object(os, 'link', autospec=True)
|
||||
@mock.patch.object(os, 'mkdir', autospec=True)
|
||||
def test__publish_image_local_copy(
|
||||
self, mock_mkdir, mock_link, mock_shutil):
|
||||
self.config(use_swift=False, group='redfish')
|
||||
self.config(http_url='http://localhost', group='deploy')
|
||||
|
||||
mock_link.side_effect = OSError()
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
|
||||
url = task.driver.boot._publish_image('file.iso', 'boot.iso')
|
||||
|
||||
self.assertEqual(
|
||||
'http://localhost/redfish?filename=file.iso', url)
|
||||
|
||||
mock_mkdir.assert_called_once_with('/httpboot/redfish', 0x755)
|
||||
|
||||
mock_shutil.copyfile.assert_called_once_with(
|
||||
'file.iso', '/httpboot/redfish/boot.iso')
|
||||
|
||||
@mock.patch.object(redfish_boot, 'ironic_utils', autospec=True)
|
||||
def test__unpublish_image_local(self, mock_ironic_utils):
|
||||
self.config(use_swift=False, group='redfish')
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
object_name = 'image-%s' % task.node.uuid
|
||||
|
||||
expected_file = '/httpboot/redfish/' + object_name
|
||||
|
||||
task.driver.boot._unpublish_image(object_name)
|
||||
|
||||
mock_ironic_utils.unlink_without_raise.assert_called_once_with(
|
||||
expected_file)
|
||||
|
||||
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
|
||||
'_unpublish_image', autospec=True)
|
||||
def test__cleanup_floppy_image(self, mock_unpublish):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
task.driver.boot._cleanup_floppy_image(task)
|
||||
|
||||
object_name = 'image-%s' % task.node.uuid
|
||||
|
||||
mock_unpublish.assert_called_once_with(object_name)
|
||||
|
||||
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
|
||||
'_publish_image', autospec=True)
|
||||
@mock.patch.object(images, 'create_vfat_image', autospec=True)
|
||||
def test__prepare_floppy_image(
|
||||
self, mock_create_vfat_image, mock__publish_image):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
expected_url = 'https://a.b/c.f?e=f'
|
||||
|
||||
mock__publish_image.return_value = expected_url
|
||||
|
||||
url = task.driver.boot._prepare_floppy_image(task)
|
||||
|
||||
object_name = 'image-%s' % task.node.uuid
|
||||
|
||||
mock__publish_image.assert_called_once_with(
|
||||
mock.ANY, object_name)
|
||||
|
||||
mock_create_vfat_image.assert_called_once_with(
|
||||
mock.ANY, parameters=mock.ANY)
|
||||
|
||||
self.assertEqual(expected_url, url)
|
||||
|
||||
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
|
||||
'_unpublish_image', autospec=True)
|
||||
def test__cleanup_iso_image(self, mock_unpublish):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
task.driver.boot._cleanup_iso_image(task)
|
||||
|
||||
object_name = 'boot-%s' % task.node.uuid
|
||||
|
||||
mock_unpublish.assert_called_once_with(object_name)
|
||||
|
||||
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
|
||||
'_publish_image', autospec=True)
|
||||
@mock.patch.object(images, 'create_boot_iso', autospec=True)
|
||||
def test__prepare_iso_image_uefi(self, mock_create_boot_iso, mock_swift):
|
||||
def test__prepare_iso_image_uefi(
|
||||
self, mock_create_boot_iso, mock__publish_image):
|
||||
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'
|
||||
expected_url = 'https://a.b/c.f?e=f'
|
||||
|
||||
mock__publish_image.return_value = expected_url
|
||||
|
||||
url = task.driver.boot._prepare_iso_image(
|
||||
task, 'http://kernel/img', 'http://ramdisk/img',
|
||||
'http://bootloader/img', root_uuid=task.node.uuid)
|
||||
|
||||
self.assertIn('filename=bootme.iso', url)
|
||||
object_name = 'boot-%s' % task.node.uuid
|
||||
|
||||
mock__publish_image.assert_called_once_with(
|
||||
mock.ANY, object_name)
|
||||
|
||||
mock_create_boot_iso.assert_called_once_with(
|
||||
mock.ANY, mock.ANY, 'http://kernel/img', 'http://ramdisk/img',
|
||||
@ -270,28 +366,28 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
|
||||
kernel_params='nofb nomodeset vga=normal',
|
||||
root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123')
|
||||
|
||||
mock_swift.SwiftAPI.assert_called_once_with()
|
||||
self.assertEqual(expected_url, url)
|
||||
|
||||
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(redfish_boot.RedfishVirtualMediaBoot,
|
||||
'_publish_image', autospec=True)
|
||||
@mock.patch.object(images, 'create_boot_iso', autospec=True)
|
||||
def test__prepare_iso_image_bios(self, mock_create_boot_iso, mock_swift):
|
||||
def test__prepare_iso_image_bios(
|
||||
self, mock_create_boot_iso, mock__publish_image):
|
||||
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'
|
||||
expected_url = 'https://a.b/c.f?e=f'
|
||||
|
||||
mock__publish_image.return_value = expected_url
|
||||
|
||||
url = task.driver.boot._prepare_iso_image(
|
||||
task, 'http://kernel/img', 'http://ramdisk/img',
|
||||
bootloader_href=None, root_uuid=task.node.uuid)
|
||||
|
||||
self.assertIn('filename=bootme.iso', url)
|
||||
object_name = 'boot-%s' % task.node.uuid
|
||||
|
||||
mock__publish_image.assert_called_once_with(
|
||||
mock.ANY, object_name)
|
||||
|
||||
mock_create_boot_iso.assert_called_once_with(
|
||||
mock.ANY, mock.ANY, 'http://kernel/img', 'http://ramdisk/img',
|
||||
@ -299,13 +395,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
|
||||
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)
|
||||
self.assertEqual(expected_url, url)
|
||||
|
||||
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
|
||||
'_prepare_iso_image', autospec=True)
|
||||
|
@ -7,4 +7,7 @@ features:
|
||||
``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.
|
||||
to convey ESP location to ironic. Bootable ISO images can be served to
|
||||
BMC either from Swift or from a HTTP server running on ironic conductor
|
||||
machine. This is controlled by the ``[redfish]use_swift`` ironic
|
||||
configuration option.
|
||||
|
Loading…
Reference in New Issue
Block a user