api: Add response body schemas for images APIs

This is mostly uneventful save for us needing to fix our API ref, which
indicated that the 'OS-EXT-IMG-SIZE:size' field shown in the 'show' and
'detail' views was a string rather than an int. You can confirm this is
*not* the case like so:

  >>> import openstack
  >>> conn = openstack.connect()
  >>> conn.conn.compute.get('https://example.com/compute/v2.1/images/detail').json()

(obviously replace 'https://example.com/' with a compute API host)

Change-Id: Ia318478dfdb50f8d57a74958b3555f6ad97351ec
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane
2024-11-12 15:44:54 +00:00
parent ed984eb756
commit d8e1248b7e
18 changed files with 472 additions and 230 deletions

View File

@@ -1,7 +1,7 @@
{
"image": {
"OS-DCF:diskConfig": "AUTO",
"OS-EXT-IMG-SIZE:size": "74185822",
"OS-EXT-IMG-SIZE:size": 74185822,
"created": "2011-01-01T01:02:03Z",
"id": "70a599e0-31e7-49b7-b260-868f441e862b",
"links": [

View File

@@ -1,7 +1,7 @@
{
"images": [
{
"OS-EXT-IMG-SIZE:size": "25165824",
"OS-EXT-IMG-SIZE:size": 25165824,
"created": "2011-01-01T01:02:03Z",
"id": "155d900f-4e14-4e4c-a73d-069cbf4541e6",
"links": [
@@ -32,7 +32,7 @@
"updated": "2011-01-01T01:02:03Z"
},
{
"OS-EXT-IMG-SIZE:size": "58145823",
"OS-EXT-IMG-SIZE:size": 58145823,
"created": "2011-01-01T01:02:03Z",
"id": "a2459075-d96c-40d5-893e-577ff92e721c",
"links": [
@@ -62,7 +62,7 @@
"updated": "2011-01-01T01:02:03Z"
},
{
"OS-EXT-IMG-SIZE:size": "83594576",
"OS-EXT-IMG-SIZE:size": 83594576,
"created": "2011-01-01T01:02:03Z",
"id": "76fa36fc-c930-4bf3-8c8a-ea2a2420deb6",
"links": [
@@ -93,7 +93,7 @@
"updated": "2011-01-01T01:02:03Z"
},
{
"OS-EXT-IMG-SIZE:size": "84035174",
"OS-EXT-IMG-SIZE:size": 84035174,
"created": "2011-01-01T01:02:03Z",
"id": "cedef40a-ed67-4d10-800e-17455edce175",
"links": [
@@ -123,7 +123,7 @@
"updated": "2011-01-01T01:02:03Z"
},
{
"OS-EXT-IMG-SIZE:size": "26360814",
"OS-EXT-IMG-SIZE:size": 26360814,
"created": "2011-01-01T01:02:03Z",
"id": "c905cedb-7281-47e4-8a62-f26bc5fc4c77",
"links": [
@@ -154,7 +154,7 @@
},
{
"OS-DCF:diskConfig": "MANUAL",
"OS-EXT-IMG-SIZE:size": "49163826",
"OS-EXT-IMG-SIZE:size": 49163826,
"created": "2011-01-01T01:02:03Z",
"id": "a440c04b-79fa-479c-bed1-0b816eaec379",
"links": [
@@ -187,7 +187,7 @@
},
{
"OS-DCF:diskConfig": "AUTO",
"OS-EXT-IMG-SIZE:size": "74185822",
"OS-EXT-IMG-SIZE:size": 74185822,
"created": "2011-01-01T01:02:03Z",
"id": "70a599e0-31e7-49b7-b260-868f441e862b",
"links": [
@@ -219,7 +219,7 @@
"updated": "2011-01-01T01:02:03Z"
},
{
"OS-EXT-IMG-SIZE:size": "25165824",
"OS-EXT-IMG-SIZE:size": 25165824,
"created": "2011-01-01T01:02:03Z",
"id": "95fad737-9325-4855-b37e-20a62268ec88",
"links": [
@@ -248,7 +248,7 @@
"updated": "2011-01-01T01:02:03Z"
},
{
"OS-EXT-IMG-SIZE:size": "25165824",
"OS-EXT-IMG-SIZE:size": 25165824,
"created": "2011-01-01T01:02:03Z",
"id": "535426d4-5d75-44f4-9591-a2123d23c33f",
"links": [
@@ -277,7 +277,7 @@
"updated": "2011-01-01T01:02:03Z"
},
{
"OS-EXT-IMG-SIZE:size": "25165824",
"OS-EXT-IMG-SIZE:size": 25165824,
"created": "2011-01-01T01:02:03Z",
"id": "5f7d4f5b-3781-4a4e-9046-a2a800e807e5",
"links": [
@@ -307,7 +307,7 @@
"updated": "2011-01-01T01:02:03Z"
},
{
"OS-EXT-IMG-SIZE:size": "25165824",
"OS-EXT-IMG-SIZE:size": 25165824,
"created": "2011-01-01T01:02:03Z",
"id": "261b52ed-f693-4147-8f3b-d25df5efd968",
"links": [
@@ -337,4 +337,4 @@
"updated": "2011-01-01T01:02:03Z"
}
]
}
}

View File

@@ -38,20 +38,21 @@ SUPPORTED_FILTERS = {
}
@validation.validated
class ImagesController(wsgi.Controller):
"""Base controller for retrieving/displaying images."""
_view_builder_class = views_images.ViewBuilder
def __init__(self):
super(ImagesController, self).__init__()
super().__init__()
self._image_api = glance.API()
def _get_filters(self, req):
"""Return a dictionary of query param filters from the request.
:param req: the Request object coming from the wsgi layer
:retval a dict of key/value filters
:returns: a dict of key/value filters
"""
filters = {}
for param in req.params:
@@ -77,6 +78,7 @@ class ImagesController(wsgi.Controller):
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(404)
@validation.query_schema(schema.show_query)
@validation.response_body_schema(schema.show_response)
def show(self, req, id):
"""Return detailed information about a specific image.
@@ -96,6 +98,7 @@ class ImagesController(wsgi.Controller):
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors((403, 404))
@wsgi.response(204)
@validation.response_body_schema(schema.delete_response)
def delete(self, req, id):
"""Delete an image, if allowed.
@@ -117,11 +120,11 @@ class ImagesController(wsgi.Controller):
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(400)
@validation.query_schema(schema.index_query)
@validation.response_body_schema(schema.index_response)
def index(self, req):
"""Return an index listing of images available to the request.
:param req: `wsgi.Request` object
"""
context = req.environ['nova.context']
filters = self._get_filters(req)
@@ -137,11 +140,11 @@ class ImagesController(wsgi.Controller):
@wsgi.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
@wsgi.expected_errors(400)
@validation.query_schema(schema.detail_query)
@validation.response_body_schema(schema.detail_response)
def detail(self, req):
"""Return a detailed index listing of images available to the request.
:param req: `wsgi.Request` object.
"""
context = req.environ['nova.context']
filters = self._get_filters(req)

View File

@@ -10,7 +10,10 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
from nova.api.validation import parameter_types
from nova.api.validation import response_types
# NOTE(stephenfin): These schemas are incomplete but won't be enhanced further
# since these APIs have been removed
@@ -50,3 +53,179 @@ index_query = {
}
detail_query = index_query
_links_response = {
'type': 'array',
'prefixItems': [
{
'type': 'object',
'properties': {
'href': {'type': 'string', 'format': 'uri'},
'rel': {'const': 'self'},
},
'required': ['href', 'rel'],
'additionalProperties': False,
},
{
'type': 'object',
'properties': {
'href': {'type': 'string', 'format': 'uri'},
'rel': {'const': 'bookmark'},
},
'required': ['href', 'rel'],
'additionalProperties': False,
},
{
'type': 'object',
'properties': {
'href': {'type': 'string', 'format': 'uri'},
'rel': {'const': 'alternate'},
'type': {'const': 'application/vnd.openstack.image'},
},
'required': ['href', 'rel', 'type'],
'additionalProperties': False,
},
],
'minItems': 3,
'maxItems': 3,
}
_image_response = {
'type': 'object',
'properties': {
'created': {'type': 'string', 'format': 'date-time'},
'id': {'type': 'string', 'format': 'uuid'},
'links': _links_response,
'metadata': {
'type': 'object',
'patternProperties': {
# unlike nova's metadata, glance doesn't have a maximum length
# on property values. Also, while glance serializes all
# non-null values as strings, nova's image API deserializes
# these again, so we can expected practically any primitive
# type here. Listing all these is effectively the same as
# providing an empty schema so we're mainly doing it for the
# benefit of tooling.
'^[a-zA-Z0-9-_:. ]{1,255}$': {
'type': [
'array',
'boolean',
'integer',
'number',
'object',
'string',
'null',
]
},
},
'additionalProperties': False,
},
'minDisk': {'type': 'integer', 'minimum': 0},
'minRam': {'type': 'integer', 'minimum': 0},
'name': {'type': ['string', 'null']},
'progress': {
'type': 'integer',
'enum': [0, 25, 50, 100],
},
'server': {
'type': 'object',
'properties': {
'id': {'type': 'string', 'format': 'uuid'},
'links': {
'type': 'array',
'prefixItems': [
{
'type': 'object',
'properties': {
'href': {'type': 'string', 'format': 'uri'},
'rel': {'const': 'self'},
},
'required': ['href', 'rel'],
'additionalProperties': False,
},
{
'type': 'object',
'properties': {
'href': {'type': 'string', 'format': 'uri'},
'rel': {'const': 'bookmark'},
},
'required': ['href', 'rel'],
'additionalProperties': False,
},
],
'minItems': 2,
'maxItems': 2,
},
},
'required': ['id', 'links'],
'additionalProperties': False,
},
'status': {
'type': 'string',
'enum': ['ACTIVE', 'SAVING', 'DELETED', 'ERROR', 'UNKNOWN'],
},
'updated': {'type': ['string', 'null'], 'format': 'date-time'},
'OS-DCF:diskConfig': {'type': 'string', 'enum': ['AUTO', 'MANUAL']},
'OS-EXT-IMG-SIZE:size': {'type': 'integer'},
},
'required': [
'created',
'id',
'links',
'metadata',
'minDisk',
'minRam',
'name',
'progress',
'status',
'updated',
'OS-EXT-IMG-SIZE:size',
],
'additionalProperties': False,
}
show_response = {
'type': 'object',
'properties': {
'image': copy.deepcopy(_image_response),
},
'required': [],
'additionalProperties': False,
}
delete_response = {'type': 'null'}
index_response = {
'type': 'object',
'properties': {
'images': {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'id': {'type': 'string', 'format': 'uuid'},
'links': _links_response,
'name': {'type': ['string', 'null']},
},
'required': ['id', 'links', 'name'],
'additionalProperties': False,
},
},
'images_links': response_types.collection_links,
},
'required': [],
'additionalProperties': False,
}
detail_response = {
'type': 'object',
'properties': {
'images': {
'type': 'array',
'items': copy.deepcopy(_image_response),
},
'images_links': response_types.collection_links,
},
'required': ['images'],
'additionalProperties': False,
}

View File

@@ -19,7 +19,7 @@ metadata = {
'type': 'object',
'patternProperties': {
'^[a-zA-Z0-9-_:. ]{1,255}$': {
'type': 'string', 'maxLength': 255,
'type': ['string', 'null'], 'maxLength': 255,
}
},
'additionalProperties': False,

View File

@@ -30,7 +30,9 @@ class GlanceFixture(fixtures.Fixture):
# NOTE(justinsb): The OpenStack API can't upload an image?
# So, make sure we've got one..
timestamp = datetime.datetime(2011, 1, 1, 1, 2, 3)
timestamp = datetime.datetime(
2011, 1, 1, 1, 2, 3, tzinfo=datetime.timezone.utc
)
image1 = {
'id': '155d900f-4e14-4e4c-a73d-069cbf4541e6',
@@ -43,7 +45,7 @@ class GlanceFixture(fixtures.Fixture):
'is_public': False,
'container_format': 'raw',
'disk_format': 'raw',
'size': '25165824',
'size': 25165824,
'min_ram': 0,
'min_disk': 0,
'protected': False,
@@ -67,7 +69,7 @@ class GlanceFixture(fixtures.Fixture):
'is_public': True,
'container_format': 'ami',
'disk_format': 'ami',
'size': '58145823',
'size': 58145823,
'min_ram': 0,
'min_disk': 0,
'protected': False,
@@ -90,7 +92,7 @@ class GlanceFixture(fixtures.Fixture):
'is_public': True,
'container_format': 'bare',
'disk_format': 'raw',
'size': '83594576',
'size': 83594576,
'min_ram': 0,
'min_disk': 0,
'protected': False,
@@ -114,7 +116,7 @@ class GlanceFixture(fixtures.Fixture):
'is_public': True,
'container_format': 'ami',
'disk_format': 'ami',
'size': '84035174',
'size': 84035174,
'min_ram': 0,
'min_disk': 0,
'protected': False,
@@ -137,7 +139,7 @@ class GlanceFixture(fixtures.Fixture):
'is_public': True,
'container_format': 'ami',
'disk_format': 'ami',
'size': '26360814',
'size': 26360814,
'min_ram': 0,
'min_disk': 0,
'protected': False,
@@ -160,7 +162,7 @@ class GlanceFixture(fixtures.Fixture):
'is_public': False,
'container_format': 'ova',
'disk_format': 'vhd',
'size': '49163826',
'size': 49163826,
'min_ram': 0,
'min_disk': 0,
'protected': False,
@@ -185,7 +187,7 @@ class GlanceFixture(fixtures.Fixture):
'is_public': False,
'container_format': 'ova',
'disk_format': 'vhd',
'size': '74185822',
'size': 74185822,
'min_ram': 0,
'min_disk': 0,
'protected': False,
@@ -309,7 +311,7 @@ class GlanceFixture(fixtures.Fixture):
# by the caller. This is needed to avoid a KeyError in the
# image-size API.
if 'size' not in image_meta:
image_meta['size'] = None
image_meta['size'] = 74185822
# Similarly, Glance provides the status on the image once it's created
# and this is checked in the compute API when booting a server from
@@ -325,6 +327,13 @@ class GlanceFixture(fixtures.Fixture):
# proxy API by throwing it into the generic "properties" dict.
image_meta.get('properties', {})['owner'] = context.project_id
# Glance would always populate these fields, so we need to ensure we do
# the same
if not image_meta.get('created_at'):
image_meta['created_at'] = self.timestamp
if not image_meta.get('updated_at'):
image_meta['updated_at'] = self.timestamp
self.images[image_id] = image_meta
if data:

View File

@@ -411,7 +411,7 @@ class InstanceHelperMixin:
'is_public': False,
'container_format': 'raw',
'disk_format': 'raw',
'size': '25165824',
'size': 25165824,
'min_ram': 0,
'min_disk': 0,
'protected': False,
@@ -740,8 +740,9 @@ class InstanceHelperMixin:
def _create_server_boot_from_volume(self, image_args=None,
flavor_id=None, networks=None):
bfv_image_id = uuids.bfv_image_uuid
timestamp = datetime.datetime(2011, 1, 1, 1, 2, 3)
timestamp = datetime.datetime(
2011, 1, 1, 1, 2, 3, tzinfo=datetime.timezone.utc
)
image = {
'id': bfv_image_id,
'name': 'fake_image_name',
@@ -752,7 +753,8 @@ class InstanceHelperMixin:
'status': 'active',
'container_format': 'raw',
'disk_format': 'raw',
'min_disk': 0
'min_disk': 0,
'size': 74185822,
}
if image_args:
image.update(image_args)

View File

@@ -111,20 +111,23 @@ class LibvirtDeviceBusMigration(base.ServersTestBase):
'hw_video_model': 'qxl',
'hw_vif_model': 'e1000',
}
timestamp = datetime.datetime(
2011, 1, 1, 1, 2, 3, tzinfo=datetime.timezone.utc
)
self.glance.create(
None,
{
'id': uuids.hw_bus_model_image_uuid,
'name': 'hw_bus_model_image',
'created_at': datetime.datetime(2011, 1, 1, 1, 2, 3),
'updated_at': datetime.datetime(2011, 1, 1, 1, 2, 3),
'created_at': timestamp,
'updated_at': timestamp,
'deleted_at': None,
'deleted': False,
'status': 'active',
'is_public': False,
'container_format': 'bare',
'disk_format': 'qcow2',
'size': '74185822',
'size': 74185822,
'min_ram': 0,
'min_disk': 0,
'protected': False,
@@ -330,20 +333,23 @@ class LibvirtDeviceBusMigration(base.ServersTestBase):
'hw_video_model': 'cirrus',
'hw_vif_model': 'e1000',
}
timestamp = datetime.datetime(
2011, 1, 1, 1, 2, 3, tzinfo=datetime.timezone.utc
)
self.glance.create(
None,
{
'id': uuids.pc_image_uuid,
'name': 'pc_image',
'created_at': datetime.datetime(2011, 1, 1, 1, 2, 3),
'updated_at': datetime.datetime(2011, 1, 1, 1, 2, 3),
'created_at': timestamp,
'updated_at': timestamp,
'deleted_at': None,
'deleted': False,
'status': 'active',
'is_public': False,
'container_format': 'bare',
'disk_format': 'qcow2',
'size': '74185822',
'size': 74185822,
'min_ram': 0,
'min_disk': 0,
'protected': False,

View File

@@ -57,7 +57,9 @@ class RescueServerTestWithDeletedBaseImage(
'nova.virt.libvirt.utils.get_instance_path', fake_path))
def _create_test_images(self):
timestamp = datetime.datetime(2021, 1, 2, 3, 4, 5)
timestamp = datetime.datetime(
2021, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc
)
base_image = {
'id': uuids.base_image,
'name': 'base_image',
@@ -69,7 +71,7 @@ class RescueServerTestWithDeletedBaseImage(
'is_public': False,
'container_format': 'ova',
'disk_format': 'vhd',
'size': '74185822',
'size': 74185822,
'min_ram': 0,
'min_disk': 0,
'protected': False,
@@ -88,7 +90,7 @@ class RescueServerTestWithDeletedBaseImage(
'is_public': False,
'container_format': 'ova',
'disk_format': 'vhd',
'size': '74185822',
'size': 74185822,
'min_ram': 0,
'min_disk': 0,
'protected': False,

View File

@@ -75,7 +75,9 @@ class UEFIServersTest(base.ServersTestBase):
self.assertIn('COMPUTE_SECURITY_UEFI_SECURE_BOOT', traits)
# create a server with UEFI and secure boot
timestamp = datetime.datetime(2021, 1, 2, 3, 4, 5)
timestamp = datetime.datetime(
2021, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc
)
uefi_image = {
'id': uuids.uefi_image,
'name': 'uefi_image',
@@ -87,7 +89,7 @@ class UEFIServersTest(base.ServersTestBase):
'is_public': False,
'container_format': 'ova',
'disk_format': 'vhd',
'size': '74185822',
'size': 74185822,
'min_ram': 0,
'min_disk': 0,
'protected': False,

View File

@@ -30,20 +30,23 @@ class LibvirtVifModelTest(base.ServersTestBase):
CONF.set_default("image_metadata_prefilter", True, group='scheduler')
super().setUp()
timestamp = datetime.datetime(
2011, 1, 1, 1, 2, 3, tzinfo=datetime.timezone.utc
)
self.glance.create(
None,
{
'id': uuids.image_vif_model_igb,
'name': 'image-with-igb',
'created_at': datetime.datetime(2011, 1, 1, 1, 2, 3),
'updated_at': datetime.datetime(2011, 1, 1, 1, 2, 3),
'created_at': timestamp,
'updated_at': timestamp,
'deleted_at': None,
'deleted': False,
'status': 'active',
'is_public': False,
'container_format': 'bare',
'disk_format': 'qcow2',
'size': '74185822',
'size': 74185822,
'min_ram': 0,
'min_disk': 0,
'protected': False,

View File

@@ -44,7 +44,7 @@ class TestServerGet(test.TestCase):
'is_public': False,
'container_format': 'raw',
'disk_format': 'raw',
'size': '25165824',
'size': 25165824,
'properties': {'kernel_id': 'nokernel',
'ramdisk_id': 'nokernel',
'architecture': 'x64'}}

View File

@@ -34,7 +34,9 @@ class TestNonBootableImageMeta(integrated_helpers._IntegratedTestBase):
super().setUp()
# Add an image to the Glance fixture with cinder_encryption_key set
timestamp = datetime.datetime(2011, 1, 1, 1, 2, 3)
timestamp = datetime.datetime(
2011, 1, 1, 1, 2, 3, tzinfo=datetime.timezone.utc
)
cinder_encrypted_image = {
'id': uuids.cinder_encrypted_image_uuid,
'name': 'cinder_encryption_key_image',
@@ -46,7 +48,7 @@ class TestNonBootableImageMeta(integrated_helpers._IntegratedTestBase):
'is_public': False,
'container_format': 'ova',
'disk_format': 'vhd',
'size': '74185822',
'size': 74185822,
'min_ram': 0,
'min_disk': 0,
'protected': False,

View File

@@ -58,7 +58,7 @@ class AggregateImagePropertiesIsolationTestCase(_AggregateTestCase):
'is_public': False,
'container_format': 'raw',
'disk_format': 'raw',
'size': '25165824',
'size': 25165824,
'min_ram': 0,
'min_disk': 0,
'protected': False,

View File

@@ -39,7 +39,7 @@ class BFVRescue(integrated_helpers.ProviderUsageBaseTestCase):
'is_public': False,
'container_format': 'raw',
'disk_format': 'raw',
'size': '25165824',
'size': 25165824,
'min_ram': 0,
'min_disk': 0,
'protected': False,

View File

@@ -111,7 +111,7 @@ class DiskConfigTestCaseV21(test.TestCase):
'is_public': False,
'container_format': 'ova',
'disk_format': 'vhd',
'size': '74185822',
'size': 74185822,
'properties': {'auto_disk_config': 'Disabled'}}
self.image_service.create(None, image)

View File

@@ -68,104 +68,116 @@ class ImagesControllerTestV21(test.NoDBTestCase):
self.server_uuid))
self.alternate = "%s/images/%s"
self.expected_image_123 = {
"image": {'id': '123',
'name': 'public image',
'metadata': {'key1': 'value1'},
'updated': NOW_API_FORMAT,
'created': NOW_API_FORMAT,
'status': 'ACTIVE',
'minDisk': 10,
'progress': 100,
'minRam': 128,
'OS-EXT-IMG-SIZE:size': 25165824,
"links": [{
"rel": "self",
"href": "%s/123" % self.url_prefix
},
{
"rel": "bookmark",
"href":
"%s/123" % self.bookmark_prefix
},
{
"rel": "alternate",
"type": "application/vnd.openstack.image",
"href": self.alternate %
(glance.generate_glance_url('ctx'),
123),
}],
self.image_a_uuid = IMAGE_FIXTURES[0]['id']
self.expected_image_a = {
"image": {
'id': self.image_a_uuid,
'name': 'public image',
'metadata': {'key1': 'value1'},
'updated': NOW_API_FORMAT,
'created': NOW_API_FORMAT,
'status': 'ACTIVE',
'minDisk': 10,
'progress': 100,
'minRam': 128,
'OS-EXT-IMG-SIZE:size': 25165824,
"links": [
{
"rel": "self",
"href": f"{self.url_prefix}/{self.image_a_uuid}"
},
{
"rel": "bookmark",
"href": f"{self.bookmark_prefix}/{self.image_a_uuid}"
},
{
"rel": "alternate",
"type": "application/vnd.openstack.image",
"href": self.alternate % (
glance.generate_glance_url('ctx'),
self.image_a_uuid,
),
}
],
},
}
self.expected_image_124 = {
"image": {'id': '124',
'name': 'queued snapshot',
'metadata': {
u'instance_uuid': self.server_uuid,
u'user_id': u'fake',
},
'updated': NOW_API_FORMAT,
'created': NOW_API_FORMAT,
'status': 'SAVING',
'progress': 25,
'minDisk': 0,
'minRam': 0,
'OS-EXT-IMG-SIZE:size': 25165824,
'server': {
'id': self.server_uuid,
"links": [{
"rel": "self",
"href": self.server_href,
},
{
"rel": "bookmark",
"href": self.server_bookmark,
}],
},
"links": [{
"rel": "self",
"href": "%s/124" % self.url_prefix
},
{
"rel": "bookmark",
"href":
"%s/124" % self.bookmark_prefix
},
{
"rel": "alternate",
"type":
"application/vnd.openstack.image",
"href": self.alternate %
(glance.generate_glance_url('ctx'),
124),
}],
self.image_b_uuid = IMAGE_FIXTURES[1]['id']
self.expected_image_b = {
"image": {
'id': self.image_b_uuid,
'name': 'queued snapshot',
'metadata': {
'instance_uuid': self.server_uuid,
'user_id': 'fake',
},
'updated': NOW_API_FORMAT,
'created': NOW_API_FORMAT,
'status': 'SAVING',
'progress': 25,
'minDisk': 0,
'minRam': 0,
'OS-EXT-IMG-SIZE:size': 25165824,
'server': {
'id': self.server_uuid,
"links": [
{
"rel": "self",
"href": self.server_href,
},
{
"rel": "bookmark",
"href": self.server_bookmark,
}
],
},
"links": [
{
"rel": "self",
"href": f"{self.url_prefix}/{self.image_b_uuid}"
},
{
"rel": "bookmark",
"href": f"{self.bookmark_prefix}/{self.image_b_uuid}"
},
{
"rel": "alternate",
"type": "application/vnd.openstack.image",
"href": self.alternate % (
glance.generate_glance_url('ctx'),
self.image_b_uuid,
),
},
],
},
}
@mock.patch('nova.image.glance.API.get', return_value=IMAGE_FIXTURES[0])
def test_get_image(self, get_mocked):
request = self.http_request.blank(self.url_base + 'images/123')
actual_image = self.controller.show(request, '123')
request = self.http_request.blank(
self.url_base + f'images/{self.image_a_uuid}')
actual_image = self.controller.show(request, self.image_a_uuid)
self.assertThat(actual_image,
matchers.DictMatches(self.expected_image_123))
get_mocked.assert_called_once_with(mock.ANY, '123')
matchers.DictMatches(self.expected_image_a))
get_mocked.assert_called_once_with(mock.ANY, self.image_a_uuid)
@mock.patch('nova.image.glance.API.get', return_value=IMAGE_FIXTURES[1])
def test_get_image_with_custom_prefix(self, _get_mocked):
self.flags(compute_link_prefix='https://zoo.com:42',
glance_link_prefix='http://circus.com:34',
group='api')
fake_req = self.http_request.blank(self.url_base + 'images/124')
actual_image = self.controller.show(fake_req, '124')
fake_req = self.http_request.blank(
self.url_base + f'images/{self.image_b_uuid}')
actual_image = self.controller.show(fake_req, self.image_b_uuid)
expected_image = self.expected_image_124
expected_image = self.expected_image_b
expected_image["image"]["links"][0]["href"] = (
"https://zoo.com:42%s/images/124" % self.url_base)
f"https://zoo.com:42{self.url_base}/images/{self.image_b_uuid}")
expected_image["image"]["links"][1]["href"] = (
"https://zoo.com:42%s/images/124" % self.bookmark_base)
f"https://zoo.com:42{self.bookmark_base}/images/"
f"{self.image_b_uuid}")
expected_image["image"]["links"][2]["href"] = (
"http://circus.com:34/images/124")
f"http://circus.com:34/images/{self.image_b_uuid}")
expected_image["image"]["server"]["links"][0]["href"] = (
"https://zoo.com:42%s/servers/%s" % (self.url_base,
self.server_uuid))
@@ -190,82 +202,96 @@ class ImagesControllerTestV21(test.NoDBTestCase):
get_all_mocked.assert_called_once_with(mock.ANY, filters={})
response_list = response["images"]
image_125 = copy.deepcopy(self.expected_image_124["image"])
image_125['id'] = '125'
image_125['name'] = 'saving snapshot'
image_125['progress'] = 50
image_125["links"][0]["href"] = "%s/125" % self.url_prefix
image_125["links"][1]["href"] = "%s/125" % self.bookmark_prefix
image_125["links"][2]["href"] = (
"%s/images/125" % glance.generate_glance_url('ctx'))
image_c = copy.deepcopy(self.expected_image_b["image"])
image_c['id'] = IMAGE_FIXTURES[2]['id']
image_c['name'] = 'saving snapshot'
image_c['progress'] = 50
image_c["links"][0]["href"] = "%s/%s" % (
self.url_prefix, IMAGE_FIXTURES[2]['id'])
image_c["links"][1]["href"] = "%s/%s" % (
self.bookmark_prefix, IMAGE_FIXTURES[2]['id'])
image_c["links"][2]["href"] = "%s/images/%s" % (
glance.generate_glance_url('ctx'), IMAGE_FIXTURES[2]['id'])
image_126 = copy.deepcopy(self.expected_image_124["image"])
image_126['id'] = '126'
image_126['name'] = 'active snapshot'
image_126['status'] = 'ACTIVE'
image_126['progress'] = 100
image_126["links"][0]["href"] = "%s/126" % self.url_prefix
image_126["links"][1]["href"] = "%s/126" % self.bookmark_prefix
image_126["links"][2]["href"] = (
"%s/images/126" % glance.generate_glance_url('ctx'))
image_d = copy.deepcopy(self.expected_image_b["image"])
image_d['id'] = IMAGE_FIXTURES[3]['id']
image_d['name'] = 'active snapshot'
image_d['status'] = 'ACTIVE'
image_d['progress'] = 100
image_d["links"][0]["href"] = "%s/%s" % (
self.url_prefix, IMAGE_FIXTURES[3]['id'])
image_d["links"][1]["href"] = "%s/%s" % (
self.bookmark_prefix, IMAGE_FIXTURES[3]['id'])
image_d["links"][2]["href"] = "%s/images/%s" % (
glance.generate_glance_url('ctx'), IMAGE_FIXTURES[3]['id'])
image_127 = copy.deepcopy(self.expected_image_124["image"])
image_127['id'] = '127'
image_127['name'] = 'killed snapshot'
image_127['status'] = 'ERROR'
image_127['progress'] = 0
image_127["links"][0]["href"] = "%s/127" % self.url_prefix
image_127["links"][1]["href"] = "%s/127" % self.bookmark_prefix
image_127["links"][2]["href"] = (
"%s/images/127" % glance.generate_glance_url('ctx'))
image_e = copy.deepcopy(self.expected_image_b["image"])
image_e['id'] = IMAGE_FIXTURES[4]['id']
image_e['name'] = 'killed snapshot'
image_e['status'] = 'ERROR'
image_e['progress'] = 0
image_e["links"][0]["href"] = "%s/%s" % (
self.url_prefix, IMAGE_FIXTURES[4]['id'])
image_e["links"][1]["href"] = "%s/%s" % (
self.bookmark_prefix, IMAGE_FIXTURES[4]['id'])
image_e["links"][2]["href"] = "%s/images/%s" % (
glance.generate_glance_url('ctx'), IMAGE_FIXTURES[4]['id'])
image_128 = copy.deepcopy(self.expected_image_124["image"])
image_128['id'] = '128'
image_128['name'] = 'deleted snapshot'
image_128['status'] = 'DELETED'
image_128['progress'] = 0
image_128["links"][0]["href"] = "%s/128" % self.url_prefix
image_128["links"][1]["href"] = "%s/128" % self.bookmark_prefix
image_128["links"][2]["href"] = (
"%s/images/128" % glance.generate_glance_url('ctx'))
image_f = copy.deepcopy(self.expected_image_b["image"])
image_f['id'] = IMAGE_FIXTURES[5]['id']
image_f['name'] = 'deleted snapshot'
image_f['status'] = 'DELETED'
image_f['progress'] = 0
image_f["links"][0]["href"] = "%s/%s" % (
self.url_prefix, IMAGE_FIXTURES[5]['id'])
image_f["links"][1]["href"] = "%s/%s" % (
self.bookmark_prefix, IMAGE_FIXTURES[5]['id'])
image_f["links"][2]["href"] = "%s/images/%s" % (
glance.generate_glance_url('ctx'), IMAGE_FIXTURES[5]['id'])
image_129 = copy.deepcopy(self.expected_image_124["image"])
image_129['id'] = '129'
image_129['name'] = 'pending_delete snapshot'
image_129['status'] = 'DELETED'
image_129['progress'] = 0
image_129["links"][0]["href"] = "%s/129" % self.url_prefix
image_129["links"][1]["href"] = "%s/129" % self.bookmark_prefix
image_129["links"][2]["href"] = (
"%s/images/129" % glance.generate_glance_url('ctx'))
image_g = copy.deepcopy(self.expected_image_b["image"])
image_g['id'] = IMAGE_FIXTURES[6]['id']
image_g['name'] = 'pending_delete snapshot'
image_g['status'] = 'DELETED'
image_g['progress'] = 0
image_g["links"][0]["href"] = "%s/%s" % (
self.url_prefix, IMAGE_FIXTURES[6]['id'])
image_g["links"][1]["href"] = "%s/%s" % (
self.bookmark_prefix, IMAGE_FIXTURES[6]['id'])
image_g["links"][2]["href"] = "%s/images/%s" % (
glance.generate_glance_url('ctx'), IMAGE_FIXTURES[6]['id'])
image_130 = copy.deepcopy(self.expected_image_123["image"])
image_130['id'] = '130'
image_130['name'] = None
image_130['metadata'] = {}
image_130['minDisk'] = 0
image_130['minRam'] = 0
image_130["links"][0]["href"] = "%s/130" % self.url_prefix
image_130["links"][1]["href"] = "%s/130" % self.bookmark_prefix
image_130["links"][2]["href"] = (
"%s/images/130" % glance.generate_glance_url('ctx'))
image_h = copy.deepcopy(self.expected_image_a["image"])
image_h['id'] = IMAGE_FIXTURES[7]['id']
image_h['name'] = None
image_h['metadata'] = {}
image_h['minDisk'] = 0
image_h['minRam'] = 0
image_h["links"][0]["href"] = "%s/%s" % (
self.url_prefix, IMAGE_FIXTURES[7]['id'])
image_h["links"][1]["href"] = "%s/%s" % (
self.bookmark_prefix, IMAGE_FIXTURES[7]['id'])
image_h["links"][2]["href"] = "%s/images/%s" % (
glance.generate_glance_url('ctx'), IMAGE_FIXTURES[7]['id'])
image_131 = copy.deepcopy(self.expected_image_123["image"])
image_131['id'] = '131'
image_131['name'] = None
image_131['metadata'] = {}
image_131['minDisk'] = 0
image_131['minRam'] = 0
image_131["links"][0]["href"] = "%s/131" % self.url_prefix
image_131["links"][1]["href"] = "%s/131" % self.bookmark_prefix
image_131["links"][2]["href"] = (
"%s/images/131" % glance.generate_glance_url('ctx'))
image_i = copy.deepcopy(self.expected_image_a["image"])
image_i['id'] = IMAGE_FIXTURES[8]['id']
image_i['name'] = None
image_i['metadata'] = {}
image_i['minDisk'] = 0
image_i['minRam'] = 0
image_i["links"][0]["href"] = "%s/%s" % (
self.url_prefix, IMAGE_FIXTURES[8]['id'])
image_i["links"][1]["href"] = "%s/%s" % (
self.bookmark_prefix, IMAGE_FIXTURES[8]['id'])
image_i["links"][2]["href"] = "%s/images/%s" % (
glance.generate_glance_url('ctx'), IMAGE_FIXTURES[8]['id'])
expected = [self.expected_image_123["image"],
self.expected_image_124["image"],
image_125, image_126, image_127,
image_128, image_129, image_130,
image_131]
expected = [self.expected_image_a["image"],
self.expected_image_b["image"],
image_c, image_d, image_e,
image_f, image_g, image_h,
image_i]
self.assertThat(expected, matchers.DictListMatches(response_list))
@@ -358,29 +384,37 @@ class ImagesControllerTestV21(test.NoDBTestCase):
@mock.patch('nova.image.glance.API.delete')
def test_delete_image(self, delete_mocked):
request = self.http_request.blank(self.url_base + 'images/124')
request = self.http_request.blank(
self.url_base + f'images/{self.image_a_uuid}')
request.method = 'DELETE'
delete_method = self.controller.delete
delete_method(request, '124')
delete_method(request, self.image_a_uuid)
self.assertEqual(204, delete_method.wsgi_codes(request))
delete_mocked.assert_called_once_with(mock.ANY, '124')
delete_mocked.assert_called_once_with(mock.ANY, self.image_a_uuid)
@mock.patch('nova.image.glance.API.delete',
side_effect=exception.ImageNotAuthorized(image_id='123'))
def test_delete_deleted_image(self, _delete_mocked):
def test_delete_deleted_image(self):
# If you try to delete a deleted image, you get back 403 Forbidden.
request = self.http_request.blank(self.url_base + 'images/123')
request = self.http_request.blank(
self.url_base + f'images/{self.image_a_uuid}')
request.method = 'DELETE'
self.assertRaises(webob.exc.HTTPForbidden, self.controller.delete,
request, '123')
with mock.patch(
'nova.image.glance.API.delete',
side_effect=exception.ImageNotAuthorized(
image_id=self.image_a_uuid
)
):
self.assertRaises(webob.exc.HTTPForbidden, self.controller.delete,
request, self.image_a_uuid)
@mock.patch('nova.image.glance.API.delete',
side_effect=exception.ImageNotFound(image_id='123'))
def test_delete_image_not_found(self, _delete_mocked):
def test_delete_image_not_found(self):
request = self.http_request.blank(self.url_base + 'images/300')
request.method = 'DELETE'
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.delete, request, '300')
with mock.patch(
'nova.image.glance.API.delete',
side_effect=exception.ImageNotFound(image_id='300')
):
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.delete, request, '300')
@mock.patch('nova.image.glance.API.get_all',
return_value=[IMAGE_FIXTURES[0]])

View File

@@ -12,6 +12,8 @@
import datetime
from oslo_utils import uuidutils
# nova.image.glance._translate_from_glance() returns datetime
# objects, not strings.
NOW_DATE = datetime.datetime(2010, 10, 11, 10, 30, 22)
@@ -22,25 +24,22 @@ def get_image_fixtures():
Returns a set of dicts representing images/snapshots of varying statuses
that would be returned from a call to
`glanceclient.client.Client.images.list`. The IDs of the images returned
start at 123 and go to 131, with the following brief summary of image
attributes:
`glanceclient.client.Client.images.list`. The IDs of the images are random,
with the following brief summary of image attributes:
| ID Type Status Notes
| # Type Status Notes
| ----------------------------------------------------------
| 123 Public image active
| 124 Snapshot queued
| 125 Snapshot saving
| 126 Snapshot active
| 127 Snapshot killed
| 128 Snapshot deleted
| 129 Snapshot pending_delete
| 130 Public image active Has no name
| 0 Public image active
| 1 Snapshot queued
| 2 Snapshot saving
| 3 Snapshot active
| 4 Snapshot killed
| 5 Snapshot deleted
| 6 Snapshot pending_delete
| 7 Public image active Has no name
"""
image_id = 123
fixtures = []
def add_fixture(**kwargs):
@@ -49,10 +48,10 @@ def get_image_fixtures():
fixtures.append(kwargs)
# Public image
image_id = uuidutils.generate_uuid()
add_fixture(id=str(image_id), name='public image', is_public=True,
status='active', properties={'key1': 'value1'},
min_ram="128", min_disk="10", size=25165824)
image_id += 1
# Snapshot for User 1
uuid = 'aa640691-d1a7-4a67-9d3c-d35ee6b3cc74'
@@ -62,17 +61,18 @@ def get_image_fixtures():
deleted = False if status != 'deleted' else True
deleted_at = NOW_DATE if deleted else None
image_id = uuidutils.generate_uuid()
add_fixture(id=str(image_id), name='%s snapshot' % status,
is_public=False, status=status,
properties=snapshot_properties, size=25165824,
deleted=deleted, deleted_at=deleted_at)
image_id += 1
# Image without a name
image_id = uuidutils.generate_uuid()
add_fixture(id=str(image_id), is_public=True, status='active',
properties={}, size=25165824)
# Image for permission tests
image_id += 1
image_id = uuidutils.generate_uuid()
add_fixture(id=str(image_id), is_public=True, status='active',
properties={}, owner='authorized_fake', size=25165824)