# Copyright 2012 OpenStack Foundation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import errno import hashlib import testtools from unittest import mock import ddt from glanceclient.common import utils as common_utils from glanceclient import exc from glanceclient.tests.unit.v2 import base from glanceclient.tests import utils from glanceclient.v2 import images _CHKSUM = '93264c3edf5972c9f1cb309543d38a5c' _CHKSUM1 = '54264c3edf5972c9f1cb309453d38a46' _HASHVAL = '54264c3edf93264c3edf5972c9f1cb309543d38a5c5972c9f1cb309453d38a46' _HASHVAL1 = 'cb309543d38a5c5972c9f1cb309453d38a4654264c3edf93264c3edf5972c9f1' _HASHBAD = '93264c3edf597254264c3edf5972c9f1cb309453d38a46c9f1cb309543d38a5c' _TAG1 = 'power' _TAG2 = '64bit' _BOGUS_ID = '63e7f218-29de-4477-abdc-8db7c9533188' _EVERYTHING_ID = '802cbbb7-0379-4c38-853f-37302b5e3d29' _OWNED_IMAGE_ID = 'a4963502-acc7-42ba-ad60-5aa0962b7faf' _OWNER_ID = '6bd473f0-79ae-40ad-a927-e07ec37b642f' _PRIVATE_ID = 'e33560a7-3964-4de5-8339-5a24559f99ab' _PUBLIC_ID = '857806e7-05b6-48e0-9d40-cb0e6fb727b9' _SHARED_ID = '331ac905-2a38-44c5-a83d-653db8f08313' _COMMUNITY_ID = '609ec9fc-0ee4-44c4-854d-0480af576929' _STATUS_REJECTED_ID = 'f3ea56ff-d7e4-4451-998c-1e3d33539c8e' data_fixtures = { '/v2/schemas/image': { 'GET': ( {}, { 'name': 'image', 'properties': { 'id': {}, 'name': {}, 'locations': { 'type': 'array', 'items': { 'type': 'object', 'properties': { 'metadata': {'type': 'object'}, 'url': {'type': 'string'}, }, 'required': ['url', 'metadata'], }, }, 'color': {'type': 'string', 'is_base': False}, }, 'additionalProperties': {'type': 'string'}, }, ), }, '/v2/images?limit=%d' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ]}, ), }, '/v2/images?limit=2': { 'GET': ( {}, { 'images': [ { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ], 'next': ('/v2/images?limit=2&' 'marker=6f99bf80-2ee6-47cf-acfe-1f1fabb7e810'), }, ), }, '/v2/images?limit=1': { 'GET': ( {}, { 'images': [ { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ], 'next': ('/v2/images?limit=1&' 'marker=3a4560a1-e585-443e-9b39-553b46ec92d1'), }, ), }, ('/v2/images?limit=1&marker=3a4560a1-e585-443e-9b39-553b46ec92d1'): { 'GET': ( {}, {'images': [ { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ]}, ), }, ('/v2/images?limit=1&marker=6f99bf80-2ee6-47cf-acfe-1f1fabb7e810'): { 'GET': ( {}, {'images': [ { 'id': '3f99bf80-2ee6-47cf-acfe-1f1fabb7e811', 'name': 'image-3', }, ]}, ), }, '/v2/images/3a4560a1-e585-443e-9b39-553b46ec92d1': { 'GET': ( {}, { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ), 'PATCH': ( {}, '', ), }, '/v2/images/e7e59ff6-fa2e-4075-87d3-1a1398a07dc3': { 'GET': ( {}, { 'id': 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3', 'name': 'image-3', 'barney': 'rubble', 'george': 'jetson', 'color': 'red', }, ), 'PATCH': ( {}, '', ), }, '/v2/images': { 'POST': ( {}, { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ), }, '/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a': { 'DELETE': ( {}, { 'id': '87b634c1-f893-33c9-28a9-e5673c99239a', }, ), }, '/v2/images/606b0e88-7c5a-4d54-b5bb-046105d4de6f/file': { 'PUT': ( {}, '', ), }, '/v2/images/5cc4bebc-db27-11e1-a1eb-080027cbe205/file': { 'GET': ( {}, 'A', ), }, '/v2/images/5cc4bebc-db27-11e1-a1eb-080027cbe205': { 'GET': ( {}, {}, ), }, '/v2/images/headeronly-db27-11e1-a1eb-080027cbe205/file': { 'GET': ( { 'content-md5': 'wrong' }, 'BB', ), }, '/v2/images/headeronly-db27-11e1-a1eb-080027cbe205': { 'GET': ( {}, {}, ), }, '/v2/images/chkonly-db27-11e1-a1eb-080027cbe205/file': { 'GET': ( { 'content-md5': 'wrong' }, 'BB', ), }, '/v2/images/chkonly-db27-11e1-a1eb-080027cbe205': { 'GET': ( {}, { 'checksum': 'wrong', }, ), }, '/v2/images/multihash-db27-11e1-a1eb-080027cbe205/file': { 'GET': ( { 'content-md5': 'wrong' }, 'BB', ), }, '/v2/images/multihash-db27-11e1-a1eb-080027cbe205': { 'GET': ( {}, { 'checksum': 'wrong', 'os_hash_algo': 'md5', 'os_hash_value': 'junk' }, ), }, '/v2/images/badalgo-db27-11e1-a1eb-080027cbe205/file': { 'GET': ( { 'content-md5': hashlib.md5(b'BB').hexdigest() }, 'BB', ), }, '/v2/images/badalgo-db27-11e1-a1eb-080027cbe205': { 'GET': ( {}, { 'checksum': hashlib.md5(b'BB').hexdigest(), 'os_hash_algo': 'not_an_algo', 'os_hash_value': 'whatever' }, ), }, '/v2/images/bad-multihash-value-good-checksum/file': { 'GET': ( { 'content-md5': hashlib.md5(b'GOODCHECKSUM').hexdigest() }, 'GOODCHECKSUM', ), }, '/v2/images/bad-multihash-value-good-checksum': { 'GET': ( {}, { 'checksum': hashlib.md5(b'GOODCHECKSUM').hexdigest(), 'os_hash_algo': 'sha512', 'os_hash_value': 'badmultihashvalue' }, ), }, '/v2/images/headeronly-dd57-11e1-af0f-02163e68b1d8/file': { 'GET': ( { 'content-md5': 'defb99e69a9f1f6e06f15006b1f166ae' }, 'CCC', ), }, '/v2/images/headeronly-dd57-11e1-af0f-02163e68b1d8': { 'GET': ( {}, {}, ), }, '/v2/images/chkonly-dd57-11e1-af0f-02163e68b1d8/file': { 'GET': ( { 'content-md5': 'defb99e69a9f1f6e06f15006b1f166ae' }, 'CCC', ), }, '/v2/images/chkonly-dd57-11e1-af0f-02163e68b1d8': { 'GET': ( {}, { 'checksum': 'defb99e69a9f1f6e06f15006b1f166ae', }, ), }, '/v2/images/multihash-dd57-11e1-af0f-02163e68b1d8/file': { 'GET': ( { 'content-md5': 'defb99e69a9f1f6e06f15006b1f166ae' }, 'CCC', ), }, '/v2/images/multihash-dd57-11e1-af0f-02163e68b1d8': { 'GET': ( {}, { 'checksum': 'defb99e69a9f1f6e06f15006b1f166ae', 'os_hash_algo': 'sha384', 'os_hash_value': hashlib.sha384(b'CCC').hexdigest() }, ), }, '/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a/actions/reactivate': { 'POST': ({}, None) }, '/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a/actions/deactivate': { 'POST': ({}, None) }, '/v2/images/606b0e88-7c5a-4d54-b5bb-046105d4de6f/import': { 'POST': ({}, None) }, '/v2/images?limit=%d&visibility=public' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': _PUBLIC_ID, 'harvey': 'lipshitz', }, ]}, ), }, '/v2/images?limit=%d&visibility=private' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': _PRIVATE_ID, }, ]}, ), }, '/v2/images?limit=%d&visibility=shared' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': _SHARED_ID, }, ]}, ), }, '/v2/images?limit=%d&visibility=community' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': _COMMUNITY_ID, }, ]}, ), }, '/v2/images?limit=%d&member_status=rejected' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': _STATUS_REJECTED_ID, }, ]}, ), }, '/v2/images?limit=%d&member_status=pending' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': []}, ), }, '/v2/images?limit=%d&owner=%s' % (images.DEFAULT_PAGE_SIZE, _OWNER_ID): { 'GET': ( {}, {'images': [ { 'id': _OWNED_IMAGE_ID, }, ]}, ), }, '/v2/images?limit=%d&owner=%s' % (images.DEFAULT_PAGE_SIZE, _BOGUS_ID): { 'GET': ( {}, {'images': []}, ), }, '/v2/images?limit=%d&member_status=pending&owner=%s&visibility=shared' % (images.DEFAULT_PAGE_SIZE, _BOGUS_ID): { 'GET': ( {}, {'images': [ { 'id': _EVERYTHING_ID, }, ]}, ), }, '/v2/images?checksum=%s&limit=%d' % (_CHKSUM, images.DEFAULT_PAGE_SIZE): { 'GET': ( {}, {'images': [ { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', } ]}, ), }, '/v2/images?checksum=%s&limit=%d' % (_CHKSUM1, images.DEFAULT_PAGE_SIZE): { 'GET': ( {}, {'images': [ { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ]}, ), }, '/v2/images?checksum=wrong&limit=%d' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': []}, ), }, '/v2/images?limit=%d&os_hash_value=%s' % (images.DEFAULT_PAGE_SIZE, _HASHVAL): { 'GET': ( {}, {'images': [ { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', } ]}, ), }, '/v2/images?limit=%d&os_hash_value=%s' % (images.DEFAULT_PAGE_SIZE, _HASHVAL1): { 'GET': ( {}, {'images': [ { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ]}, ), }, '/v2/images?limit=%d&os_hash_value=%s' % (images.DEFAULT_PAGE_SIZE, _HASHBAD): { 'GET': ( {}, {'images': []}, ), }, '/v2/images?limit=%d&tag=%s' % (images.DEFAULT_PAGE_SIZE, _TAG1): { 'GET': ( {}, {'images': [ { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', } ]}, ), }, '/v2/images?limit=%d&tag=%s' % (images.DEFAULT_PAGE_SIZE, _TAG2): { 'GET': ( {}, {'images': [ { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ]}, ), }, '/v2/images?limit=%d&tag=%s&tag=%s' % (images.DEFAULT_PAGE_SIZE, _TAG1, _TAG2): { 'GET': ( {}, {'images': [ { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', } ]}, ), }, '/v2/images?limit=%d&tag=fake' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': []}, ), }, '/v2/images/a2b83adc-888e-11e3-8872-78acc0b951d8': { 'GET': ( {}, { 'id': 'a2b83adc-888e-11e3-8872-78acc0b951d8', 'name': 'image-location-tests', 'locations': [{'url': 'http://foo.com/', 'metadata': {'foo': 'foometa'}}, {'url': 'http://bar.com/', 'metadata': {'bar': 'barmeta'}}], }, ), 'PATCH': ( {}, '', ) }, '/v2/images?limit=%d&os_distro=NixOS' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '8b052954-c76c-4e02-8e90-be89a70183a8', 'name': 'image-5', 'os_distro': 'NixOS', }, ]}, ), }, '/v2/images?limit=%d&my_little_property=cant_be_this_cute' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': []}, ), }, '/v2/images?limit=%d&sort_key=name' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ]}, ), }, '/v2/images?limit=%d&sort_key=name&sort_key=id' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image', }, ]}, ), }, '/v2/images?limit=%d&sort_dir=desc&sort_key=id' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ]}, ), }, '/v2/images?limit=%d&sort_dir=desc&sort_key=name&sort_key=id' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ]}, ), }, '/v2/images?limit=%d&sort_dir=desc&sort_dir=asc&sort_key=name&sort_key=id' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ]}, ), }, '/v2/images?limit=%d&sort=name%%3Adesc%%2Csize%%3Aasc' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ]}, ), }, '/v2/images/3a4560a1-e585-443e-9b39-553b46ec92d1/tasks': { 'GET': ( {}, {'tasks': [ { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'status': 'succeed', 'message': 'Copied 44 MiB', 'updated_at': '2021-03-01T18:28:26.000000' } ]}, ), }, } schema_fixtures = { 'image': { 'GET': ( {}, { 'name': 'image', 'properties': { 'id': {}, 'name': {}, 'locations': { 'type': 'array', 'items': { 'type': 'object', 'properties': { 'metadata': {'type': 'object'}, 'url': {'type': 'string'}, }, 'required': ['url', 'metadata'], } }, 'color': {'type': 'string', 'is_base': False}, 'tags': {'type': 'array'}, }, 'additionalProperties': {'type': 'string'}, } ) } } @ddt.ddt class TestController(testtools.TestCase): def setUp(self): super(TestController, self).setUp() self.api = utils.FakeAPI(data_fixtures) self.schema_api = utils.FakeSchemaAPI(schema_fixtures) self.controller = base.BaseController(self.api, self.schema_api, images.Controller) def test_image_tasks_supported(self): with mock.patch.object(common_utils, 'has_version') as mock_has_version: mock_has_version.return_value = True image_tasks = self.controller.get_associated_image_tasks( '3a4560a1-e585-443e-9b39-553b46ec92d1') self.assertEqual(1, len(image_tasks['tasks'])) def test_image_tasks_not_supported(self): with mock.patch.object(common_utils, 'has_version') as mock_has_version: mock_has_version.return_value = False self.assertRaises(exc.HTTPNotImplemented, self.controller.get_associated_image_tasks, '3a4560a1-e585-443e-9b39-553b46ec92d1') def test_list_images(self): images = self.controller.list() self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', images[0].id) self.assertEqual('image-1', images[0].name) self.assertEqual('6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', images[1].id) self.assertEqual('image-2', images[1].name) def test_list_images_paginated(self): images = self.controller.list(page_size=1) self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', images[0].id) self.assertEqual('image-1', images[0].name) self.assertEqual('6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', images[1].id) self.assertEqual('image-2', images[1].name) def test_list_images_paginated_with_limit(self): images = self.controller.list(limit=3, page_size=2) self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', images[0].id) self.assertEqual('image-1', images[0].name) self.assertEqual('6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', images[1].id) self.assertEqual('image-2', images[1].name) self.assertEqual('3f99bf80-2ee6-47cf-acfe-1f1fabb7e811', images[2].id) self.assertEqual('image-3', images[2].name) self.assertEqual(3, len(images)) def test_list_images_with_marker(self): images = self.controller.list( limit=1, marker='3a4560a1-e585-443e-9b39-553b46ec92d1') self.assertEqual('6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', images[0].id) self.assertEqual('image-2', images[0].name) def test_list_images_visibility_public(self): filters = {'filters': {'visibility': 'public'}} images = self.controller.list(**filters) self.assertEqual(_PUBLIC_ID, images[0].id) def test_list_images_visibility_private(self): filters = {'filters': {'visibility': 'private'}} images = self.controller.list(**filters) self.assertEqual(_PRIVATE_ID, images[0].id) def test_list_images_visibility_shared(self): filters = {'filters': {'visibility': 'shared'}} images = self.controller.list(**filters) self.assertEqual(_SHARED_ID, images[0].id) def test_list_images_visibility_community(self): filters = {'filters': {'visibility': 'community'}} images = list(self.controller.list(**filters)) self.assertEqual(_COMMUNITY_ID, images[0].id) def test_list_images_member_status_rejected(self): filters = {'filters': {'member_status': 'rejected'}} images = self.controller.list(**filters) self.assertEqual(_STATUS_REJECTED_ID, images[0].id) def test_list_images_for_owner(self): filters = {'filters': {'owner': _OWNER_ID}} images = self.controller.list(**filters) self.assertEqual(_OWNED_IMAGE_ID, images[0].id) def test_list_images_for_checksum_single_image(self): fake_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' filters = {'filters': {'checksum': _CHKSUM}} images = self.controller.list(**filters) self.assertEqual(1, len(images)) self.assertEqual('%s' % fake_id, images[0].id) def test_list_images_for_checksum_multiple_images(self): fake_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' fake_id2 = '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810' filters = {'filters': {'checksum': _CHKSUM1}} images = self.controller.list(**filters) self.assertEqual(2, len(images)) self.assertEqual('%s' % fake_id1, images[0].id) self.assertEqual('%s' % fake_id2, images[1].id) def test_list_images_for_wrong_checksum(self): filters = {'filters': {'checksum': 'wrong'}} images = self.controller.list(**filters) self.assertEqual(0, len(images)) def test_list_images_for_hash_single_image(self): fake_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' filters = {'filters': {'os_hash_value': _HASHVAL}} images = self.controller.list(**filters) self.assertEqual(1, len(images)) self.assertEqual('%s' % fake_id, images[0].id) def test_list_images_for_hash_multiple_images(self): fake_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' fake_id2 = '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810' filters = {'filters': {'os_hash_value': _HASHVAL1}} images = self.controller.list(**filters) self.assertEqual(2, len(images)) self.assertEqual('%s' % fake_id1, images[0].id) self.assertEqual('%s' % fake_id2, images[1].id) def test_list_images_for_wrong_hash(self): filters = {'filters': {'os_hash_value': _HASHBAD}} images = self.controller.list(**filters) self.assertEqual(0, len(images)) def test_list_images_for_bogus_owner(self): filters = {'filters': {'owner': _BOGUS_ID}} images = self.controller.list(**filters) self.assertEqual([], images) def test_list_images_for_bunch_of_filters(self): filters = {'filters': {'owner': _BOGUS_ID, 'visibility': 'shared', 'member_status': 'pending'}} images = self.controller.list(**filters) self.assertEqual(_EVERYTHING_ID, images[0].id) def test_list_images_filters_encoding(self): filters = {"owner": u"ni\xf1o"} try: self.controller.list(filters=filters) except KeyError: # NOTE(flaper87): It raises KeyError because there's # no fixture supporting this query: # /v2/images?owner=ni%C3%B1o&limit=20 # We just want to make sure filters are correctly encoded. pass self.assertEqual(b"ni\xc3\xb1o", filters["owner"]) def test_list_images_for_tag_single_image(self): img_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' filters = {'filters': {'tag': [_TAG1]}} images = self.controller.list(**filters) self.assertEqual(1, len(images)) self.assertEqual('%s' % img_id, images[0].id) def test_list_images_for_tag_multiple_images(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' img_id2 = '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810' filters = {'filters': {'tag': [_TAG2]}} images = self.controller.list(**filters) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[0].id) self.assertEqual('%s' % img_id2, images[1].id) def test_list_images_for_multi_tags(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' filters = {'filters': {'tag': [_TAG1, _TAG2]}} images = self.controller.list(**filters) self.assertEqual(1, len(images)) self.assertEqual('%s' % img_id1, images[0].id) def test_list_images_for_non_existent_tag(self): filters = {'filters': {'tag': ['fake']}} images = self.controller.list(**filters) self.assertEqual(0, len(images)) def test_list_images_for_invalid_tag(self): filters = {'filters': {'tag': [[]]}} self.assertRaises(exc.HTTPBadRequest, self.controller.list, **filters) def test_list_images_with_single_sort_key(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' sort_key = 'name' images = self.controller.list(sort_key=sort_key) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[0].id) def test_list_with_multiple_sort_keys(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' sort_key = ['name', 'id'] images = self.controller.list(sort_key=sort_key) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[0].id) def test_list_images_with_desc_sort_dir(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' sort_key = 'id' sort_dir = 'desc' images = self.controller.list(sort_key=sort_key, sort_dir=sort_dir) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[1].id) def test_list_images_with_multiple_sort_keys_and_one_sort_dir(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' sort_key = ['name', 'id'] sort_dir = 'desc' images = self.controller.list(sort_key=sort_key, sort_dir=sort_dir) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[1].id) def test_list_images_with_multiple_sort_dirs(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' sort_key = ['name', 'id'] sort_dir = ['desc', 'asc'] images = self.controller.list(sort_key=sort_key, sort_dir=sort_dir) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[1].id) def test_list_images_with_new_sorting_syntax(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' sort = 'name:desc,size:asc' images = self.controller.list(sort=sort) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[1].id) def test_list_images_sort_dirs_fewer_than_keys(self): sort_key = ['name', 'id', 'created_at'] sort_dir = ['desc', 'asc'] self.assertRaises(exc.HTTPBadRequest, self.controller.list, sort_key=sort_key, sort_dir=sort_dir) def test_list_images_combined_syntax(self): sort_key = ['name', 'id'] sort_dir = ['desc', 'asc'] sort = 'name:asc' self.assertRaises(exc.HTTPBadRequest, self.controller.list, sort=sort, sort_key=sort_key, sort_dir=sort_dir) def test_list_images_new_sorting_syntax_invalid_key(self): sort = 'INVALID:asc' self.assertRaises(exc.HTTPBadRequest, self.controller.list, sort=sort) def test_list_images_new_sorting_syntax_invalid_direction(self): sort = 'name:INVALID' self.assertRaises(exc.HTTPBadRequest, self.controller.list, sort=sort) def test_list_images_for_property(self): filters = {'filters': dict([('os_distro', 'NixOS')])} images = self.controller.list(**filters) self.assertEqual(1, len(images)) def test_list_images_for_non_existent_property(self): filters = {'filters': dict([('my_little_property', 'cant_be_this_cute')])} images = self.controller.list(**filters) self.assertEqual(0, len(images)) def test_get_image(self): image = self.controller.get('3a4560a1-e585-443e-9b39-553b46ec92d1') self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', image.id) self.assertEqual('image-1', image.name) def test_create_image(self): properties = { 'name': 'image-1' } image = self.controller.create(**properties) self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', image.id) self.assertEqual('image-1', image.name) def test_create_bad_additionalProperty_type(self): properties = { 'name': 'image-1', 'bad_prop': True, } with testtools.ExpectedException(TypeError): self.controller.create(**properties) def test_delete_image(self): self.controller.delete('87b634c1-f893-33c9-28a9-e5673c99239a') expect = [ ('DELETE', '/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a', {}, None)] self.assertEqual(expect, self.api.calls) def test_deactivate_image(self): id_image = '87b634c1-f893-33c9-28a9-e5673c99239a' self.controller.deactivate(id_image) expect = [('POST', '/v2/images/%s/actions/deactivate' % id_image, {}, None)] self.assertEqual(expect, self.api.calls) def test_reactivate_image(self): id_image = '87b634c1-f893-33c9-28a9-e5673c99239a' self.controller.reactivate(id_image) expect = [('POST', '/v2/images/%s/actions/reactivate' % id_image, {}, None)] self.assertEqual(expect, self.api.calls) def test_data_upload(self): image_data = 'CCC' image_id = '606b0e88-7c5a-4d54-b5bb-046105d4de6f' self.controller.upload(image_id, image_data) expect = [('PUT', '/v2/images/%s/file' % image_id, {'Content-Type': 'application/octet-stream'}, image_data)] self.assertEqual(expect, self.api.calls) def test_data_upload_w_size(self): image_data = 'CCC' image_id = '606b0e88-7c5a-4d54-b5bb-046105d4de6f' self.controller.upload(image_id, image_data, image_size=3) expect = [('PUT', '/v2/images/%s/file' % image_id, {'Content-Type': 'application/octet-stream'}, image_data)] self.assertEqual(expect, self.api.calls) def test_data_without_checksum(self): body = self.controller.data('5cc4bebc-db27-11e1-a1eb-080027cbe205', do_checksum=False) body = ''.join([b for b in body]) self.assertEqual('A', body) body = self.controller.data('5cc4bebc-db27-11e1-a1eb-080027cbe205') body = ''.join([b for b in body]) self.assertEqual('A', body) def test_data_with_wrong_checksum(self): body = self.controller.data('headeronly-db27-11e1-a1eb-080027cbe205', do_checksum=False) body = ''.join([b for b in body]) self.assertEqual('BB', body) body = self.controller.data('headeronly-db27-11e1-a1eb-080027cbe205') try: body = ''.join([b for b in body]) self.fail('data did not raise an error.') except IOError as e: self.assertEqual(errno.EPIPE, e.errno) msg = 'was 9d3d9048db16a7eee539e93e3618cbe7 expected wrong' self.assertIn(msg, str(e)) body = self.controller.data('chkonly-db27-11e1-a1eb-080027cbe205', do_checksum=False) body = ''.join([b for b in body]) self.assertEqual('BB', body) body = self.controller.data('chkonly-db27-11e1-a1eb-080027cbe205') try: body = ''.join([b for b in body]) self.fail('data did not raise an error.') except IOError as e: self.assertEqual(errno.EPIPE, e.errno) msg = 'was 9d3d9048db16a7eee539e93e3618cbe7 expected wrong' self.assertIn(msg, str(e)) body = self.controller.data('multihash-db27-11e1-a1eb-080027cbe205', do_checksum=False) body = ''.join([b for b in body]) self.assertEqual('BB', body) body = self.controller.data('multihash-db27-11e1-a1eb-080027cbe205') try: body = ''.join([b for b in body]) self.fail('data did not raise an error.') except IOError as e: self.assertEqual(errno.EPIPE, e.errno) msg = 'was 9d3d9048db16a7eee539e93e3618cbe7 expected junk' self.assertIn(msg, str(e)) body = self.controller.data('badalgo-db27-11e1-a1eb-080027cbe205', do_checksum=False) body = ''.join([b for b in body]) self.assertEqual('BB', body) try: body = self.controller.data('badalgo-db27-11e1-a1eb-080027cbe205') self.fail('bad os_hash_algo did not raise an error.') except ValueError as e: msg = 'unsupported hash type not_an_algo' self.assertIn(msg, str(e)) def test_data_with_checksum(self): for prefix in ['headeronly', 'chkonly', 'multihash']: body = self.controller.data(prefix + '-dd57-11e1-af0f-02163e68b1d8', do_checksum=False) body = ''.join([b for b in body]) self.assertEqual('CCC', body) body = self.controller.data(prefix + '-dd57-11e1-af0f-02163e68b1d8') body = ''.join([b for b in body]) self.assertEqual('CCC', body) @ddt.data('headeronly', 'chkonly', 'multihash') def test_data_with_checksum_but_no_md5_algo(self, prefix): with mock.patch('hashlib.new', mock.MagicMock( side_effect=ValueError('unsupported hash type'))): body = self.controller.data(prefix + '-dd57-11e1-af0f-02163e68b1d8', allow_md5_fallback=True) try: body = ''.join([b for b in body]) self.fail('missing md5 algo did not raise an error') except IOError as e: self.assertEqual(errno.EPIPE, e.errno) msg = 'md5 algorithm is not available on the client' self.assertIn(msg, str(e)) def test_data_with_checksum_and_fallback(self): # make sure the allow_md5_fallback option does not cause any # incorrect behavior when fallback is not needed for prefix in ['headeronly', 'chkonly', 'multihash']: body = self.controller.data(prefix + '-dd57-11e1-af0f-02163e68b1d8', do_checksum=False, allow_md5_fallback=True) body = ''.join([b for b in body]) self.assertEqual('CCC', body) body = self.controller.data(prefix + '-dd57-11e1-af0f-02163e68b1d8', allow_md5_fallback=True) body = ''.join([b for b in body]) self.assertEqual('CCC', body) def test_data_with_bad_hash_algo_and_fallback(self): # shouldn't matter when do_checksum is False body = self.controller.data('badalgo-db27-11e1-a1eb-080027cbe205', do_checksum=False, allow_md5_fallback=True) body = ''.join([b for b in body]) self.assertEqual('BB', body) # default value for do_checksum is True body = self.controller.data('badalgo-db27-11e1-a1eb-080027cbe205', allow_md5_fallback=True) body = ''.join([b for b in body]) self.assertEqual('BB', body) def test_neg_data_with_bad_hash_value_and_fallback_enabled(self): # make sure download fails when good hash_algo but bad hash_value # even when correct checksum is present regardless of # allow_md5_fallback setting body = self.controller.data('bad-multihash-value-good-checksum', allow_md5_fallback=False) try: body = ''.join([b for b in body]) self.fail('bad os_hash_value did not raise an error.') except IOError as e: self.assertEqual(errno.EPIPE, e.errno) msg = 'expected badmultihashvalue' self.assertIn(msg, str(e)) body = self.controller.data('bad-multihash-value-good-checksum', allow_md5_fallback=True) try: body = ''.join([b for b in body]) self.fail('bad os_hash_value did not raise an error.') except IOError as e: self.assertEqual(errno.EPIPE, e.errno) msg = 'expected badmultihashvalue' self.assertIn(msg, str(e)) # download should succeed when do_checksum is off, though body = self.controller.data('bad-multihash-value-good-checksum', do_checksum=False) body = ''.join([b for b in body]) self.assertEqual('GOODCHECKSUM', body) def test_image_import_web_download(self): uri = 'http://example.com/image.qcow' data = [('method', {'name': 'web-download', 'uri': uri})] image_id = '606b0e88-7c5a-4d54-b5bb-046105d4de6f' self.controller.image_import(image_id, 'web-download', uri) expect = [('POST', '/v2/images/%s/import' % image_id, {}, data)] self.assertEqual(expect, self.api.calls) def test_image_import_glance_download(self): region = 'REGION2' remote_image_id = '75baf7b6-253a-11ed-8307-4b1057986a78' image_id = '606b0e88-7c5a-4d54-b5bb-046105d4de6f' service_interface = 'public' data = [('method', {'name': 'glance-download', 'glance_region': region, 'glance_image_id': remote_image_id, 'glance_service_interface': service_interface})] self.controller.image_import( image_id, 'glance-download', remote_region=region, remote_image_id=remote_image_id, remote_service_interface=service_interface) expect = [('POST', '/v2/images/%s/import' % image_id, {}, data)] self.assertEqual(expect, self.api.calls) def test_download_no_data(self): resp = utils.FakeResponse(headers={}, status_code=204) self.controller.controller.http_client.get = mock.Mock( return_value=(resp, {})) self.controller.data('image_id') def test_download_forbidden(self): self.controller.controller.http_client.get = mock.Mock( side_effect=exc.HTTPForbidden()) try: self.controller.data('image_id') self.fail('No forbidden exception raised.') except exc.HTTPForbidden: pass def test_update_replace_prop(self): image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' params = {'name': 'pong'} image = self.controller.update(image_id, **params) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = [[('op', 'replace'), ('path', '/name'), ('value', 'pong')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) # NOTE(bcwaldon):due to limitations of our fake api framework, the name # will not actually change - yet in real life it will... self.assertEqual('image-1', image.name) def test_update_add_prop(self): image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' params = {'finn': 'human'} image = self.controller.update(image_id, **params) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = [[('op', 'add'), ('path', '/finn'), ('value', 'human')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) # NOTE(bcwaldon):due to limitations of our fake api framework, the name # will not actually change - yet in real life it will... self.assertEqual('image-1', image.name) def test_update_remove_prop(self): image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3' remove_props = ['barney'] image = self.controller.update(image_id, remove_props) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = [[('op', 'remove'), ('path', '/barney')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) # NOTE(bcwaldon):due to limitations of our fake api framework, the name # will not actually change - yet in real life it will... self.assertEqual('image-3', image.name) def test_update_replace_remove_same_prop(self): image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3' # Updating a property takes precedence over removing a property params = {'barney': 'miller'} remove_props = ['barney'] image = self.controller.update(image_id, remove_props, **params) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = ([[('op', 'replace'), ('path', '/barney'), ('value', 'miller')]]) expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) # NOTE(bcwaldon):due to limitations of our fake api framework, the name # will not actually change - yet in real life it will... self.assertEqual('image-3', image.name) def test_update_add_remove_same_prop(self): image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3' # Adding a property takes precedence over removing a property params = {'finn': 'human'} remove_props = ['finn'] image = self.controller.update(image_id, remove_props, **params) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = [[('op', 'add'), ('path', '/finn'), ('value', 'human')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) # NOTE(bcwaldon):due to limitations of our fake api framework, the name # will not actually change - yet in real life it will... self.assertEqual('image-3', image.name) def test_update_bad_additionalProperty_type(self): image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3' params = {'name': 'pong', 'bad_prop': False} with testtools.ExpectedException(TypeError): self.controller.update(image_id, **params) def test_update_add_custom_property(self): image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' params = {'color': 'red'} image = self.controller.update(image_id, **params) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = [[('op', 'add'), ('path', '/color'), ('value', 'red')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) def test_update_replace_custom_property(self): image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3' params = {'color': 'blue'} image = self.controller.update(image_id, **params) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = [[('op', 'replace'), ('path', '/color'), ('value', 'blue')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) def test_location_ops_when_server_disabled_location_ops(self): # Location operations should not be allowed if server has not # enabled location related operations. There is no need to check it # when do location add, because the check would be done in server side. image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' estr = 'The administrator has disabled API access to image locations' url = 'http://bar.com/' meta = {'bar': 'barmeta'} e = self.assertRaises(exc.HTTPBadRequest, self.controller.delete_locations, image_id, set([url])) self.assertIn(estr, str(e)) e = self.assertRaises(exc.HTTPBadRequest, self.controller.update_location, image_id, url, meta) self.assertIn(estr, str(e)) def _empty_get(self, image_id, headers=None): return ('GET', '/v2/images/%s' % image_id, headers or {}, None) def _patch_req(self, image_id, patch_body): c_type = 'application/openstack-images-v2.1-json-patch' data = [sorted(d.items()) for d in patch_body] return ('PATCH', '/v2/images/%s' % image_id, {'Content-Type': c_type}, data) def test_add_location(self): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' new_loc = {'url': 'http://spam.com/', 'metadata': {'spam': 'ham'}} add_patch = {'path': '/locations/-', 'value': new_loc, 'op': 'add'} headers = {'x-openstack-request-id': 'req-1234'} self.controller.add_location(image_id, **new_loc) self.assertEqual([self._patch_req(image_id, [add_patch]), self._empty_get(image_id, headers=headers)], self.api.calls) @mock.patch.object(images.Controller, '_send_image_update_request', side_effect=exc.HTTPBadRequest) def test_add_duplicate_location(self, mock_request): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' new_loc = {'url': 'http://foo.com/', 'metadata': {'foo': 'newfoo'}} self.assertRaises(exc.HTTPBadRequest, self.controller.add_location, image_id, **new_loc) def test_remove_location(self): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' url_set = set(['http://foo.com/', 'http://bar.com/']) del_patches = [{'path': '/locations/1', 'op': 'remove'}, {'path': '/locations/0', 'op': 'remove'}] self.controller.delete_locations(image_id, url_set) self.assertEqual([self._empty_get(image_id), self._patch_req(image_id, del_patches)], self.api.calls) def test_remove_missing_location(self): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' url_set = set(['http://spam.ham/']) err_str = 'Unknown URL(s): %s' % list(url_set) err = self.assertRaises(exc.HTTPNotFound, self.controller.delete_locations, image_id, url_set) self.assertIn(err_str, str(err)) def test_update_location(self): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' new_loc = {'url': 'http://foo.com/', 'metadata': {'spam': 'ham'}} headers = {'x-openstack-request-id': 'req-1234'} fixture_idx = '/v2/images/%s' % (image_id) orig_locations = data_fixtures[fixture_idx]['GET'][1]['locations'] loc_map = dict([(l['url'], l) for l in orig_locations]) loc_map[new_loc['url']] = new_loc mod_patch = [{'path': '/locations', 'op': 'replace', 'value': list(loc_map.values())}] self.controller.update_location(image_id, **new_loc) self.assertEqual([self._empty_get(image_id), self._patch_req(image_id, mod_patch), self._empty_get(image_id, headers=headers)], self.api.calls) def test_update_tags(self): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' tag_map = {'tags': ['tag01', 'tag02', 'tag03']} headers = {'x-openstack-request-id': 'req-1234'} image = self.controller.update(image_id, **tag_map) expected_body = [{'path': '/tags', 'op': 'replace', 'value': tag_map['tags']}] expected = [ self._empty_get(image_id), self._patch_req(image_id, expected_body), self._empty_get(image_id, headers=headers) ] self.assertEqual(expected, self.api.calls) self.assertEqual(image_id, image.id) def test_update_missing_location(self): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' new_loc = {'url': 'http://spam.com/', 'metadata': {'spam': 'ham'}} err_str = 'Unknown URL: %s' % new_loc['url'] err = self.assertRaises(exc.HTTPNotFound, self.controller.update_location, image_id, **new_loc) self.assertIn(err_str, str(err))