Merge "Add virtual-media-boot to openstack driver"
This commit is contained in:
commit
bb08bb4605
@ -86,8 +86,9 @@ SUSHY_EMULATOR_INDICATOR_LEDS = {
|
||||
# Manager(s) and possibly used by the System(s) if system emulation
|
||||
# backend supports boot image configuration.
|
||||
#
|
||||
# If this map is not present in the configuration, the following configuration
|
||||
# is used:
|
||||
# This value is ignored by the OpenStack driver, which only supports the 'Cd'
|
||||
# device. If this map is not present in the configuration, the following
|
||||
# configuration is used for other drivers:
|
||||
SUSHY_EMULATOR_VMEDIA_DEVICES = {
|
||||
u'Cd': {
|
||||
u'Name': 'Virtual CD',
|
||||
|
@ -292,7 +292,7 @@ Redfish *Systems*:
|
||||
"Members": [
|
||||
|
||||
{
|
||||
"@odata.id": "/redfish/v1/Systems/vbmc-node"
|
||||
"@odata.id": "/redfish/v1/Systems/8dbe91da-4002-4d61-a56d-1a00fc61c35d"
|
||||
}
|
||||
|
||||
],
|
||||
@ -528,7 +528,8 @@ to *Cd* and boot mode to *Uefi* will cause the system to boot from
|
||||
virtual media image.
|
||||
|
||||
User can change virtual media devices and their properties through
|
||||
emulator configuration:
|
||||
emulator configuration (except for the OpenStack driver which only
|
||||
supports *Cd*):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@ -549,11 +550,11 @@ emulator configuration:
|
||||
}
|
||||
}
|
||||
|
||||
Virtual Media resource will be revealed when querying Manager resource:
|
||||
Virtual Media resource will be revealed when querying System resource:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -L http://localhost:8000/redfish/v1/Managers/58893887-8974-2487-2389-841168418919/VirtualMedia
|
||||
curl -L http://localhost:8000/redfish/v1/Systems/58893887-8974-2487-2389-841168418919/VirtualMedia
|
||||
{
|
||||
"@odata.type": "#VirtualMediaCollection.VirtualMediaCollection",
|
||||
"Name": "Virtual Media Services",
|
||||
@ -562,16 +563,16 @@ Virtual Media resource will be revealed when querying Manager resource:
|
||||
"Members": [
|
||||
|
||||
{
|
||||
"@odata.id": "/redfish/v1/Managers/58893887-8974-2487-2389-841168418919/VirtualMedia/Cd"
|
||||
"@odata.id": "/redfish/v1/Systems/58893887-8974-2487-2389-841168418919/VirtualMedia/Cd"
|
||||
},
|
||||
|
||||
{
|
||||
"@odata.id": "/redfish/v1/Managers/58893887-8974-2487-2389-841168418919/VirtualMedia/Floppy"
|
||||
"@odata.id": "/redfish/v1/Systems/58893887-8974-2487-2389-841168418919/VirtualMedia/Floppy"
|
||||
}
|
||||
|
||||
],
|
||||
"@odata.context": "/redfish/v1/$metadata#VirtualMediaCollection.VirtualMediaCollection",
|
||||
"@odata.id": "/redfish/v1/Managers/58893887-8974-2487-2389-841168418919/VirtualMedia",
|
||||
"@odata.id": "/redfish/v1/Systems/58893887-8974-2487-2389-841168418919/VirtualMedia",
|
||||
"@Redfish.Copyright": "Copyright 2014-2017 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."
|
||||
}
|
||||
|
||||
@ -579,22 +580,19 @@ Redfish client can insert a HTTP-based image into the virtual device:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -d '{"Image":"http://localhost.localdomain/mini.iso",\
|
||||
"Inserted": true}' \
|
||||
curl -d '{"Image": "http://localhost.localdomain/mini.iso", "Inserted": true}' \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
http://localhost:8000/redfish/v1/Managers/58893887-8974-2487-2389-841168418919/VirtualMedia/Cd/Actions/VirtualMedia.InsertMedia
|
||||
http://localhost:8000/redfish/v1/Systems/58893887-8974-2487-2389-841168418919/VirtualMedia/Cd/Actions/VirtualMedia.InsertMedia
|
||||
|
||||
.. note::
|
||||
On insert the OpenStack driver will:
|
||||
|
||||
All systems being managed by this manager and booting from their
|
||||
corresponding removable media device (e.g. cdrom or fd) will boot the
|
||||
image inserted into manager's virtual media device.
|
||||
|
||||
.. warning::
|
||||
|
||||
System boot from virtual media only works if *System* resource emulation
|
||||
driver supports setting boot image.
|
||||
* 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 the same size as the root disk
|
||||
* Rebuild the server with the image, replacing the contents of the root disk
|
||||
* Delete the image
|
||||
|
||||
Redfish client can eject image from virtual media device:
|
||||
|
||||
@ -603,7 +601,17 @@ Redfish client can eject image from virtual media device:
|
||||
curl -d '{}' \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
http://localhost:8000/redfish/v1/Managers/58893887-8974-2487-2389-841168418919/VirtualMedia/Cd/Actions/VirtualMedia.EjectMedia
|
||||
http://localhost:8000/redfish/v1/Systems/58893887-8974-2487-2389-841168418919/VirtualMedia/Cd/Actions/VirtualMedia.EjectMedia
|
||||
|
||||
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
|
||||
|
||||
Virtual media boot
|
||||
++++++++++++++++++
|
||||
@ -632,11 +640,11 @@ being offered:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ curl http://localhost:8000/redfish/v1/Managers/58893887-894-2487-2389-841168418919/VirtualMedia
|
||||
$ curl http://localhost:8000/redfish/v1/Systems/58893887-894-2487-2389-841168418919/VirtualMedia
|
||||
...
|
||||
"Members": [
|
||||
{
|
||||
"@odata.id": "/redfish/v1/Managers/58893887-8974-2487-2389-841168418919/VirtualMedia/Cd"
|
||||
"@odata.id": "/redfish/v1/Systems/58893887-8974-2487-2389-841168418919/VirtualMedia/Cd"
|
||||
},
|
||||
...
|
||||
|
||||
@ -644,7 +652,7 @@ Knowing virtual media device name, the client can check out its present state:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ curl http://localhost:8000/redfish/v1/Managers/58893887-8974-2487-2389-841168418919/VirtualMedia/Cd
|
||||
$ curl http://localhost:8000/redfish/v1/Systems/58893887-8974-2487-2389-841168418919/VirtualMedia/Cd
|
||||
{
|
||||
...
|
||||
"Name": "Virtual CD",
|
||||
@ -669,13 +677,13 @@ virtual CD drive:
|
||||
'{"Image":"http:://localhost/var/tmp/mini.iso", "Inserted": true}' \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
http://localhost:8000/redfish/v1/Managers/58893887-8974-2487-2389-841168418919/VirtualMedia/Cd/Actions/VirtualMedia.InsertMedia
|
||||
http://localhost:8000/redfish/v1/Systems/58893887-8974-2487-2389-841168418919/VirtualMedia/Cd/Actions/VirtualMedia.InsertMedia
|
||||
|
||||
Querying again, the emulator should have it in the drive:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ curl http://localhost:8000/redfish/v1/Managers/58893887-8974-2487-2389-841168418919/VirtualMedia/Cd
|
||||
$ curl http://localhost:8000/redfish/v1/Systems/58893887-8974-2487-2389-841168418919/VirtualMedia/Cd
|
||||
{
|
||||
...
|
||||
"Name": "Virtual CD",
|
||||
@ -705,6 +713,11 @@ over UEFI:
|
||||
}' \
|
||||
http://localhost:8000/redfish/v1/Systems/281c2fc3-dd34-439a-9f0f-63df45e2c998
|
||||
|
||||
.. note::
|
||||
|
||||
With the OpenStack driver the boot source is changed during insert and eject, so setting
|
||||
`BootSourceOverrideTarget` to `Cd` or `Hdd` has no effect.
|
||||
|
||||
By this point the system will boot off the virtual CD drive when powering it on:
|
||||
|
||||
.. code-block:: bash
|
||||
|
13
releasenotes/notes/vmedia-openstack-fc422b845c343fc3.yaml
Normal file
13
releasenotes/notes/vmedia-openstack-fc422b845c343fc3.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
The openstack driver now supports insert and eject of virtual media. On
|
||||
insert a new empty volume is created and attached to the server and the
|
||||
server is rebuilt with the that image. On eject it is assumed that the
|
||||
attached volume has been rewritten with bootable image data. The volume is
|
||||
detached and uploaded as an image, then the server is rebuilt with that
|
||||
image.
|
||||
|
||||
Both insert and delete results in the root disk being wiped and replaced
|
||||
with the contents of an image, so this should not be used in any scenario
|
||||
where the root disk data needs to be retained.
|
@ -170,6 +170,10 @@ class Application(flask.Flask):
|
||||
@property
|
||||
@memoize.memoize()
|
||||
def vmedia(self):
|
||||
os_cloud = self.config.get('SUSHY_EMULATOR_OS_CLOUD')
|
||||
if os_cloud:
|
||||
return vmddriver.OpenstackDriver(self.config, self.logger,
|
||||
self.systems)
|
||||
return vmddriver.StaticDriver(self.config, self.logger)
|
||||
|
||||
@property
|
||||
|
@ -13,7 +13,12 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import base64
|
||||
from concurrent import futures
|
||||
import math
|
||||
import os
|
||||
import time
|
||||
from urllib import parse as urlparse
|
||||
|
||||
from sushy_tools.emulator import memoize
|
||||
from sushy_tools.emulator.resources.systems.base import AbstractSystemsDriver
|
||||
@ -28,6 +33,8 @@ except ImportError:
|
||||
|
||||
is_loaded = bool(openstack)
|
||||
|
||||
FUTURES = {}
|
||||
|
||||
|
||||
class OpenStackDriver(AbstractSystemsDriver):
|
||||
"""OpenStack driver"""
|
||||
@ -58,6 +65,7 @@ class OpenStackDriver(AbstractSystemsDriver):
|
||||
cls._os_cloud = os_cloud
|
||||
|
||||
cls._cc = openstack.connect(cloud=os_cloud)
|
||||
cls._executor = futures.ThreadPoolExecutor(max_workers=4)
|
||||
|
||||
return cls
|
||||
|
||||
@ -95,6 +103,18 @@ class OpenStackDriver(AbstractSystemsDriver):
|
||||
def _set_server_metadata(self, identity, metadata):
|
||||
self._cc.compute.set_server_metadata(identity, metadata)
|
||||
|
||||
@property
|
||||
def _futures(self):
|
||||
return FUTURES
|
||||
|
||||
@property
|
||||
def connection(self):
|
||||
"""Return openstack connection
|
||||
|
||||
:returns: Connection object
|
||||
"""
|
||||
return self._cc
|
||||
|
||||
@property
|
||||
def driver(self):
|
||||
"""Return human-friendly driver description
|
||||
@ -358,3 +378,339 @@ class OpenStackDriver(AbstractSystemsDriver):
|
||||
'Could not find MAC address in %s', adr)
|
||||
return [{'id': mac, 'mac': mac}
|
||||
for mac in macs]
|
||||
|
||||
def get_boot_image(self, identity, device):
|
||||
"""Get backend VM boot image info
|
||||
|
||||
:param identity: node name or ID
|
||||
:param device: device type (from
|
||||
`sushy_tools.emulator.constants`)
|
||||
:returns: a `tuple` of (boot_image, write_protected, inserted)
|
||||
:raises: `error.FishyError` if boot device can't be accessed
|
||||
"""
|
||||
instance = self._get_instance(identity)
|
||||
return instance.image.id, False, True
|
||||
|
||||
def set_boot_image(self, identity, device, boot_image=None,
|
||||
write_protected=True):
|
||||
"""Set backend VM boot image
|
||||
|
||||
:param identity: node name or ID
|
||||
:param device: device type (from
|
||||
`sushy_tools.emulator.constants`)
|
||||
:param boot_image: ID of the image, or `None` to switch to
|
||||
boot from volume
|
||||
:param write_protected: expose media as read-only or writable
|
||||
|
||||
:raises: `error.FishyError` if boot device can't be set
|
||||
"""
|
||||
instance = self._get_instance(identity)
|
||||
|
||||
if instance.image.id == boot_image:
|
||||
msg = ('Image %(identity)s already has image %(boot_image)s. '
|
||||
'Skipping rebuild.' % {'identity': identity,
|
||||
'boot_image': boot_image})
|
||||
self._logger.debug(msg)
|
||||
|
||||
elif boot_image is None:
|
||||
self._logger.debug(
|
||||
'Creating task to upload volume and rebuild for %(identity)s' %
|
||||
{'identity': identity})
|
||||
self._submit_future(
|
||||
True, self._rebuild_with_volume_image, identity)
|
||||
else:
|
||||
self._logger.debug(
|
||||
'Creating task to finish import and rebuild for %(identity)s' %
|
||||
{'identity': identity})
|
||||
self._submit_future(
|
||||
True, self._rebuild_with_imported_image, identity, boot_image)
|
||||
|
||||
def insert_image(self, identity, image_url):
|
||||
self._logger.debug(
|
||||
'Creating task to insert image for %(identity)s' %
|
||||
{'identity': identity})
|
||||
return self._submit_future(
|
||||
False, self._insert_image, identity, image_url)
|
||||
|
||||
def _insert_image(self, identity, image_url):
|
||||
parsed_url = urlparse.urlparse(image_url)
|
||||
local_file = os.path.basename(parsed_url.path)
|
||||
unique = base64.urlsafe_b64encode(os.urandom(6)).decode('utf-8')
|
||||
image_attrs = {
|
||||
'name': '%s %s' % (local_file, unique),
|
||||
'disk_format': 'raw',
|
||||
'container_format': 'bare',
|
||||
'visibility': 'private'
|
||||
}
|
||||
image = None
|
||||
volume = None
|
||||
try:
|
||||
|
||||
# Create image, and begin importing. Waiting for import to complete
|
||||
# will be part of a long-running operation
|
||||
image = self._cc.image.create_image(**image_attrs)
|
||||
self._logger.debug(
|
||||
'Importing image %(url)s for %(identity)s' %
|
||||
{'identity': identity, 'url': image_url})
|
||||
self._cc.image.import_image(image, method='web-download',
|
||||
uri=image_url)
|
||||
self._cc.set_server_metadata(
|
||||
identity, {
|
||||
'sushy-tools-import-image': image.id,
|
||||
'sushy-tools-image-url': image_url
|
||||
})
|
||||
|
||||
# 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, identity,
|
||||
'sushy-tools-import-image', 'sushy-tools-volume')
|
||||
if not isinstance(ex, error.FishyError):
|
||||
ex = error.FishyError(msg)
|
||||
raise ex
|
||||
|
||||
return image.id, image.name
|
||||
|
||||
def eject_image(self, identity):
|
||||
self._logger.debug(
|
||||
'Creating task to eject image for %(identity)s' %
|
||||
{'identity': identity})
|
||||
self._submit_future(False, self._eject_image, identity)
|
||||
|
||||
def _eject_image(self, identity):
|
||||
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_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)
|
||||
|
||||
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_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})
|
||||
|
||||
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
|
||||
|
||||
def _attempt_delete_image_volume(self, image, volume, 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
|
||||
if image:
|
||||
try:
|
||||
self._logger.debug('Deleting image %(image)s' %
|
||||
{'image': image})
|
||||
self._cc.delete_image(image)
|
||||
except Exception:
|
||||
pass
|
||||
if identity and metadata_keys:
|
||||
try:
|
||||
self._cc.delete_server_metadata(identity, metadata_keys)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _submit_future(self, run_async, fn, identity, *args, **kwargs):
|
||||
future = self._futures.get(identity, None)
|
||||
if future is not None:
|
||||
if future.running():
|
||||
raise error.Conflict(
|
||||
'An insert or eject operation is already in progress for '
|
||||
'%(identity)s' % {'identity': identity})
|
||||
|
||||
ex = future.exception()
|
||||
del self._futures[identity]
|
||||
if ex is not None:
|
||||
# A previous operation failed, and the server may be in an
|
||||
# unknown state. Raise the previous error as an error for
|
||||
# this operation.
|
||||
raise ex
|
||||
|
||||
future = self._executor.submit(fn, identity, *args, **kwargs)
|
||||
self._futures[identity] = future
|
||||
if run_async:
|
||||
return
|
||||
ex = future.exception()
|
||||
if ex is not None:
|
||||
raise ex
|
||||
return future.result()
|
||||
|
||||
def _rebuild_with_imported_image(self, identity, image_id):
|
||||
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')
|
||||
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)
|
||||
if image.status != 'active':
|
||||
raise error.FishyError('Image import ended with status %s' %
|
||||
image.status)
|
||||
|
||||
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 != 'ACTIVE':
|
||||
raise error.FishyError('Server rebuild attempt resulted in '
|
||||
'status %s' % server.status)
|
||||
self._logger.debug(
|
||||
'Rebuild %(identity)s complete' % {'identity': identity})
|
||||
|
||||
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, identity, 'sushy-tools-volume')
|
||||
if not isinstance(ex, error.FishyError):
|
||||
ex = error.FishyError(msg)
|
||||
raise ex
|
||||
finally:
|
||||
self._attempt_delete_image_volume(
|
||||
image_id, None, identity, 'sushy-tools-image')
|
||||
|
||||
def _rebuild_with_volume_image(self, identity):
|
||||
try:
|
||||
|
||||
server = self._cc.compute.get_server(identity)
|
||||
image_id = server.metadata.get('sushy-tools-volume-image')
|
||||
volume_id = server.metadata.get('sushy-tools-volume')
|
||||
image_url = server.metadata.get('sushy-tools-image-url')
|
||||
|
||||
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)
|
||||
|
||||
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 != 'ACTIVE':
|
||||
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, identity,
|
||||
'sushy-tools-volume-image', 'sushy-tools-volume')
|
||||
|
@ -13,6 +13,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import abc
|
||||
import collections
|
||||
import os
|
||||
import re
|
||||
@ -37,7 +38,7 @@ Certificate = collections.namedtuple(
|
||||
_CERT_ID = "Default"
|
||||
|
||||
|
||||
class StaticDriver(base.DriverBase):
|
||||
class BaseDriver(base.DriverBase):
|
||||
"""Redfish virtual media simulator."""
|
||||
|
||||
def __init__(self, config, logger):
|
||||
@ -196,6 +197,34 @@ class StaticDriver(base.DriverBase):
|
||||
del device_info["Certificate"]
|
||||
self._devices[(identity, device)] = device_info
|
||||
|
||||
@abc.abstractmethod
|
||||
def insert_image(self, identity, device, image_url,
|
||||
inserted=True, write_protected=True,
|
||||
username=None, password=None):
|
||||
"""Upload, remove or insert virtual media
|
||||
|
||||
:param identity: parent resource ID
|
||||
:param device: device name
|
||||
:param image_url: URL to ISO image to place into `device` or `None`
|
||||
to eject currently present media
|
||||
:param inserted: treat currently present media as inserted or not
|
||||
:param write_protected: prevent write access the inserted media
|
||||
:raises: `FishyError` if image can't be manipulated
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def eject_image(self, identity, device):
|
||||
"""Eject virtual media image
|
||||
|
||||
:param identity: parent resource ID
|
||||
:param device: device name
|
||||
:raises: `FishyError` if image can't be manipulated
|
||||
"""
|
||||
|
||||
|
||||
class StaticDriver(BaseDriver):
|
||||
"""Redfish virtual media simulator for local image storage."""
|
||||
|
||||
def _write_from_response(self, image_url, rsp, tmp_file):
|
||||
with open(tmp_file.name, 'wb') as fl:
|
||||
for chunk in rsp.iter_content(chunk_size=8192):
|
||||
@ -344,3 +373,84 @@ class StaticDriver(base.DriverBase):
|
||||
except FileNotFoundError:
|
||||
# Ignore error as we are trying to remove the file anyway
|
||||
pass
|
||||
|
||||
|
||||
class OpenstackDriver(BaseDriver):
|
||||
"""Redfish virtual media simulator for openstack image storage."""
|
||||
|
||||
def __init__(self, config, logger, driver):
|
||||
super().__init__(config, logger)
|
||||
# Only support 'Cd', ignore SUSHY_EMULATOR_VMEDIA_DEVICES
|
||||
self._device_types = {
|
||||
'Cd': {
|
||||
'Name': 'Virtual CD',
|
||||
'MediaTypes': [
|
||||
'CD',
|
||||
'DVD'
|
||||
]
|
||||
}
|
||||
}
|
||||
self._driver = driver
|
||||
|
||||
@property
|
||||
def driver(self):
|
||||
"""Return human-friendly driver description
|
||||
|
||||
:returns: driver description as `str`
|
||||
"""
|
||||
return self._driver.driver
|
||||
|
||||
def insert_image(self, identity, device, image_url,
|
||||
inserted=True, write_protected=True,
|
||||
username=None, password=None):
|
||||
"""Upload, remove or insert virtual media
|
||||
|
||||
:param identity: parent resource ID
|
||||
:param device: device name
|
||||
:param image_url: URL to ISO image to place into `device` or `None`
|
||||
to eject currently present media
|
||||
:param inserted: treat currently present media as inserted or not
|
||||
:param write_protected: prevent write access the inserted media
|
||||
:raises: `FishyError` if image can't be manipulated
|
||||
"""
|
||||
device_info = self._get_device(identity, device)
|
||||
verify_media_cert = device_info.get(
|
||||
'Verify',
|
||||
# NOTE(dtantsur): it's de facto standard for Redfish to default
|
||||
# to no certificate validation.
|
||||
self._config.get('SUSHY_EMULATOR_VMEDIA_VERIFY_SSL', False))
|
||||
if verify_media_cert:
|
||||
msg = ('The cloud driver %(driver)s does not support inserting an '
|
||||
'image with a custom download certificate' %
|
||||
{'driver': self.driver})
|
||||
raise error.NotSupportedError(msg)
|
||||
|
||||
auth = (username, password) if (username and password) else None
|
||||
if auth:
|
||||
msg = ('The cloud driver %(driver)s does not support inserting an '
|
||||
'image with download credentials' % {'driver': self.driver})
|
||||
raise error.NotSupportedError(msg)
|
||||
|
||||
image_id, image_name = self._driver.insert_image(
|
||||
identity, image_url)
|
||||
|
||||
device_info['Image'] = image_url
|
||||
device_info['ImageName'] = image_name
|
||||
device_info['Inserted'] = inserted
|
||||
device_info['WriteProtected'] = write_protected
|
||||
|
||||
self._devices.update({(identity, device): device_info})
|
||||
return image_id
|
||||
|
||||
def eject_image(self, identity, device):
|
||||
"""Eject virtual media image
|
||||
|
||||
:param identity: parent resource ID
|
||||
:param device: device name
|
||||
:raises: `FishyError` if image can't be manipulated
|
||||
"""
|
||||
device_info = self._get_device(identity, device)
|
||||
self._driver.eject_image(identity)
|
||||
device_info['Image'] = ''
|
||||
device_info['ImageName'] = ''
|
||||
device_info['Inserted'] = False
|
||||
|
@ -52,3 +52,10 @@ class FeatureNotAvailable(NotFound):
|
||||
|
||||
def __init__(self, feature, code=404):
|
||||
super().__init__(f"Feature {feature} not available", code=code)
|
||||
|
||||
|
||||
class Conflict(FishyError):
|
||||
"""Conflict with current state of the resource."""
|
||||
|
||||
def __init__(self, msg, code=409):
|
||||
super().__init__(msg, code)
|
||||
|
@ -12,6 +12,8 @@
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import base64
|
||||
import time
|
||||
from unittest import mock
|
||||
|
||||
from munch import Munch
|
||||
@ -30,10 +32,12 @@ class NovaDriverTestCase(base.BaseTestCase):
|
||||
def setUp(self):
|
||||
self.nova_patcher = mock.patch('openstack.connect', autospec=True)
|
||||
self.nova_mock = self.nova_patcher.start()
|
||||
self._cc = self.nova_mock.return_value
|
||||
|
||||
test_driver_class = OpenStackDriver.initialize(
|
||||
{}, mock.MagicMock(), 'fake-cloud')
|
||||
self.test_driver = test_driver_class()
|
||||
self.test_driver._futures.clear()
|
||||
|
||||
super(NovaDriverTestCase, self).setUp()
|
||||
|
||||
@ -305,3 +309,349 @@ class NovaDriverTestCase(base.BaseTestCase):
|
||||
self.assertRaises(error.NotSupportedError,
|
||||
self.test_driver.set_http_boot_uri,
|
||||
None)
|
||||
|
||||
@mock.patch.object(base64, 'urlsafe_b64encode', autospec=True)
|
||||
def test_insert_image(self, mock_b64e):
|
||||
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')
|
||||
|
||||
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')
|
||||
|
||||
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'
|
||||
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')
|
||||
|
||||
e = self.assertRaises(
|
||||
error.FishyError, self.test_driver.insert_image,
|
||||
self.uuid, 'http://fish.it/red.iso')
|
||||
self.assertEqual(
|
||||
'Failed insert image from URL http://fish.it/red.iso: ouch',
|
||||
str(e))
|
||||
|
||||
def test_insert_image_future_running(self):
|
||||
|
||||
mock_future = mock.Mock()
|
||||
mock_future.running.return_value = True
|
||||
self.test_driver._futures[self.uuid] = mock_future
|
||||
e = self.assertRaises(
|
||||
error.FishyError, self.test_driver.insert_image,
|
||||
self.uuid, 'http://fish.it/red.iso')
|
||||
self.assertEqual(
|
||||
'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_future = mock.Mock()
|
||||
mock_future.running.return_value = False
|
||||
mock_future.exception.return_value = error.FishyError('ouch')
|
||||
self.test_driver._futures[self.uuid] = mock_future
|
||||
e = self.assertRaises(
|
||||
error.FishyError, self.test_driver.insert_image,
|
||||
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):
|
||||
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
|
||||
|
||||
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.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')
|
||||
|
||||
@mock.patch.object(time, 'sleep', autospec=True)
|
||||
def test_eject_image_error_detach(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.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'),
|
||||
]
|
||||
|
||||
e = self.assertRaises(
|
||||
error.FishyError, self.test_driver.eject_image,
|
||||
self.uuid)
|
||||
self.assertEqual('Volume detachment resulted in status error', str(e))
|
||||
|
||||
self._cc.delete_image.assert_not_called()
|
||||
self._cc.block_storage.delete_volume.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'
|
||||
}
|
||||
self._cc.compute.get_server.return_value = mock_server
|
||||
queued_image = mock.Mock(id='aaa-bbb', status='queued')
|
||||
self._cc.image.get_image.side_effect = [
|
||||
queued_image,
|
||||
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 = [
|
||||
mock.Mock(status='REBUILD'),
|
||||
mock.Mock(status='ACTIVE'),
|
||||
]
|
||||
|
||||
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):
|
||||
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.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'),
|
||||
mock.Mock(id='aaa-bbb', status='error'),
|
||||
]
|
||||
e = self.assertRaises(
|
||||
error.FishyError, self.test_driver._rebuild_with_imported_image,
|
||||
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 = [
|
||||
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='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 = [
|
||||
mock.Mock(status='REBUILD'),
|
||||
mock.Mock(status='ERROR'),
|
||||
]
|
||||
e = self.assertRaises(
|
||||
error.FishyError, self.test_driver._rebuild_with_imported_image,
|
||||
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')
|
||||
|
@ -495,3 +495,100 @@ class StaticDriverTestCase(base.BaseTestCase):
|
||||
self.assertRaises(error.NotFound,
|
||||
self.test_driver.delete_certificate,
|
||||
self.UUID, 'Cd', 'Default')
|
||||
|
||||
|
||||
class OpenstackDriverTestCase(base.BaseTestCase):
|
||||
|
||||
UUID = 'ZZZ-YYY-XXX'
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.novadriver = mock.Mock()
|
||||
with mock.patch('sushy_tools.emulator.memoize.PersistentDict',
|
||||
return_value={}, autospec=True):
|
||||
self.test_driver = vmedia.OpenstackDriver(
|
||||
{}, mock.MagicMock(), self.novadriver)
|
||||
|
||||
@mock.patch.object(vmedia.OpenstackDriver, '_get_device', autospec=True)
|
||||
def test_insert_image(self, mock_get_device):
|
||||
device_info = {}
|
||||
mock_get_device.return_value = device_info
|
||||
|
||||
self.novadriver.insert_image.return_value = ('aaa-bbb', 'red.iso')
|
||||
|
||||
image_id = self.test_driver.insert_image(
|
||||
self.UUID, 'Cd', 'http://fish.it/red.iso', inserted=True,
|
||||
write_protected=False)
|
||||
|
||||
self.novadriver.insert_image.assert_called_once_with(
|
||||
self.UUID, 'http://fish.it/red.iso')
|
||||
self.assertEqual('aaa-bbb', image_id)
|
||||
|
||||
self.assertEqual('http://fish.it/red.iso', device_info['Image'])
|
||||
self.assertEqual('red.iso', device_info['ImageName'])
|
||||
self.assertTrue(device_info['Inserted'])
|
||||
self.assertFalse(device_info['WriteProtected'])
|
||||
|
||||
@mock.patch.object(vmedia.OpenstackDriver, '_get_device', autospec=True)
|
||||
def test_insert_image_auth(self, mock_get_device):
|
||||
device_info = {}
|
||||
mock_get_device.return_value = device_info
|
||||
|
||||
self.assertRaises(
|
||||
error.NotSupportedError, self.test_driver.insert_image,
|
||||
self.UUID, 'Cd', 'http://fish.it/red.iso', inserted=True,
|
||||
write_protected=False, username='Admin', password='Secret')
|
||||
|
||||
@mock.patch.object(vmedia.OpenstackDriver, '_get_device', autospec=True)
|
||||
def test_insert_image_verify_ssl(self, mock_get_device):
|
||||
device_info = {}
|
||||
mock_get_device.return_value = device_info
|
||||
|
||||
ssl_conf_key = 'SUSHY_EMULATOR_VMEDIA_VERIFY_SSL'
|
||||
self.test_driver._config[ssl_conf_key] = True
|
||||
self.assertRaises(
|
||||
error.NotSupportedError, self.test_driver.insert_image,
|
||||
self.UUID, 'Cd', 'https://fish.it/red.iso', inserted=True,
|
||||
write_protected=False)
|
||||
|
||||
@mock.patch.object(vmedia.OpenstackDriver, '_get_device', autospec=True)
|
||||
def test_insert_image_fail(self, mock_get_device):
|
||||
device_info = {}
|
||||
mock_get_device.return_value = device_info
|
||||
self.novadriver.insert_image.side_effect = error.FishyError('ouch')
|
||||
|
||||
e = self.assertRaises(
|
||||
error.FishyError, self.test_driver.insert_image,
|
||||
self.UUID, 'Cd', 'http://fish.it/red.iso', inserted=True,
|
||||
write_protected=False)
|
||||
self.assertEqual('ouch', str(e))
|
||||
|
||||
@mock.patch.object(vmedia.OpenstackDriver, '_get_device', autospec=True)
|
||||
def test_eject_image(self, mock_get_device):
|
||||
|
||||
device_info = {
|
||||
'Image': 'http://fish.it/red.iso',
|
||||
'Inserted': True
|
||||
}
|
||||
mock_get_device.return_value = device_info
|
||||
|
||||
self.test_driver.eject_image(self.UUID, 'Cd')
|
||||
|
||||
self.assertFalse(device_info['Inserted'])
|
||||
self.assertEqual('', device_info['Image'])
|
||||
self.assertEqual('', device_info['ImageName'])
|
||||
|
||||
@mock.patch.object(vmedia.OpenstackDriver, '_get_device', autospec=True)
|
||||
def test_eject_image_error(self, mock_get_device):
|
||||
device_info = {
|
||||
'Image': 'http://fish.it/red.iso',
|
||||
'Inserted': True
|
||||
}
|
||||
mock_get_device.return_value = device_info
|
||||
self.novadriver.eject_image.side_effect = error.FishyError('ouch')
|
||||
|
||||
e = self.assertRaises(
|
||||
error.FishyError, self.test_driver.eject_image,
|
||||
self.UUID, 'Cd')
|
||||
self.assertEqual('ouch', str(e))
|
||||
self.assertTrue(device_info['Inserted'])
|
||||
|
Loading…
Reference in New Issue
Block a user