Openstack vmedia - refactor to pre-defined volumes

... I hit the wall with the rebuild from image volume when instances
where UEFI. The rebuild from volume image failed with this exception
when UEFI image properties where populated.
  libvirt.libvirtError: Requested operation is not valid: cannot
                        undefine domain with nvram

This changes the approach, instead of creating and attaching the
volume the Nova instance is pre-confonfigured with two volumes.
One is device_type: `disk` and the other device_type: `cdrom` on
`scsi` bus. The cdrom volume is configured with boot_index: 0, and
the disk with boot_index: 1

The image used for the `cdrom` device initially, is a non-bootable
"blank-image", the result is that the instance fall back to the
`disk` device when booting.

After insert + set_boot_image the server is rebuilt with the inserted
image, this rebuild will only re-image the volume with `boot_index: 0`
and so following reboot the instance will boot of the `cdrom` volume.
After eject + set_boot_image the server is rebuilt using the
"blank-image", following reboot will boot of the `disk` volume.

Change-Id: I16f3fcf79f34b8288e6b8c107fd49bbd7acd4b51
This commit is contained in:
Harald Jensås
2025-02-11 23:02:19 +01:00
parent ddcb838547
commit 8f5b808cf1
4 changed files with 190 additions and 397 deletions

View File

@@ -23,6 +23,10 @@ SUSHY_EMULATOR_OS_CLOUD = None
# import OpenStack cloud virtual media
SUSHY_EMULATOR_OS_VMEDIA_IMAGE_FILE_UPLOAD = False
# Blank non-bootable image used by the Openstack driver virtual media.
# In "ejected" state the cdrom device is rebuilt with this image.
SUSHY_EMULATOR_OS_VMEDIA_BLANK_IMAGE = 'sushy-tools-blank-image'
# The OpenStack cloud ID to use for Ironic. This option enables Ironic driver.
SUSHY_EMULATOR_IRONIC_CLOUD = None

View File

@@ -315,6 +315,75 @@ And flip an instance's power state via the Redfish call:
You can have as many OpenStack instances as you need. The instances can be
concurrently managed over Redfish and functionally similar tools.
Creating Openstack instances for virtual media boot
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When creating Openstack instances for virtual media boot the instances must be
configured to boot from volumes. One volume configured with
``device_type: disk`` and ``boot_index: 1``. A second volume configured with
``device_type: cdrom``, ``disk_bus: scsi`` and ``boot_index: 0``.
The ``cdrom`` volume should initially be created with small (1 Megabyte) "blank"
non-bootable image so that the server boots from the ``disk``. On insert/eject,
this volume will be rebuilt. Following is an example showing how to create a
"blank" image, and upload to glance:
.. code-block:: shell
qemu-img create -f qcow2 blank-image.qcow2 1M
openstack image create --disk-format qcow2 --file blank-image.qcow2 \
--property hw_firmware_type=uefi --property hw_machine_type=q35 \
--property os_shutdown_timeout=5 \
sushy-tools-blank-image
The following is an example show ``block_device_mapping`` that can be used to when
creating an instance using create_server from the Openstack SDK.
.. code-block:: python
block_device_mapping=[
{
'uuid': IMAGE_ID,
'boot_index': 1,
'source_type': 'image',
'destination_type': 'volume',
'device_type': 'disk',
'volume_size': 20,
'delete_on_termination': True,
},
{
'uuid': BLANK_IMG_ID,
'boot_index': 0,
'source_type': 'image',
'destination_type': 'volume',
'device_type': 'cdrom',
'disk_bus': 'scsi',
'volume_size': 5,
'delete_on_termination': True,
}
]
The following is an example Openstack heat template for creating an instance:
.. code-block:: yaml
ironic0:
type: OS::Nova::Server
properties:
flavor: m1.medium
block_device_mapping_v2:
- device_type: disk
boot_index: 1
image_id: glance-image-name
volume_size: 40
delete_on_termination: true
- device_type: cdrom
disk_bus: scsi
boot_index: 0
image_id: sushy-tools-blank-image
volume_size: 5
delete_on_termination: true
Systems resource driver: Ironic
++++++++++++++++++++++++++++++++++
@@ -626,10 +695,8 @@ On insert the OpenStack driver will:
* Upload the image directly to glance from the URL (long running)
* Store the URL, image ID and volume ID in server metadata properties
`sushy-tools-image-url`, `sushy-tools-import-image`, `sushy-tools-volume`
* Create and attach a new volume with the same size as the root disk
* Rebuild the server with the image, replacing the contents of the root disk
* Delete the image
`sushy-tools-image-url`, `sushy-tools-import-image`.
* Rebuild the volume with `boot_index: 0` using the image from Glance.
Redfish client can eject image from virtual media device:
@@ -642,19 +709,22 @@ Redfish client can eject image from virtual media device:
On eject the OpenStack driver will:
* Assume the attached Volume has been rewritten with a new image (an ISO
installer or IPA)
* Detach the Volume
* Create an image from the Volume (long running)
* Store the Volume image ID in server metadata property
`sushy-tools-volume-image`
* Rebuild the server with the new image
* Delete the Volume
* Delete the image
* Look up the imported image from instance metadata `sushy-tools-import-image`.
* Delete the imported image.
* Reset the instance metadata.
* Rebuild the server volume with `boot_index: 0` with a "blank" (non-bootable)
image. The "blank" image used is defined in the configuration using
`SUSHY_EMULATOR_OS_VMEDIA_BLANK_IMAGE` (defaults to: `sushy-tools-blank-image`)
Virtual media boot
++++++++++++++++++
.. note::
With the OpenStack driver the cloud backing the server instances must have
support for rebuilding a volume-backed instance with a different image. This
was introduced in 26.0.0 (Zed), Nova API microversion 2.93.
To boot a system from a virtual media device, the client first needs to figure
out which Manager is responsible for the system of interest:

