Merge "Support configdrive when doing ramdisk deploy with redfish-virtual-media"
This commit is contained in:
commit
6af2e2d9d1
@ -269,7 +269,11 @@ of ``ACTIVE``.
|
|||||||
|
|
||||||
This initial interface does not support bootloader configuration
|
This initial interface does not support bootloader configuration
|
||||||
parameter injection, as such the ``[instance_info]/kernel_append_params``
|
parameter injection, as such the ``[instance_info]/kernel_append_params``
|
||||||
setting is ignored. Configuration drives are not supported yet.
|
setting is ignored.
|
||||||
|
|
||||||
|
Configuration drives are supported starting with the Wallaby release
|
||||||
|
for nodes that have a free virtual USB slot. The configuration option
|
||||||
|
``[deploy]configdrive_use_object_store`` must be set to ``False`` for now.
|
||||||
|
|
||||||
Layer 3 or DHCP-less ramdisk booting
|
Layer 3 or DHCP-less ramdisk booting
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -99,7 +99,8 @@ The intended use case is for advanced scientific and ephemeral workloads
|
|||||||
where the step of writing an image to the local storage is not required
|
where the step of writing an image to the local storage is not required
|
||||||
or desired. As such, this interface does come with several caveats:
|
or desired. As such, this interface does come with several caveats:
|
||||||
|
|
||||||
* Configuration drives are not supported.
|
* Configuration drives are not supported with network boot, only with Redfish
|
||||||
|
virtual media.
|
||||||
* Disk image contents are not written to the bare metal node.
|
* Disk image contents are not written to the bare metal node.
|
||||||
* Users and Operators who intend to leverage this interface should
|
* Users and Operators who intend to leverage this interface should
|
||||||
expect to leverage a metadata service, custom ramdisk images, or the
|
expect to leverage a metadata service, custom ramdisk images, or the
|
||||||
|
@ -13,7 +13,9 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import base64
|
||||||
import functools
|
import functools
|
||||||
|
import gzip
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
@ -118,6 +120,23 @@ class ImageHandler(object):
|
|||||||
|
|
||||||
ironic_utils.unlink_without_raise(published_file)
|
ironic_utils.unlink_without_raise(published_file)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def unpublish_image_for_node(cls, node, prefix='', suffix=''):
|
||||||
|
"""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 node: the node for which image was published.
|
||||||
|
:param prefix: object name prefix.
|
||||||
|
:param suffix: object name suffix.
|
||||||
|
"""
|
||||||
|
name = _get_name(node, prefix=prefix, suffix=suffix)
|
||||||
|
cls(node.driver).unpublish_image(name)
|
||||||
|
LOG.debug('Removed image %(name)s for node %(node)s',
|
||||||
|
{'node': node.uuid, 'name': name})
|
||||||
|
|
||||||
def _append_filename_param(self, url, filename):
|
def _append_filename_param(self, url, filename):
|
||||||
"""Append 'filename=<file>' parameter to given URL.
|
"""Append 'filename=<file>' parameter to given URL.
|
||||||
|
|
||||||
@ -205,20 +224,16 @@ class ImageHandler(object):
|
|||||||
return image_url
|
return image_url
|
||||||
|
|
||||||
|
|
||||||
def _get_floppy_image_name(node):
|
def _get_name(node, prefix='', suffix=''):
|
||||||
"""Returns the floppy image name for a given node.
|
"""Get an object name for a given node.
|
||||||
|
|
||||||
:param node: the node for which image name is to be provided.
|
:param node: the node for which image name is to be provided.
|
||||||
"""
|
"""
|
||||||
return "image-%s" % node.uuid
|
if prefix:
|
||||||
|
name = "%s-%s" % (prefix, node.uuid)
|
||||||
|
else:
|
||||||
def _get_iso_image_name(node):
|
name = node.uuid
|
||||||
"""Returns the boot iso image name for a given node.
|
return name + suffix
|
||||||
|
|
||||||
:param node: the node for which image name is to be provided.
|
|
||||||
"""
|
|
||||||
return "boot-%s.iso" % node.uuid
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup_iso_image(task):
|
def cleanup_iso_image(task):
|
||||||
@ -226,10 +241,8 @@ def cleanup_iso_image(task):
|
|||||||
|
|
||||||
:param task: A task from TaskManager.
|
:param task: A task from TaskManager.
|
||||||
"""
|
"""
|
||||||
iso_object_name = _get_iso_image_name(task.node)
|
ImageHandler.unpublish_image_for_node(task.node, prefix='boot',
|
||||||
img_handler = ImageHandler(task.node.driver)
|
suffix='.iso')
|
||||||
|
|
||||||
img_handler.unpublish_image(iso_object_name)
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_floppy_image(task, params=None):
|
def prepare_floppy_image(task, params=None):
|
||||||
@ -251,7 +264,7 @@ def prepare_floppy_image(task, params=None):
|
|||||||
:raises: SwiftOperationError, if any operation with Swift fails.
|
:raises: SwiftOperationError, if any operation with Swift fails.
|
||||||
:returns: image URL for the floppy image.
|
:returns: image URL for the floppy image.
|
||||||
"""
|
"""
|
||||||
object_name = _get_floppy_image_name(task.node)
|
object_name = _get_name(task.node, prefix='image')
|
||||||
|
|
||||||
LOG.debug("Trying to create floppy image for node "
|
LOG.debug("Trying to create floppy image for node "
|
||||||
"%(node)s", {'node': task.node.uuid})
|
"%(node)s", {'node': task.node.uuid})
|
||||||
@ -280,10 +293,79 @@ def cleanup_floppy_image(task):
|
|||||||
|
|
||||||
:param task: an ironic node object.
|
:param task: an ironic node object.
|
||||||
"""
|
"""
|
||||||
floppy_object_name = _get_floppy_image_name(task.node)
|
ImageHandler.unpublish_image_for_node(task.node, prefix='image')
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_configdrive_image(task, content):
|
||||||
|
"""Prepare an image with configdrive.
|
||||||
|
|
||||||
|
:param task: a TaskManager instance containing the node to act on.
|
||||||
|
:param content: Config drive as a base64-encoded string.
|
||||||
|
:raises: ImageCreationFailed, if it failed while creating the image.
|
||||||
|
:raises: SwiftOperationError, if any operation with Swift fails.
|
||||||
|
:returns: image URL for the image.
|
||||||
|
"""
|
||||||
|
# FIXME(dtantsur): download and convert?
|
||||||
|
if '://' in content:
|
||||||
|
raise exception.ImageCreationFailed(
|
||||||
|
_('URLs are not supported for configdrive images yet'))
|
||||||
|
|
||||||
|
with tempfile.TemporaryFile(dir=CONF.tempdir) as comp_tmpfile_obj:
|
||||||
|
comp_tmpfile_obj.write(base64.b64decode(content))
|
||||||
|
comp_tmpfile_obj.seek(0)
|
||||||
|
gz = gzip.GzipFile(fileobj=comp_tmpfile_obj, mode='rb')
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
dir=CONF.tempdir, suffix='.img') as image_tmpfile_obj:
|
||||||
|
shutil.copyfileobj(gz, image_tmpfile_obj)
|
||||||
|
image_tmpfile_obj.flush()
|
||||||
|
return prepare_disk_image(task, image_tmpfile_obj.name,
|
||||||
|
prefix='configdrive')
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_disk_image(task, content, prefix=None):
|
||||||
|
"""Prepare an image with the given content.
|
||||||
|
|
||||||
|
If content is already an HTTP URL, return it unchanged.
|
||||||
|
|
||||||
|
:param task: a TaskManager instance containing the node to act on.
|
||||||
|
:param content: Content as a string with a file name or bytes with
|
||||||
|
contents.
|
||||||
|
:param prefix: Prefix to use for the object name.
|
||||||
|
:raises: ImageCreationFailed, if it failed while creating the image.
|
||||||
|
:raises: SwiftOperationError, if any operation with Swift fails.
|
||||||
|
:returns: image URL for the image.
|
||||||
|
"""
|
||||||
|
object_name = _get_name(task.node, prefix=prefix)
|
||||||
|
|
||||||
|
LOG.debug("Creating a disk image for node %s", task.node.uuid)
|
||||||
|
|
||||||
img_handler = ImageHandler(task.node.driver)
|
img_handler = ImageHandler(task.node.driver)
|
||||||
img_handler.unpublish_image(floppy_object_name)
|
if isinstance(content, str):
|
||||||
|
image_url = img_handler.publish_image(content, object_name)
|
||||||
|
else:
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
dir=CONF.tempdir, suffix='.img') as image_tmpfile_obj:
|
||||||
|
image_tmpfile_obj.write(content)
|
||||||
|
image_tmpfile_obj.flush()
|
||||||
|
|
||||||
|
image_tmpfile = image_tmpfile_obj.name
|
||||||
|
image_url = img_handler.publish_image(image_tmpfile, object_name)
|
||||||
|
|
||||||
|
LOG.debug("Created a disk image %(name)s for node %(node)s, "
|
||||||
|
"exposed as URL %(url)s", {'node': task.node.uuid,
|
||||||
|
'name': object_name,
|
||||||
|
'url': image_url})
|
||||||
|
|
||||||
|
return image_url
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_disk_image(task, prefix=None):
|
||||||
|
"""Deletes the image if it was created for the node.
|
||||||
|
|
||||||
|
:param task: an ironic node object.
|
||||||
|
:param prefix: Prefix to use for the object name.
|
||||||
|
"""
|
||||||
|
ImageHandler.unpublish_image_for_node(task.node, prefix=prefix)
|
||||||
|
|
||||||
|
|
||||||
def _prepare_iso_image(task, kernel_href, ramdisk_href,
|
def _prepare_iso_image(task, kernel_href, ramdisk_href,
|
||||||
@ -366,7 +448,7 @@ def _prepare_iso_image(task, kernel_href, ramdisk_href,
|
|||||||
base_iso=base_iso,
|
base_iso=base_iso,
|
||||||
inject_files=inject_files)
|
inject_files=inject_files)
|
||||||
|
|
||||||
iso_object_name = _get_iso_image_name(task.node)
|
iso_object_name = _get_name(task.node, prefix='boot', suffix='.iso')
|
||||||
|
|
||||||
image_url = img_handler.publish_image(
|
image_url = img_handler.publish_image(
|
||||||
boot_iso_tmp_file, iso_object_name)
|
boot_iso_tmp_file, iso_object_name)
|
||||||
|
@ -532,10 +532,21 @@ class RedfishVirtualMediaBoot(base.BootInterface):
|
|||||||
params.update(root_uuid=root_uuid)
|
params.update(root_uuid=root_uuid)
|
||||||
|
|
||||||
deploy_info = _parse_deploy_info(node)
|
deploy_info = _parse_deploy_info(node)
|
||||||
|
configdrive = node.instance_info.get('configdrive')
|
||||||
iso_ref = image_utils.prepare_boot_iso(task, deploy_info, **params)
|
iso_ref = image_utils.prepare_boot_iso(task, deploy_info, **params)
|
||||||
eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD)
|
eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD)
|
||||||
_insert_vmedia(task, iso_ref, sushy.VIRTUAL_MEDIA_CD)
|
_insert_vmedia(task, iso_ref, sushy.VIRTUAL_MEDIA_CD)
|
||||||
|
|
||||||
|
if configdrive and boot_option == 'ramdisk':
|
||||||
|
eject_vmedia(task, sushy.VIRTUAL_MEDIA_USBSTICK)
|
||||||
|
cd_ref = image_utils.prepare_configdrive_image(task, configdrive)
|
||||||
|
try:
|
||||||
|
_insert_vmedia(task, cd_ref, sushy.VIRTUAL_MEDIA_USBSTICK)
|
||||||
|
except exception.InvalidParameterValue:
|
||||||
|
raise exception.InstanceDeployFailure(
|
||||||
|
_('Cannot attach configdrive for node %s: no suitable '
|
||||||
|
'virtual USB slot has been found') % node.uuid)
|
||||||
|
|
||||||
boot_mode_utils.sync_boot_mode(task)
|
boot_mode_utils.sync_boot_mode(task)
|
||||||
|
|
||||||
self._set_boot_device(task, boot_devices.CDROM, persistent=True)
|
self._set_boot_device(task, boot_devices.CDROM, persistent=True)
|
||||||
@ -562,6 +573,12 @@ class RedfishVirtualMediaBoot(base.BootInterface):
|
|||||||
if config_via_floppy:
|
if config_via_floppy:
|
||||||
eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY)
|
eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY)
|
||||||
|
|
||||||
|
boot_option = deploy_utils.get_boot_option(task.node)
|
||||||
|
if (boot_option == 'ramdisk'
|
||||||
|
and task.node.instance_info.get('configdrive')):
|
||||||
|
eject_vmedia(task, sushy.VIRTUAL_MEDIA_USBSTICK)
|
||||||
|
image_utils.cleanup_disk_image(task, prefix='configdrive')
|
||||||
|
|
||||||
image_utils.cleanup_iso_image(task)
|
image_utils.cleanup_iso_image(task)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -568,6 +568,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
|
|||||||
|
|
||||||
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
|
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
|
||||||
'clean_up_instance', autospec=True)
|
'clean_up_instance', autospec=True)
|
||||||
|
@mock.patch.object(image_utils, 'prepare_configdrive_image', autospec=True)
|
||||||
@mock.patch.object(image_utils, 'prepare_boot_iso', autospec=True)
|
@mock.patch.object(image_utils, 'prepare_boot_iso', autospec=True)
|
||||||
@mock.patch.object(redfish_boot, 'eject_vmedia', autospec=True)
|
@mock.patch.object(redfish_boot, 'eject_vmedia', autospec=True)
|
||||||
@mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True)
|
@mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True)
|
||||||
@ -578,13 +579,14 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
|
|||||||
def test_prepare_instance_ramdisk_boot(
|
def test_prepare_instance_ramdisk_boot(
|
||||||
self, mock_boot_mode_utils, mock_deploy_utils, mock_manager_utils,
|
self, mock_boot_mode_utils, mock_deploy_utils, mock_manager_utils,
|
||||||
mock__parse_deploy_info, mock__insert_vmedia, mock__eject_vmedia,
|
mock__parse_deploy_info, mock__insert_vmedia, mock__eject_vmedia,
|
||||||
mock_prepare_boot_iso, mock_clean_up_instance):
|
mock_prepare_boot_iso, mock_prepare_disk, mock_clean_up_instance):
|
||||||
|
configdrive = 'Y29udGVudA=='
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
shared=True) as task:
|
shared=True) as task:
|
||||||
task.node.provision_state = states.DEPLOYING
|
task.node.provision_state = states.DEPLOYING
|
||||||
task.node.driver_internal_info[
|
task.node.driver_internal_info[
|
||||||
'root_uuid_or_disk_id'] = self.node.uuid
|
'root_uuid_or_disk_id'] = self.node.uuid
|
||||||
|
task.node.instance_info['configdrive'] = configdrive
|
||||||
|
|
||||||
mock_deploy_utils.get_boot_option.return_value = 'ramdisk'
|
mock_deploy_utils.get_boot_option.return_value = 'ramdisk'
|
||||||
|
|
||||||
@ -596,18 +598,24 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
|
|||||||
mock__parse_deploy_info.return_value = d_info
|
mock__parse_deploy_info.return_value = d_info
|
||||||
|
|
||||||
mock_prepare_boot_iso.return_value = 'image-url'
|
mock_prepare_boot_iso.return_value = 'image-url'
|
||||||
|
mock_prepare_disk.return_value = 'cd-url'
|
||||||
|
|
||||||
task.driver.boot.prepare_instance(task)
|
task.driver.boot.prepare_instance(task)
|
||||||
|
|
||||||
mock_clean_up_instance.assert_called_once_with(mock.ANY, task)
|
mock_clean_up_instance.assert_called_once_with(mock.ANY, task)
|
||||||
|
|
||||||
mock_prepare_boot_iso.assert_called_once_with(task, d_info)
|
mock_prepare_boot_iso.assert_called_once_with(task, d_info)
|
||||||
|
mock_prepare_disk.assert_called_once_with(task, configdrive)
|
||||||
|
|
||||||
mock__eject_vmedia.assert_called_once_with(
|
mock__eject_vmedia.assert_has_calls([
|
||||||
task, sushy.VIRTUAL_MEDIA_CD)
|
mock.call(task, sushy.VIRTUAL_MEDIA_CD),
|
||||||
|
mock.call(task, sushy.VIRTUAL_MEDIA_USBSTICK),
|
||||||
|
])
|
||||||
|
|
||||||
mock__insert_vmedia.assert_called_once_with(
|
mock__insert_vmedia.assert_has_calls([
|
||||||
task, 'image-url', sushy.VIRTUAL_MEDIA_CD)
|
mock.call(task, 'image-url', sushy.VIRTUAL_MEDIA_CD),
|
||||||
|
mock.call(task, 'cd-url', sushy.VIRTUAL_MEDIA_USBSTICK),
|
||||||
|
])
|
||||||
|
|
||||||
mock_manager_utils.node_set_boot_device.assert_called_once_with(
|
mock_manager_utils.node_set_boot_device.assert_called_once_with(
|
||||||
task, boot_devices.CDROM, persistent=True)
|
task, boot_devices.CDROM, persistent=True)
|
||||||
@ -633,6 +641,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
|
|||||||
task.node.provision_state = states.DEPLOYING
|
task.node.provision_state = states.DEPLOYING
|
||||||
task.node.driver_internal_info[
|
task.node.driver_internal_info[
|
||||||
'root_uuid_or_disk_id'] = self.node.uuid
|
'root_uuid_or_disk_id'] = self.node.uuid
|
||||||
|
task.node.instance_info['configdrive'] = None
|
||||||
|
|
||||||
mock_deploy_utils.get_boot_option.return_value = 'ramdisk'
|
mock_deploy_utils.get_boot_option.return_value = 'ramdisk'
|
||||||
|
|
||||||
@ -679,6 +688,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
|
|||||||
task.node.provision_state = states.DEPLOYING
|
task.node.provision_state = states.DEPLOYING
|
||||||
i_info = task.node.instance_info
|
i_info = task.node.instance_info
|
||||||
i_info['boot_iso'] = "super-magic"
|
i_info['boot_iso'] = "super-magic"
|
||||||
|
del i_info['configdrive']
|
||||||
task.node.instance_info = i_info
|
task.node.instance_info = i_info
|
||||||
mock_deploy_utils.get_boot_option.return_value = 'ramdisk'
|
mock_deploy_utils.get_boot_option.return_value = 'ramdisk'
|
||||||
mock__parse_deploy_info.return_value = {}
|
mock__parse_deploy_info.return_value = {}
|
||||||
@ -760,6 +770,29 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
|
|||||||
self.node.save()
|
self.node.save()
|
||||||
self._test_clean_up_instance()
|
self._test_clean_up_instance()
|
||||||
|
|
||||||
|
@mock.patch.object(deploy_utils, 'get_boot_option', autospec=True)
|
||||||
|
@mock.patch.object(redfish_boot, 'eject_vmedia', autospec=True)
|
||||||
|
@mock.patch.object(image_utils, 'cleanup_disk_image', autospec=True)
|
||||||
|
@mock.patch.object(image_utils, 'cleanup_iso_image', autospec=True)
|
||||||
|
def test_clean_up_instance_ramdisk(self, mock_cleanup_iso_image,
|
||||||
|
mock_cleanup_disk_image,
|
||||||
|
mock__eject_vmedia,
|
||||||
|
mock_get_boot_option):
|
||||||
|
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
|
shared=True) as task:
|
||||||
|
mock_get_boot_option.return_value = 'ramdisk'
|
||||||
|
|
||||||
|
task.driver.boot.clean_up_instance(task)
|
||||||
|
|
||||||
|
mock_cleanup_iso_image.assert_called_once_with(task)
|
||||||
|
mock_cleanup_disk_image.assert_called_once_with(
|
||||||
|
task, prefix='configdrive')
|
||||||
|
eject_calls = [mock.call(task, sushy.VIRTUAL_MEDIA_CD),
|
||||||
|
mock.call(task, sushy.VIRTUAL_MEDIA_USBSTICK)]
|
||||||
|
|
||||||
|
mock__eject_vmedia.assert_has_calls(eject_calls)
|
||||||
|
|
||||||
@mock.patch.object(redfish_boot, 'redfish_utils', autospec=True)
|
@mock.patch.object(redfish_boot, 'redfish_utils', autospec=True)
|
||||||
def test__insert_vmedia_anew(self, mock_redfish_utils):
|
def test__insert_vmedia_anew(self, mock_redfish_utils):
|
||||||
|
|
||||||
|
@ -217,6 +217,93 @@ class RedfishImageUtilsTestCase(db_base.DbTestCase):
|
|||||||
|
|
||||||
self.assertEqual(expected_url, url)
|
self.assertEqual(expected_url, url)
|
||||||
|
|
||||||
|
@mock.patch.object(image_utils.ImageHandler, 'publish_image',
|
||||||
|
autospec=True)
|
||||||
|
def test_prepare_disk_image(self, 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'
|
||||||
|
expected_object_name = task.node.uuid
|
||||||
|
|
||||||
|
def _publish(img_handler, tmp_file, object_name):
|
||||||
|
self.assertEqual(expected_object_name, object_name)
|
||||||
|
self.assertEqual(b'content', open(tmp_file, 'rb').read())
|
||||||
|
return expected_url
|
||||||
|
|
||||||
|
mock_publish_image.side_effect = _publish
|
||||||
|
|
||||||
|
url = image_utils.prepare_disk_image(task, b'content')
|
||||||
|
|
||||||
|
mock_publish_image.assert_called_once_with(mock.ANY, mock.ANY,
|
||||||
|
expected_object_name)
|
||||||
|
|
||||||
|
self.assertEqual(expected_url, url)
|
||||||
|
|
||||||
|
@mock.patch.object(image_utils.ImageHandler, 'publish_image',
|
||||||
|
autospec=True)
|
||||||
|
def test_prepare_disk_image_prefix(self, 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'
|
||||||
|
expected_object_name = 'configdrive-%s' % task.node.uuid
|
||||||
|
|
||||||
|
def _publish(img_handler, tmp_file, object_name):
|
||||||
|
self.assertEqual(expected_object_name, object_name)
|
||||||
|
self.assertEqual(b'content', open(tmp_file, 'rb').read())
|
||||||
|
return expected_url
|
||||||
|
|
||||||
|
mock_publish_image.side_effect = _publish
|
||||||
|
|
||||||
|
url = image_utils.prepare_disk_image(task, b'content',
|
||||||
|
prefix='configdrive')
|
||||||
|
|
||||||
|
mock_publish_image.assert_called_once_with(mock.ANY, mock.ANY,
|
||||||
|
expected_object_name)
|
||||||
|
|
||||||
|
self.assertEqual(expected_url, url)
|
||||||
|
|
||||||
|
@mock.patch.object(image_utils.ImageHandler, 'publish_image',
|
||||||
|
autospec=True)
|
||||||
|
def test_prepare_disk_image_file(self, 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'
|
||||||
|
expected_object_name = task.node.uuid
|
||||||
|
|
||||||
|
def _publish(img_handler, tmp_file, object_name):
|
||||||
|
self.assertEqual(expected_object_name, object_name)
|
||||||
|
self.assertEqual(b'content', open(tmp_file, 'rb').read())
|
||||||
|
return expected_url
|
||||||
|
|
||||||
|
mock_publish_image.side_effect = _publish
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile() as fp:
|
||||||
|
fp.write(b'content')
|
||||||
|
fp.flush()
|
||||||
|
url = image_utils.prepare_disk_image(task, fp.name)
|
||||||
|
|
||||||
|
mock_publish_image.assert_called_once_with(mock.ANY, mock.ANY,
|
||||||
|
expected_object_name)
|
||||||
|
|
||||||
|
self.assertEqual(expected_url, url)
|
||||||
|
|
||||||
|
@mock.patch.object(image_utils, 'prepare_disk_image', autospec=True)
|
||||||
|
def test_prepare_configdrive_image(self, mock_prepare):
|
||||||
|
expected_url = 'https://a.b/c.f?e=f'
|
||||||
|
encoded = 'H4sIAPJ8418C/0vOzytJzSsBAKkwxf4HAAAA'
|
||||||
|
|
||||||
|
def _prepare(task, content, prefix):
|
||||||
|
with open(content, 'rb') as fp:
|
||||||
|
self.assertEqual(b'content', fp.read())
|
||||||
|
return expected_url
|
||||||
|
|
||||||
|
mock_prepare.side_effect = _prepare
|
||||||
|
|
||||||
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
|
shared=True) as task:
|
||||||
|
result = image_utils.prepare_configdrive_image(task, encoded)
|
||||||
|
self.assertEqual(expected_url, result)
|
||||||
|
|
||||||
@mock.patch.object(image_utils.ImageHandler, 'unpublish_image',
|
@mock.patch.object(image_utils.ImageHandler, 'unpublish_image',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
def test_cleanup_iso_image(self, mock_unpublish):
|
def test_cleanup_iso_image(self, mock_unpublish):
|
||||||
|
@ -155,6 +155,7 @@ SUSHY_SPEC = (
|
|||||||
'STATE_ABSENT',
|
'STATE_ABSENT',
|
||||||
'VIRTUAL_MEDIA_CD',
|
'VIRTUAL_MEDIA_CD',
|
||||||
'VIRTUAL_MEDIA_FLOPPY',
|
'VIRTUAL_MEDIA_FLOPPY',
|
||||||
|
'VIRTUAL_MEDIA_USBSTICK',
|
||||||
'APPLY_TIME_ON_RESET',
|
'APPLY_TIME_ON_RESET',
|
||||||
'TASK_STATE_COMPLETED',
|
'TASK_STATE_COMPLETED',
|
||||||
'HEALTH_OK',
|
'HEALTH_OK',
|
||||||
|
@ -217,6 +217,7 @@ if not sushy:
|
|||||||
STATE_ABSENT='absent',
|
STATE_ABSENT='absent',
|
||||||
VIRTUAL_MEDIA_CD='cd',
|
VIRTUAL_MEDIA_CD='cd',
|
||||||
VIRTUAL_MEDIA_FLOPPY='floppy',
|
VIRTUAL_MEDIA_FLOPPY='floppy',
|
||||||
|
VIRTUAL_MEDIA_USBSTICK='usb',
|
||||||
APPLY_TIME_ON_RESET='on reset',
|
APPLY_TIME_ON_RESET='on reset',
|
||||||
TASK_STATE_COMPLETED='completed',
|
TASK_STATE_COMPLETED='completed',
|
||||||
HEALTH_OK='ok',
|
HEALTH_OK='ok',
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Supports attaching configdrives when doing ``ramdisk`` deploy with the
|
||||||
|
``redfish-virtual-media`` boot. A configdrive is attached to a free USB
|
||||||
|
slot. Swift must not be used for configdrive storage (this limitation will
|
||||||
|
be fixed later).
|
Loading…
x
Reference in New Issue
Block a user