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:
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user