View File

@@ -106,13 +106,9 @@ class OpenStackDriver(AbstractSystemsDriver):
def _get_instance_image_id(self, instance):
# instance.image.id is always None for boot from volume instance
image_id = instance.image.id
volumes_attached = []
if image_id is None:
volumes_attached = instance['os-extended-volumes:volumes_attached']
if len(volumes_attached) > 0:
vol = self._get_volume_info(volumes_attached[0].id)
if image_id is None and len(instance.attached_volumes) > 0:
vol = self._get_volume_info(instance.attached_volumes[0].id)
image_id = vol.volume_image_metadata.get('image_id')
return image_id
@@ -455,10 +451,13 @@ class OpenStackDriver(AbstractSystemsDriver):
elif boot_image is None:
self._logger.debug(
'Creating task to upload volume and rebuild for %(identity)s' %
'Create task to rebuild with blank-image for %(identity)s' %
{'identity': identity})
# Not running async here, as long as the blank image used is small
# this should finish in ~20 seconds.
self._submit_future(
True, self._rebuild_with_volume_image, identity)
False, self._rebuild_with_blank_image, identity)
else:
self._logger.debug(
'Creating task to finish import and rebuild for %(identity)s' %
@@ -477,6 +476,8 @@ class OpenStackDriver(AbstractSystemsDriver):
parsed_url = urlparse.urlparse(image_url)
local_file = os.path.basename(parsed_url.path)
unique = base64.urlsafe_b64encode(os.urandom(6)).decode('utf-8')
boot_mode = self.get_boot_mode(identity)
image_attrs = {
'name': '%s %s' % (local_file, unique),
'disk_format': 'raw',
@@ -484,12 +485,20 @@ class OpenStackDriver(AbstractSystemsDriver):
'visibility': 'private'
}
server_metadata = {'sushy-tools-image-url': image_url}
if boot_mode == 'UEFI':
self._logger.debug('Setting UEFI image properties for '
'%(identity)s' % {'identity': identity})
image_attrs['properties'] = {
'hw_firmware_type': 'uefi',
'hw_machine_type': 'q35'
}
if local_file_path:
image_attrs['filename'] = local_file_path
server_metadata['sushy-tools-image-local-file'] = local_file_path
image = None
volume = None
try:
# Create image, and begin importing. Waiting for import to
# complete will be part of a long-running operation
@@ -510,23 +519,12 @@ class OpenStackDriver(AbstractSystemsDriver):
self._cc.set_server_metadata(identity, server_metadata)
# Create an empty volume the size of the root disk which will be
# attached during the long-running operation
self._logger.debug(
'Creating volume for %(identity)s' %
{'identity': identity})
server = self._cc.compute.get_server(identity)
volume = self._cc.block_storage.create_volume(
size=server.flavor.disk,
name=server.name)
self._cc.set_server_metadata(
identity, {'sushy-tools-volume': volume.id})
except Exception as ex:
msg = 'Failed insert image from URL %s: %s' % (image_url, ex)
self._logger.exception(msg)
self._attempt_delete_image_volume(
image, volume, local_file_path, identity,
'sushy-tools-import-image', 'sushy-tools-volume',
self._attempt_delete_image_local_file(
image, local_file_path, identity,
'sushy-tools-import-image',
'sushy-tools-image-local-file')
if not isinstance(ex, error.FishyError):
ex = error.FishyError(msg)
@@ -541,74 +539,35 @@ class OpenStackDriver(AbstractSystemsDriver):
self._submit_future(False, self._eject_image, identity)
def _eject_image(self, identity):
image_url = None
try:
# Assume that the inserted image wrote a new image to the volume,
# so convert the volume to an image and rebuild with that image
# to switch
server = self._cc.compute.get_server(identity)
image_id = server.metadata.get('sushy-tools-import-image')
image_url = server.metadata.get('sushy-tools-image-url')
volume_id = server.metadata.get('sushy-tools-volume')
volume = self._cc.block_storage.get_volume(
volume_id)
if volume.status in ('detaching', 'available'):
self._logger.debug(
'Volume %(volume)s already detaching or '
'detached from server %(identity)s' % {
'identity': identity, 'volume': volume})
else:
self._logger.debug(
'Deleting attachment for volume %(volume)s and server '
'%(identity)s' % {'identity': identity, 'volume': volume})
# Delete the attachment so the image can be created from the
# volume
self._cc.compute.delete_volume_attachment(identity, volume)
# sushy-tools-import-image not set in metadata, nothing to do
if image_id is None:
return
self._logger.debug(
'Waiting for volume %(volume)s to be available' %
{'volume': volume})
while volume.status in ('queued', 'detaching', 'in-use'):
time.sleep(1)
volume = self._cc.block_storage.get_volume(volume)
if volume.status != 'available':
raise error.FishyError(
'Volume detachment resulted in status %s' %
volume.status)
image = self._cc.image.find_image(image_id)
image_attrs = {
'volume': volume,
'image_name': volume.name,
'disk_format': 'raw',
'container_format': 'bare',
'visibility': 'private',
}
self._logger.debug(
'Creating image from volume %(volume)s for server '
'%(identity)s' %
{'identity': identity, 'volume': volume})
upload = self._cc.block_storage.upload_volume_to_image(
**image_attrs)
image_id = upload['image_id']
self._cc.set_server_metadata(
identity, {'sushy-tools-volume-image': image_id})
if image is None:
msg = ('Failed ejecting image %s. Image not found in image '
'service.' % image_url)
raise error.FishyError(msg)
self._attempt_delete_image_local_file(
image, None, identity,
'sushy-tools-import-image', 'sushy-tools-image-url')
except Exception as ex:
msg = 'Failed ejecting image %s: %s' % (image_url, ex)
msg = 'Failed eject image from URL %s: %s' % (image_url, ex)
self._logger.exception(msg)
if not isinstance(ex, error.FishyError):
ex = error.FishyError(msg)
raise ex
def _attempt_delete_image_volume(self, image, volume, local_file,
identity, *metadata_keys):
if volume:
try:
self._logger.debug('Deleting volume %(volume)s' %
{'volume': volume})
self._cc.block_storage.delete_volume(volume)
except Exception:
pass
def _attempt_delete_image_local_file(self, image, local_file, identity,
*metadata_keys):
if image:
try:
self._logger.debug('Deleting image %(image)s' %
@@ -655,46 +614,22 @@ class OpenStackDriver(AbstractSystemsDriver):
return future.result()
def _rebuild_with_imported_image(self, identity, image_id):
image_url = None
image = None
image_local_file = None
try:
image = self._cc.image.get_image(image_id)
server = self._cc.compute.get_server(identity)
image_url = server.metadata.get('sushy-tools-image-url')
image_local_file = server.metadata.get(
'sushy-tools-image-local-file')
volume_id = server.metadata.get('sushy-tools-volume')
volume = self._cc.block_storage.get_volume(volume_id)
# Wait for volume to be available
while volume.status == 'creating':
time.sleep(1)
volume = self._cc.block_storage.get_volume(volume)
if volume.status not in 'available':
raise error.FishyError(
'Volume creation resulted in status %s' %
volume.status)
self._logger.debug(
'Attaching volume %(volume)s and server %(identity)s' %
{'identity': identity, 'volume': volume})
self._cc.compute.create_volume_attachment(
identity, volume,
delete_on_termination=True)
while volume.status in ('available', 'reserved', 'attaching'):
time.sleep(1)
volume = self._cc.block_storage.get_volume(volume)
if volume.status not in 'in-use':
raise error.FishyError(
'Volume attachment resulted in status %s' %
volume.status)
# Wait for image to be imported
while image.status in ('queued', 'importing'):
time.sleep(1)
image = self._cc.image.get_image(image)
# Delete the cached local file
if image_local_file:
self._attempt_delete_image_volume(
None, None, image_local_file, identity,
'sushy-tools-image-local-file')
if image.status != 'active':
raise error.FishyError('Image import ended with status %s' %
image.status)
@@ -715,73 +650,52 @@ class OpenStackDriver(AbstractSystemsDriver):
except Exception as ex:
msg = 'Failed insert image from URL %s: %s' % (image_url, ex)
self._logger.exception(msg)
self._attempt_delete_image_volume(
None, volume_id, image_local_file, identity,
'sushy-tools-volume', 'sushy-tools-image-local-file')
self._attempt_delete_image_local_file(
image, image_local_file, identity,
'sushy-tools-import-image', 'sushy-tools-image-local-file')
if not isinstance(ex, error.FishyError):
ex = error.FishyError(msg)
raise ex
finally:
self._attempt_delete_image_volume(
image_id, None, None, identity, 'sushy-tools-image')
self._attempt_delete_image_local_file(
None, image_local_file, identity,
'sushy-tools-image-local-file')
def _rebuild_with_volume_image(self, identity):
def _rebuild_with_blank_image(self, identity):
image_url = None
try:
server = self._cc.compute.get_server(identity)
image_id = server.metadata.get('sushy-tools-volume-image')
image_local_file = server.metadata.get(
'sushy-tools-image-local-file')
volume_id = server.metadata.get('sushy-tools-volume')
image_url = server.metadata.get('sushy-tools-image-url')
blank_image = self._config.get(
'SUSHY_EMULATOR_OS_VMEDIA_BLANK_IMAGE',
'sushy-tools-blank-image')
image = self._cc.image.find_image(blank_image)
if not image_id or not volume_id:
# Nothing to do
return
image = self._cc.image.get_image(image_id)
while image.status in ('queued', 'uploading', 'saving'):
time.sleep(1)
image = self._cc.image.get_image(image)
if image.status != 'active':
raise error.FishyError(
'Image import ended with status %s' % image.status)
if image is None:
msg = ('Failed ejecting image %s, vmedia blank image: %s not '
'found.' % (image_url, blank_image))
raise error.FishyError(msg)
self._logger.debug(
'Rebuilding %(identity)s with image %(image)s' %
{'identity': identity, 'image': image.id})
server = self._cc.compute.rebuild_server(identity, image.id)
while server.status == 'REBUILD':
server = self._cc.compute.get_server(identity)
time.sleep(1)
if server.status not in ('ACTIVE', 'SHUTOFF'):
raise error.FishyError(
'Server rebuild attempt resulted in status %s'
% server.status)
raise error.FishyError('Server rebuild attempt resulted in '
'status %s' % server.status)
self._logger.debug(
'Rebuild %(identity)s complete' % {'identity': identity})
# Wait for the volume to be back into a state which can be deleted
volume = self._cc.block_storage.get_volume(
volume_id)
while volume.status == 'uploading':
time.sleep(1)
volume = self._cc.block_storage.get_volume(volume)
if volume.status != 'available':
raise error.FishyError(
'Volume upload resulted in status %s' % volume.status)
except Exception as ex:
msg = 'Failed ejecting image %s: %s' % (image_url, ex)
self._logger.exception(msg)
if not isinstance(ex, error.FishyError):
ex = error.FishyError(msg)
raise ex
finally:
self._attempt_delete_image_volume(
image_id, volume_id, image_local_file, identity,
'sushy-tools-volume-image', 'sushy-tools-volume',
'sushy-tools-image-local-file')
@staticmethod
def _delete_local_file(local_file):

View File

@@ -367,17 +367,13 @@ class NovaDriverTestCase(base.BaseTestCase):
self.test_driver.set_http_boot_uri,
None)
@mock.patch.object(OpenStackDriver, 'get_boot_mode', autospec=True)
@mock.patch.object(base64, 'urlsafe_b64encode', autospec=True)
def test_insert_image(self, mock_b64e):
def test_insert_image(self, mock_b64e, mock_get_boot_mode):
mock_get_boot_mode.return_value = None
mock_b64e.return_value = b'0hIwh_vN'
mock_server = mock.Mock()
mock_server.flavor.disk = 20
mock_server.name = 'node01'
queued_image = mock.Mock(id='aaa-bbb')
self._cc.image.create_image.return_value = queued_image
self._cc.compute.get_server.return_value = mock_server
self._cc.block_storage.create_volume.return_value = mock.Mock(
id='ccc-ddd')
image_id, image_name = self.test_driver.insert_image(
self.uuid, 'http://fish.it/red.iso')
@@ -385,34 +381,24 @@ class NovaDriverTestCase(base.BaseTestCase):
self._cc.image.create_image.assert_called_once_with(
name='red.iso 0hIwh_vN', disk_format='raw',
container_format='bare', visibility='private')
self._cc.compute.get_server.assert_called_once_with(self.uuid)
self._cc.block_storage.create_volume.assert_called_once_with(
size=20, name='node01')
self._cc.image.import_image.assert_called_once_with(
queued_image, method='web-download', uri='http://fish.it/red.iso')
calls = [
mock.call(
self.uuid,
{'sushy-tools-image-url': 'http://fish.it/red.iso',
'sushy-tools-import-image': 'aaa-bbb'}),
mock.call(self.uuid, {'sushy-tools-volume': 'ccc-ddd'})
]
self._cc.set_server_metadata.assert_has_calls(calls)
self._cc.set_server_metadata.assert_called_once_with(
self.uuid,
{'sushy-tools-image-url': 'http://fish.it/red.iso',
'sushy-tools-import-image': 'aaa-bbb'})
self.assertEqual('aaa-bbb', image_id)
@mock.patch.object(OpenStackDriver, 'get_boot_mode', autospec=True)
@mock.patch.object(base64, 'urlsafe_b64encode', autospec=True)
def test_insert_image_file_upload(self, mock_b64e):
def test_insert_image_file_upload(self, mock_b64e, mock_get_boot_mode):
mock_get_boot_mode.return_value = None
mock_b64e.return_value = b'0hIwh_vN'
mock_server = mock.Mock()
mock_server.flavor.disk = 20
mock_server.name = 'node01'
queued_image = mock.Mock(id='aaa-bbb')
self._cc.image.create_image.return_value = queued_image
self._cc.compute.get_server.return_value = mock_server
self._cc.block_storage.create_volume.return_value = mock.Mock(
id='ccc-ddd')
image_id, image_name = self.test_driver.insert_image(
self.uuid, 'http://fish.it/red.iso', '/alphabet/soup/red.iso')
@@ -421,30 +407,19 @@ class NovaDriverTestCase(base.BaseTestCase):
name='red.iso 0hIwh_vN', disk_format='raw',
container_format='bare', visibility='private',
filename='/alphabet/soup/red.iso')
self._cc.compute.get_server.assert_called_once_with(self.uuid)
self._cc.block_storage.create_volume.assert_called_once_with(
size=20, name='node01')
self._cc.image.import_image.assert_not_called()
calls = [
mock.call(
self.uuid,
{'sushy-tools-image-url': 'http://fish.it/red.iso',
'sushy-tools-image-local-file': '/alphabet/soup/red.iso',
'sushy-tools-import-image': 'aaa-bbb'}),
mock.call(self.uuid, {'sushy-tools-volume': 'ccc-ddd'})
]
self._cc.set_server_metadata.assert_has_calls(calls)
self._cc.set_server_metadata.assert_called_once_with(
self.uuid,
{'sushy-tools-image-url': 'http://fish.it/red.iso',
'sushy-tools-image-local-file': '/alphabet/soup/red.iso',
'sushy-tools-import-image': 'aaa-bbb'})
self.assertEqual('aaa-bbb', image_id)
def test_insert_image_fail(self):
mock_server = mock.Mock()
mock_server.flavor.disk = 20
mock_server.name = 'node01'
@mock.patch.object(OpenStackDriver, 'get_boot_mode', autospec=True)
def test_insert_image_fail(self, mock_get_boot_mode):
mock_get_boot_mode.return_value = None
self._cc.image.create_image.return_value = mock.Mock(id='aaa-bbb')
self._cc.compute.get_server.return_value = mock_server
self._cc.block_storage.create_volume.return_value = mock.Mock(
id='ccc-ddd')
self._cc.image.create_image.side_effect = Exception('ouch')
@@ -455,8 +430,9 @@ class NovaDriverTestCase(base.BaseTestCase):
'Failed insert image from URL http://fish.it/red.iso: ouch',
str(e))
def test_insert_image_future_running(self):
@mock.patch.object(OpenStackDriver, 'get_boot_mode', autospec=True)
def test_insert_image_future_running(self, mock_get_boot_mode):
mock_get_boot_mode.return_value = None
mock_future = mock.Mock()
mock_future.running.return_value = True
self.test_driver._futures[self.uuid] = mock_future
@@ -467,8 +443,9 @@ class NovaDriverTestCase(base.BaseTestCase):
'An insert or eject operation is already in progress for '
'c7a5fdbd-cdaf-9455-926a-d65c16db1809', str(e))
def test_insert_image_future_exception(self):
@mock.patch.object(OpenStackDriver, 'get_boot_mode', autospec=True)
def test_insert_image_future_exception(self, mock_get_boot_mode):
mock_get_boot_mode.return_value = None
mock_future = mock.Mock()
mock_future.running.return_value = False
mock_future.exception.return_value = error.FishyError('ouch')
@@ -478,70 +455,50 @@ class NovaDriverTestCase(base.BaseTestCase):
self.uuid, 'http://fish.it/red.iso')
self.assertEqual('ouch', str(e))
@mock.patch.object(time, 'sleep', autospec=True)
def test_eject_image(self, mock_sleep):
def test_eject_image(self):
mock_server = mock.Mock()
mock_server.name = 'node01'
mock_server.metadata = {
'sushy-tools-image-url': 'http://fish.it/red.iso',
'sushy-tools-volume': 'ccc-ddd'
'sushy-tools-import-image': 'ccc-ddd'
}
self._cc.compute.get_server.return_value = mock_server
mock_image = mock.Mock(id='ccc-ddd')
available_volume = mock.Mock()
available_volume.id = 'ccc-ddd'
available_volume.status = 'available'
available_volume.name = self.uuid
in_use_volume = mock.Mock(id='ccc-ddd', status='in-use')
self._cc.block_storage.get_volume.side_effect = [
in_use_volume,
mock.Mock(id='ccc-ddd', status='queued'),
mock.Mock(id='ccc-ddd', status='detaching'),
available_volume,
mock.Mock(id='ccc-ddd', status='uploading'),
available_volume
]
self._cc.block_storage.upload_volume_to_image.return_value = {
'image_id': 'aaa-bbb'}
self._cc.compute.get_server.return_value = mock_server
self._cc.image.find_image.return_value = mock_image
self.test_driver.eject_image(self.uuid)
self._cc.compute.delete_volume_attachment(self.uuid, in_use_volume)
self._cc.block_storage.upload_volume_to_image.assert_called_once_with(
volume=available_volume, image_name=self.uuid, disk_format='raw',
container_format='bare', visibility='private')
self._cc.compute.get_server.assert_called_once_with(self.uuid)
self._cc.image.find_image.assert_called_once_with('ccc-ddd')
self._cc.delete_image.assert_called_once_with(mock_image)
@mock.patch.object(time, 'sleep', autospec=True)
def test_eject_image_error_detach(self, mock_sleep):
def test_eject_image_error_detach(self):
mock_server = mock.Mock()
mock_server.name = 'node01'
mock_server.metadata = {
'sushy-tools-image-url': 'http://fish.it/red.iso',
'sushy-tools-volume': 'ccc-ddd'
'sushy-tools-import-image': 'ccc-ddd'
}
self._cc.compute.get_server.return_value = mock_server
self._cc.block_storage.get_volume.side_effect = [
mock.Mock(id='ccc-ddd', status='in-use'),
mock.Mock(id='ccc-ddd', status='queued'),
mock.Mock(id='ccc-ddd', status='detaching'),
mock.Mock(id='ccc-ddd', status='error'),
]
self._cc.image.find_image.return_value = None
e = self.assertRaises(
error.FishyError, self.test_driver.eject_image,
self.uuid)
self.assertEqual('Volume detachment resulted in status error', str(e))
self.assertEqual(
'Failed ejecting image http://fish.it/red.iso. '
'Image not found in image service.', str(e))
self._cc.delete_image.assert_not_called()
self._cc.block_storage.delete_volume.assert_not_called()
# self._cc.delete_image.assert_not_called()
@mock.patch.object(time, 'sleep', autospec=True)
def test__rebuild_with_imported_image(self, mock_sleep):
mock_server = mock.Mock()
mock_server.name = 'node01'
mock_server.metadata = {
'sushy-tools-image-url': 'http://fish.it/red.iso',
'sushy-tools-volume': 'ccc-ddd'
'sushy-tools-image-url': 'http://fish.it/red.iso'
}
self._cc.compute.get_server.return_value = mock_server
queued_image = mock.Mock(id='aaa-bbb', status='queued')
@@ -550,14 +507,6 @@ class NovaDriverTestCase(base.BaseTestCase):
mock.Mock(id='aaa-bbb', status='importing'),
mock.Mock(id='aaa-bbb', status='active'),
]
available_volume = mock.Mock(id='ccc-ddd', status='available')
self._cc.block_storage.get_volume.side_effect = [
mock.Mock(id='ccc-ddd', status='creating'),
available_volume,
mock.Mock(id='ccc-ddd', status='reserved'),
mock.Mock(id='ccc-ddd', status='attaching'),
mock.Mock(id='ccc-ddd', status='in-use')
]
self._cc.compute.rebuild_server.return_value = mock.Mock(
status='REBUILD')
self._cc.compute.get_server.side_effect = [
@@ -568,12 +517,8 @@ class NovaDriverTestCase(base.BaseTestCase):
self.test_driver._rebuild_with_imported_image(
self.uuid, 'aaa-bbb')
self._cc.compute.create_volume_attachment.assert_called_once_with(
self.uuid, available_volume, delete_on_termination=True)
self._cc.compute.rebuild_server.assert_called_once_with(
self.uuid, 'aaa-bbb')
self._cc.delete_image.assert_called_once_with('aaa-bbb')
self._cc.block_storage.delete_volume.assert_not_called()
@mock.patch.object(time, 'sleep', autospec=True)
def test__rebuild_with_imported_imaged_error_image(self, mock_sleep):
@@ -581,15 +526,7 @@ class NovaDriverTestCase(base.BaseTestCase):
mock_server.name = 'node01'
mock_server.metadata = {
'sushy-tools-image-url': 'http://fish.it/red.iso',
'sushy-tools-volume': 'ccc-ddd'
}
self._cc.block_storage.get_volume.side_effect = [
mock.Mock(id='ccc-ddd', status='creating'),
mock.Mock(id='ccc-ddd', status='available'),
mock.Mock(id='ccc-ddd', status='reserved'),
mock.Mock(id='ccc-ddd', status='attaching'),
mock.Mock(id='ccc-ddd', status='in-use')
]
self._cc.image.get_image.side_effect = [
mock.Mock(id='aaa-bbb', status='queued'),
mock.Mock(id='aaa-bbb', status='importing'),
@@ -600,37 +537,12 @@ class NovaDriverTestCase(base.BaseTestCase):
self.uuid, 'aaa-bbb')
self.assertEqual('Image import ended with status error', str(e))
@mock.patch.object(time, 'sleep', autospec=True)
def test__rebuild_with_imported_image_error_volume(self, mock_sleep):
mock_server = mock.Mock()
mock_server.name = 'node01'
mock_server.metadata = {
'sushy-tools-image-url': 'http://fish.it/red.iso',
'sushy-tools-volume': 'ccc-ddd'
}
self._cc.compute.get_server.return_value = mock_server
self._cc.image.get_image.side_effect = [
mock.Mock(id='aaa-bbb', status='queued'),
mock.Mock(id='aaa-bbb', status='importing'),
mock.Mock(id='aaa-bbb', status='active'),
]
self._cc.block_storage.get_volume.side_effect = [
mock.Mock(id='ccc-ddd', status='creating'),
mock.Mock(id='ccc-ddd', status='reserved'),
mock.Mock(id='ccc-ddd', status='error')
]
e = self.assertRaises(
error.FishyError, self.test_driver._rebuild_with_imported_image,
self.uuid, 'aaa-bbb')
self.assertEqual('Volume creation resulted in status reserved', str(e))
@mock.patch.object(time, 'sleep', autospec=True)
def test__rebuild_with_imported_image_error_rebuild(self, mock_sleep):
mock_server = mock.Mock()
mock_server.name = 'node01'
mock_server.metadata = {
'sushy-tools-image-url': 'http://fish.it/red.iso',
'sushy-tools-volume': 'ccc-ddd'
}
self._cc.compute.get_server.return_value = mock_server
self._cc.image.get_image.side_effect = [
@@ -638,13 +550,6 @@ class NovaDriverTestCase(base.BaseTestCase):
mock.Mock(id='aaa-bbb', status='importing'),
mock.Mock(id='aaa-bbb', status='active'),
]
self._cc.block_storage.get_volume.side_effect = [
mock.Mock(id='ccc-ddd', status='creating'),
mock.Mock(id='ccc-ddd', status='available'),
mock.Mock(id='ccc-ddd', status='reserved'),
mock.Mock(id='ccc-ddd', status='attaching'),
mock.Mock(id='ccc-ddd', status='in-use')
]
self._cc.compute.rebuild_server.return_value = mock.Mock(
status='REBUILD')
self._cc.compute.get_server.side_effect = [
@@ -656,103 +561,3 @@ class NovaDriverTestCase(base.BaseTestCase):
self.uuid, 'aaa-bbb')
self.assertEqual(
'Server rebuild attempt resulted in status ERROR', str(e))
@mock.patch.object(time, 'sleep', autospec=True)
def test__rebuild_with_volume_image(self, mock_sleep):
mock_server = mock.Mock()
mock_server.name = 'node01'
mock_server.metadata = {
'sushy-tools-image-url': 'http://fish.it/red.iso',
'sushy-tools-volume': 'ccc-ddd',
'sushy-tools-volume-image': 'aaa-bbb'
}
mock_server.status = 'ACTIVE'
self._cc.image.get_image.side_effect = [
mock.Mock(id='aaa-bbb', status='queued'),
mock.Mock(id='aaa-bbb', status='uploading'),
mock.Mock(id='aaa-bbb', status='saving'),
mock.Mock(id='aaa-bbb', status='active'),
]
self._cc.compute.rebuild_server.return_value = mock.Mock(
status='REBUILD')
self._cc.compute.get_server.side_effect = [
mock_server,
mock.Mock(status='REBUILD'),
mock.Mock(status='ACTIVE'),
]
self._cc.block_storage.get_volume.side_effect = [
mock.Mock(id='ccc-ddd', status='uploading'),
mock.Mock(id='ccc-ddd', status='available')
]
self.test_driver._rebuild_with_volume_image(
self.uuid)
self._cc.compute.rebuild_server.assert_called_once_with(
self.uuid, 'aaa-bbb')
self._cc.delete_image.assert_called_once_with('aaa-bbb')
self._cc.block_storage.delete_volume.assert_called_once_with('ccc-ddd')
@mock.patch.object(time, 'sleep', autospec=True)
def test__rebuild_with_volume_image_error_upload(self, mock_sleep):
mock_server = mock.Mock()
mock_server.name = 'node01'
mock_server.metadata = {
'sushy-tools-image-url': 'http://fish.it/red.iso',
'sushy-tools-volume': 'ccc-ddd',
'sushy-tools-volume-image': 'aaa-bbb'
}
mock_server.status = 'ACTIVE'
self._cc.compute.get_server.return_value = mock_server
self._cc.image.get_image.side_effect = [
mock.Mock(id='aaa-bbb', status='queued'),
mock.Mock(id='aaa-bbb', status='uploading'),
mock.Mock(id='aaa-bbb', status='saving'),
mock.Mock(id='aaa-bbb', status='error'),
]
e = self.assertRaises(
error.FishyError, self.test_driver._rebuild_with_volume_image,
self.uuid)
self.assertEqual('Image import ended with status error', str(e))
self._cc.compute.rebuild_server.assert_not_called()
self._cc.delete_image.assert_called_once_with('aaa-bbb')
self._cc.block_storage.delete_volume.assert_called_once_with('ccc-ddd')
@mock.patch.object(time, 'sleep', autospec=True)
def test__rebuild_with_volume_image_error_rebuild(self, mock_sleep):
mock_server = mock.Mock()
mock_server.name = 'node01'
mock_server.metadata = {
'sushy-tools-image-url': 'http://fish.it/red.iso',
'sushy-tools-volume': 'ccc-ddd',
'sushy-tools-volume-image': 'aaa-bbb'
}
mock_server.status = 'ACTIVE'
self._cc.image.get_image.side_effect = [
mock.Mock(id='aaa-bbb', status='queued'),
mock.Mock(id='aaa-bbb', status='uploading'),
mock.Mock(id='aaa-bbb', status='saving'),
mock.Mock(id='aaa-bbb', status='active'),
]
self._cc.block_storage.upload_volume_to_image.return_value = {
'image_id': 'aaa-bbb'}
self._cc.compute.rebuild_server.return_value = mock.Mock(
status='REBUILD')
self._cc.compute.get_server.side_effect = [
mock_server,
mock.Mock(status='REBUILD'),
mock.Mock(status='ERROR'),
]
e = self.assertRaises(
error.FishyError, self.test_driver._rebuild_with_volume_image,
self.uuid)
self.assertEqual(
'Server rebuild attempt resulted in status ERROR', str(e))
self._cc.compute.rebuild_server.assert_called_once_with(
self.uuid, 'aaa-bbb')
self._cc.delete_image.assert_called_once_with('aaa-bbb')
self._cc.block_storage.delete_volume.assert_called_once_with('ccc-ddd')