diff --git a/doc/api_samples/images/image-get-resp.json b/doc/api_samples/images/image-get-resp.json index 79978776c3e6..ef652fc64444 100644 --- a/doc/api_samples/images/image-get-resp.json +++ b/doc/api_samples/images/image-get-resp.json @@ -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": [ diff --git a/doc/api_samples/images/images-details-get-resp.json b/doc/api_samples/images/images-details-get-resp.json index 33cf667287f2..71f573fd1c29 100644 --- a/doc/api_samples/images/images-details-get-resp.json +++ b/doc/api_samples/images/images-details-get-resp.json @@ -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" } ] -} \ No newline at end of file +} diff --git a/nova/api/openstack/compute/images.py b/nova/api/openstack/compute/images.py index f6b78ce04dae..6192392cd367 100644 --- a/nova/api/openstack/compute/images.py +++ b/nova/api/openstack/compute/images.py @@ -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) diff --git a/nova/api/openstack/compute/schemas/images.py b/nova/api/openstack/compute/schemas/images.py index 978e694c6706..3eab2a55cc9b 100644 --- a/nova/api/openstack/compute/schemas/images.py +++ b/nova/api/openstack/compute/schemas/images.py @@ -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, +} diff --git a/nova/api/validation/response_types.py b/nova/api/validation/response_types.py index 11b395527312..efcd47252aaf 100644 --- a/nova/api/validation/response_types.py +++ b/nova/api/validation/response_types.py @@ -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, diff --git a/nova/tests/fixtures/glance.py b/nova/tests/fixtures/glance.py index b1124036d7c2..7589d0d09c21 100644 --- a/nova/tests/fixtures/glance.py +++ b/nova/tests/fixtures/glance.py @@ -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: diff --git a/nova/tests/functional/integrated_helpers.py b/nova/tests/functional/integrated_helpers.py index 65f84226195e..60a424cd507d 100644 --- a/nova/tests/functional/integrated_helpers.py +++ b/nova/tests/functional/integrated_helpers.py @@ -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) diff --git a/nova/tests/functional/libvirt/test_device_bus_migration.py b/nova/tests/functional/libvirt/test_device_bus_migration.py index 3852e31c68b5..89d727306684 100644 --- a/nova/tests/functional/libvirt/test_device_bus_migration.py +++ b/nova/tests/functional/libvirt/test_device_bus_migration.py @@ -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, diff --git a/nova/tests/functional/libvirt/test_rescue_deleted_base.py b/nova/tests/functional/libvirt/test_rescue_deleted_base.py index 437106711ef0..c2fba5107836 100644 --- a/nova/tests/functional/libvirt/test_rescue_deleted_base.py +++ b/nova/tests/functional/libvirt/test_rescue_deleted_base.py @@ -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, diff --git a/nova/tests/functional/libvirt/test_uefi.py b/nova/tests/functional/libvirt/test_uefi.py index 40becf425ed1..628acb6df8b0 100644 --- a/nova/tests/functional/libvirt/test_uefi.py +++ b/nova/tests/functional/libvirt/test_uefi.py @@ -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, diff --git a/nova/tests/functional/libvirt/test_vif_model.py b/nova/tests/functional/libvirt/test_vif_model.py index 277842986ae5..b9d237917247 100644 --- a/nova/tests/functional/libvirt/test_vif_model.py +++ b/nova/tests/functional/libvirt/test_vif_model.py @@ -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, diff --git a/nova/tests/functional/regressions/test_bug_1558866.py b/nova/tests/functional/regressions/test_bug_1558866.py index 868a3a8060b7..9124dde713cc 100644 --- a/nova/tests/functional/regressions/test_bug_1558866.py +++ b/nova/tests/functional/regressions/test_bug_1558866.py @@ -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'}} diff --git a/nova/tests/functional/regressions/test_bug_1895696.py b/nova/tests/functional/regressions/test_bug_1895696.py index 8f16b7b56ed8..5e6d6009b7a8 100644 --- a/nova/tests/functional/regressions/test_bug_1895696.py +++ b/nova/tests/functional/regressions/test_bug_1895696.py @@ -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, diff --git a/nova/tests/functional/test_scheduler.py b/nova/tests/functional/test_scheduler.py index 2f3f6782f6ed..2154d5e4d30c 100644 --- a/nova/tests/functional/test_scheduler.py +++ b/nova/tests/functional/test_scheduler.py @@ -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, diff --git a/nova/tests/functional/test_server_rescue.py b/nova/tests/functional/test_server_rescue.py index 8f5b9129437c..551d700df316 100644 --- a/nova/tests/functional/test_server_rescue.py +++ b/nova/tests/functional/test_server_rescue.py @@ -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, diff --git a/nova/tests/unit/api/openstack/compute/test_disk_config.py b/nova/tests/unit/api/openstack/compute/test_disk_config.py index c5ee59722a7b..0345182784e3 100644 --- a/nova/tests/unit/api/openstack/compute/test_disk_config.py +++ b/nova/tests/unit/api/openstack/compute/test_disk_config.py @@ -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) diff --git a/nova/tests/unit/api/openstack/compute/test_images.py b/nova/tests/unit/api/openstack/compute/test_images.py index 9ab9265a1671..dd4c97ad68d6 100644 --- a/nova/tests/unit/api/openstack/compute/test_images.py +++ b/nova/tests/unit/api/openstack/compute/test_images.py @@ -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]]) diff --git a/nova/tests/unit/image_fixtures.py b/nova/tests/unit/image_fixtures.py index 76cf1a21fb25..8aeba2e50f77 100644 --- a/nova/tests/unit/image_fixtures.py +++ b/nova/tests/unit/image_fixtures.py @@ -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)