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
|
# Manager(s) and possibly used by the System(s) if system emulation
|
||||||
# backend supports boot image configuration.
|
# backend supports boot image configuration.
|
||||||
#
|
#
|
||||||
# If this map is not present in the configuration, the following configuration
|
# This value is ignored by the OpenStack driver, which only supports the 'Cd'
|
||||||
# is used:
|
# device. If this map is not present in the configuration, the following
|
||||||
|
# configuration is used for other drivers:
|
||||||
SUSHY_EMULATOR_VMEDIA_DEVICES = {
|
SUSHY_EMULATOR_VMEDIA_DEVICES = {
|
||||||
u'Cd': {
|
u'Cd': {
|
||||||
u'Name': 'Virtual CD',
|
u'Name': 'Virtual CD',
|
||||||
|
@ -292,7 +292,7 @@ Redfish *Systems*:
|
|||||||
"Members": [
|
"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.
|
virtual media image.
|
||||||
|
|
||||||
User can change virtual media devices and their properties through
|
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
|
.. 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
|
.. 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",
|
"@odata.type": "#VirtualMediaCollection.VirtualMediaCollection",
|
||||||
"Name": "Virtual Media Services",
|
"Name": "Virtual Media Services",
|
||||||
@ -562,16 +563,16 @@ Virtual Media resource will be revealed when querying Manager resource:
|
|||||||
"Members": [
|
"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.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."
|
"@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
|
.. code-block:: bash
|
||||||
|
|
||||||
curl -d '{"Image":"http://localhost.localdomain/mini.iso",\
|
curl -d '{"Image": "http://localhost.localdomain/mini.iso", "Inserted": true}' \
|
||||||
"Inserted": true}' \
|
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-X POST \
|
-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
|
* Upload the image directly to glance from the URL (long running)
|
||||||
corresponding removable media device (e.g. cdrom or fd) will boot the
|
* Store the URL, image ID and volume ID in server metadata properties
|
||||||
image inserted into manager's virtual media device.
|
`sushy-tools-image-url`, `sushy-tools-import-image`, `sushy-tools-volume`
|
||||||
|
* Create and attach a new volume the same size as the root disk
|
||||||
.. warning::
|
* Rebuild the server with the image, replacing the contents of the root disk
|
||||||
|
* Delete the image
|
||||||
System boot from virtual media only works if *System* resource emulation
|
|
||||||
driver supports setting boot image.
|
|
||||||
|
|
||||||
Redfish client can eject image from virtual media device:
|
Redfish client can eject image from virtual media device:
|
||||||
|
|
||||||
@ -603,7 +601,17 @@ Redfish client can eject image from virtual media device:
|
|||||||
curl -d '{}' \
|
curl -d '{}' \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-X POST \
|
-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
|
Virtual media boot
|
||||||
++++++++++++++++++
|
++++++++++++++++++
|
||||||
@ -632,11 +640,11 @@ being offered:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. 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": [
|
"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
|
.. 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",
|
"Name": "Virtual CD",
|
||||||
@ -669,13 +677,13 @@ virtual CD drive:
|
|||||||
'{"Image":"http:://localhost/var/tmp/mini.iso", "Inserted": true}' \
|
'{"Image":"http:://localhost/var/tmp/mini.iso", "Inserted": true}' \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-X POST \
|
-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:
|
Querying again, the emulator should have it in the drive:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. 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",
|
"Name": "Virtual CD",
|
||||||
@ -705,6 +713,11 @@ over UEFI:
|
|||||||
}' \
|
}' \
|
||||||
http://localhost:8000/redfish/v1/Systems/281c2fc3-dd34-439a-9f0f-63df45e2c998
|
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:
|
By this point the system will boot off the virtual CD drive when powering it on:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. 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
|
@property
|
||||||
@memoize.memoize()
|
@memoize.memoize()
|
||||||
def vmedia(self):
|
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)
|
return vmddriver.StaticDriver(self.config, self.logger)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -13,7 +13,12 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from concurrent import futures
|
||||||
import math
|
import math
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from urllib import parse as urlparse
|
||||||
|
|
||||||
from sushy_tools.emulator import memoize
|
from sushy_tools.emulator import memoize
|
||||||
from sushy_tools.emulator.resources.systems.base import AbstractSystemsDriver
|
from sushy_tools.emulator.resources.systems.base import AbstractSystemsDriver
|
||||||
@ -28,6 +33,8 @@ except ImportError:
|
|||||||
|
|
||||||
is_loaded = bool(openstack)
|
is_loaded = bool(openstack)
|
||||||
|
|
||||||
|
FUTURES = {}
|
||||||
|
|
||||||
|
|
||||||
class OpenStackDriver(AbstractSystemsDriver):
|
class OpenStackDriver(AbstractSystemsDriver):
|
||||||
"""OpenStack driver"""
|
"""OpenStack driver"""
|
||||||
@ -58,6 +65,7 @@ class OpenStackDriver(AbstractSystemsDriver):
|
|||||||
cls._os_cloud = os_cloud
|
cls._os_cloud = os_cloud
|
||||||
|
|
||||||
cls._cc = openstack.connect(cloud=os_cloud)
|
cls._cc = openstack.connect(cloud=os_cloud)
|
||||||
|
cls._executor = futures.ThreadPoolExecutor(max_workers=4)
|
||||||
|
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
@ -95,6 +103,18 @@ class OpenStackDriver(AbstractSystemsDriver):
|
|||||||
def _set_server_metadata(self, identity, metadata):
|
def _set_server_metadata(self, identity, metadata):
|
||||||
self._cc.compute.set_server_metadata(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
|
@property
|
||||||
def driver(self):
|
def driver(self):
|
||||||
"""Return human-friendly driver description
|
"""Return human-friendly driver description
|
||||||
@ -358,3 +378,339 @@ class OpenStackDriver(AbstractSystemsDriver):
|
|||||||
'Could not find MAC address in %s', adr)
|
'Could not find MAC address in %s', adr)
|
||||||
return [{'id': mac, 'mac': mac}
|
return [{'id': mac, 'mac': mac}
|
||||||
for mac in macs]
|
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
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import abc
|
||||||
import collections
|
import collections
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@ -37,7 +38,7 @@ Certificate = collections.namedtuple(
|
|||||||
_CERT_ID = "Default"
|
_CERT_ID = "Default"
|
||||||
|
|
||||||
|
|
||||||
class StaticDriver(base.DriverBase):
|
class BaseDriver(base.DriverBase):
|
||||||
"""Redfish virtual media simulator."""
|
"""Redfish virtual media simulator."""
|
||||||
|
|
||||||
def __init__(self, config, logger):
|
def __init__(self, config, logger):
|
||||||
@ -196,6 +197,34 @@ class StaticDriver(base.DriverBase):
|
|||||||
del device_info["Certificate"]
|
del device_info["Certificate"]
|
||||||
self._devices[(identity, device)] = device_info
|
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):
|
def _write_from_response(self, image_url, rsp, tmp_file):
|
||||||
with open(tmp_file.name, 'wb') as fl:
|
with open(tmp_file.name, 'wb') as fl:
|
||||||
for chunk in rsp.iter_content(chunk_size=8192):
|
for chunk in rsp.iter_content(chunk_size=8192):
|
||||||
@ -344,3 +373,84 @@ class StaticDriver(base.DriverBase):
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# Ignore error as we are trying to remove the file anyway
|
# Ignore error as we are trying to remove the file anyway
|
||||||
pass
|
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):
|
def __init__(self, feature, code=404):
|
||||||
super().__init__(f"Feature {feature} not available", code=code)
|
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
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
import base64
|
||||||
|
import time
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from munch import Munch
|
from munch import Munch
|
||||||
@ -30,10 +32,12 @@ class NovaDriverTestCase(base.BaseTestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.nova_patcher = mock.patch('openstack.connect', autospec=True)
|
self.nova_patcher = mock.patch('openstack.connect', autospec=True)
|
||||||
self.nova_mock = self.nova_patcher.start()
|
self.nova_mock = self.nova_patcher.start()
|
||||||
|
self._cc = self.nova_mock.return_value
|
||||||
|
|
||||||
test_driver_class = OpenStackDriver.initialize(
|
test_driver_class = OpenStackDriver.initialize(
|
||||||
{}, mock.MagicMock(), 'fake-cloud')
|
{}, mock.MagicMock(), 'fake-cloud')
|
||||||
self.test_driver = test_driver_class()
|
self.test_driver = test_driver_class()
|
||||||
|
self.test_driver._futures.clear()
|
||||||
|
|
||||||
super(NovaDriverTestCase, self).setUp()
|
super(NovaDriverTestCase, self).setUp()
|
||||||
|
|
||||||
@ -305,3 +309,349 @@ class NovaDriverTestCase(base.BaseTestCase):
|
|||||||
self.assertRaises(error.NotSupportedError,
|
self.assertRaises(error.NotSupportedError,
|
||||||
self.test_driver.set_http_boot_uri,
|
self.test_driver.set_http_boot_uri,
|
||||||
None)
|
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.assertRaises(error.NotFound,
|
||||||
self.test_driver.delete_certificate,
|
self.test_driver.delete_certificate,
|
||||||
self.UUID, 'Cd', 'Default')
|
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…
x
Reference in New Issue
Block a user