# 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 hashlib import http.client as http import os import subprocess import tempfile import time import uuid import fixtures from oslo_limit import exception as ol_exc from oslo_serialization import jsonutils from oslo_utils.secretutils import md5 from oslo_utils import units import requests from six.moves import urllib from glance.quota import keystone as ks_quota from glance.tests import functional from glance.tests.functional import ft_utils as func_utils from glance.tests import utils as test_utils TENANT1 = str(uuid.uuid4()) TENANT2 = str(uuid.uuid4()) TENANT3 = str(uuid.uuid4()) TENANT4 = str(uuid.uuid4()) def get_auth_header(tenant, tenant_id=None, role='reader,member', headers=None): """Return headers to authenticate as a specific tenant. :param tenant: Tenant for the auth token :param tenant_id: Optional tenant ID for the X-Tenant-Id header :param role: Optional user role :param headers: Optional list of headers to add to """ if not headers: headers = {} auth_token = 'user:%s:%s' % (tenant, role) headers.update({'X-Auth-Token': auth_token}) if tenant_id: headers.update({'X-Tenant-Id': tenant_id}) return headers class TestImages(functional.FunctionalTest): def setUp(self): super(TestImages, self).setUp() self.cleanup() self.include_scrubber = False self.api_server.deployment_flavor = 'noauth' for i in range(3): ret = test_utils.start_http_server("foo_image_id%d" % i, "foo_image%d" % i) setattr(self, 'http_server%d' % i, ret[1]) setattr(self, 'http_port%d' % i, ret[2]) self.api_server.send_identity_credentials = True def tearDown(self): for i in range(3): httpd = getattr(self, 'http_server%d' % i, None) if httpd: httpd.shutdown() httpd.server_close() super(TestImages, self).tearDown() def _headers(self, custom_headers=None): base_headers = { 'X-Identity-Status': 'Confirmed', 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96', 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e', 'X-Tenant-Id': TENANT1, 'X-Roles': 'reader,member', } base_headers.update(custom_headers or {}) return base_headers def test_image_import_using_glance_direct(self): self.start_servers(**self.__dict__.copy()) # Image list should be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # glance-direct should be available in discovery response path = self._url('/v2/info/import') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['import-methods']['value'] self.assertIn("glance-direct", discovery_calls) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Returned image entity should have a generated id and status image = jsonutils.loads(response.text) image_id = image['id'] checked_keys = set([ u'status', u'name', u'tags', u'created_at', u'updated_at', u'visibility', u'self', u'protected', u'os_hidden', u'id', u'file', u'min_disk', u'type', u'min_ram', u'schema', u'disk_format', u'container_format', u'owner', u'checksum', u'os_hash_algo', u'os_hash_value', u'size', u'virtual_size', ]) self.assertEqual(checked_keys, set(image.keys())) expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'type': 'kernel', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Image list should now have one entry path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # Upload some image data to staging area path = self._url('/v2/images/%s/stage' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) image_data = b'ZZZZZ' response = requests.put(path, headers=headers, data=image_data) self.assertEqual(http.NO_CONTENT, response.status_code) # Verify image is in uploading state, hashes are None func_utils.verify_image_hashes_and_status(self, image_id, size=len(image_data), status='uploading') # Import image to store path = self._url('/v2/images/%s/import' % image_id) headers = self._headers({ 'content-type': 'application/json', 'X-Roles': 'admin', }) data = jsonutils.dumps({'method': { 'name': 'glance-direct' }}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.ACCEPTED, response.status_code) # Verify image is in active state and checksum is set # NOTE(abhishekk): As import is a async call we need to provide # some timelap to complete the call. path = self._url('/v2/images/%s' % image_id) func_utils.wait_for_status(request_path=path, request_headers=self._headers(), status='active', max_sec=10, delay_sec=0.2) expect_c = str(md5(image_data, usedforsecurity=False).hexdigest()) expect_h = str(hashlib.sha512(image_data).hexdigest()) func_utils.verify_image_hashes_and_status(self, image_id, checksum=expect_c, os_hash_value=expect_h, size=len(image_data), status='active') # Ensure the size is updated to reflect the data uploaded path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertEqual(5, jsonutils.loads(response.text)['size']) # Deleting image should work path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Image list should now be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) self.stop_servers() def test_image_import_using_web_download(self): self.start_servers(**self.__dict__.copy()) # Image list should be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # web-download should be available in discovery response path = self._url('/v2/info/import') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['import-methods']['value'] self.assertIn("web-download", discovery_calls) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Returned image entity should have a generated id and status image = jsonutils.loads(response.text) image_id = image['id'] checked_keys = set([ u'status', u'name', u'tags', u'created_at', u'updated_at', u'visibility', u'self', u'protected', u'os_hidden', u'id', u'file', u'min_disk', u'type', u'min_ram', u'schema', u'disk_format', u'container_format', u'owner', u'checksum', u'os_hash_algo', u'os_hash_value', u'size', u'virtual_size', ]) self.assertEqual(checked_keys, set(image.keys())) expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'type': 'kernel', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Image list should now have one entry path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # Verify image is in queued state and hashes are None func_utils.verify_image_hashes_and_status(self, image_id, status='queued') # Import image to store path = self._url('/v2/images/%s/import' % image_id) headers = self._headers({ 'content-type': 'application/json', 'X-Roles': 'admin', }) # Start http server locally thread, httpd, port = test_utils.start_standalone_http_server() image_data_uri = 'http://localhost:%s/' % port data = jsonutils.dumps({'method': { 'name': 'web-download', 'uri': image_data_uri }}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.ACCEPTED, response.status_code) # Verify image is in active state and checksum is set # NOTE(abhishekk): As import is a async call we need to provide # some timelap to complete the call. path = self._url('/v2/images/%s' % image_id) func_utils.wait_for_status(request_path=path, request_headers=self._headers(), status='active', max_sec=20, delay_sec=0.2, start_delay_sec=1) with requests.get(image_data_uri) as r: expect_c = str(md5(r.content, usedforsecurity=False).hexdigest()) expect_h = str(hashlib.sha512(r.content).hexdigest()) func_utils.verify_image_hashes_and_status(self, image_id, checksum=expect_c, os_hash_value=expect_h, size=len(r.content), status='active') # kill the local http server httpd.shutdown() httpd.server_close() # Deleting image should work path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Image list should now be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) self.stop_servers() def test_image_lifecycle(self): # Image list should be empty self.api_server.show_multiple_locations = True self.start_servers(**self.__dict__.copy()) path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # Create an image (with two deployer-defined properties) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', 'foo': 'bar', 'disk_format': 'aki', 'container_format': 'aki', 'abc': 'xyz', 'protected': True}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image_location_header = response.headers['Location'] # Returned image entity should have a generated id and status image = jsonutils.loads(response.text) image_id = image['id'] checked_keys = set([ u'status', u'name', u'tags', u'created_at', u'updated_at', u'visibility', u'self', u'protected', u'os_hidden', u'id', u'file', u'min_disk', u'foo', u'abc', u'type', u'min_ram', u'schema', u'disk_format', u'container_format', u'owner', u'checksum', u'os_hash_algo', u'os_hash_value', u'size', u'virtual_size', u'locations', ]) self.assertEqual(checked_keys, set(image.keys())) expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': True, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'foo': 'bar', 'abc': 'xyz', 'type': 'kernel', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Image list should now have one entry path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # Create another image (with two deployer-defined properties) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-2', 'type': 'kernel', 'bar': 'foo', 'disk_format': 'aki', 'container_format': 'aki', 'xyz': 'abc'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Returned image entity should have a generated id and status image = jsonutils.loads(response.text) image2_id = image['id'] checked_keys = set([ u'status', u'name', u'tags', u'created_at', u'updated_at', u'visibility', u'self', u'protected', u'os_hidden', u'id', u'file', u'min_disk', u'bar', u'xyz', u'type', u'min_ram', u'schema', u'disk_format', u'container_format', u'owner', u'checksum', u'os_hash_algo', u'os_hash_value', u'size', u'virtual_size', u'locations', ]) self.assertEqual(checked_keys, set(image.keys())) expected_image = { 'status': 'queued', 'name': 'image-2', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image2_id, 'protected': False, 'file': '/v2/images/%s/file' % image2_id, 'min_disk': 0, 'bar': 'foo', 'xyz': 'abc', 'type': 'kernel', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Image list should now have two entries path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(2, len(images)) self.assertEqual(image2_id, images[0]['id']) self.assertEqual(image_id, images[1]['id']) # Image list should list only image-2 as image-1 doesn't contain the # property 'bar' path = self._url('/v2/images?bar=foo') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image2_id, images[0]['id']) # Image list should list only image-1 as image-2 doesn't contain the # property 'foo' path = self._url('/v2/images?foo=bar') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # The "changes-since" filter shouldn't work on glance v2 path = self._url('/v2/images?changes-since=20001007T10:10:10') response = requests.get(path, headers=self._headers()) self.assertEqual(http.BAD_REQUEST, response.status_code) path = self._url('/v2/images?changes-since=aaa') response = requests.get(path, headers=self._headers()) self.assertEqual(http.BAD_REQUEST, response.status_code) # Image list should list only image-1 based on the filter # 'protected=true' path = self._url('/v2/images?protected=true') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # Image list should list only image-2 based on the filter # 'protected=false' path = self._url('/v2/images?protected=false') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image2_id, images[0]['id']) # Image list should return 400 based on the filter # 'protected=False' path = self._url('/v2/images?protected=False') response = requests.get(path, headers=self._headers()) self.assertEqual(http.BAD_REQUEST, response.status_code) # Image list should list only image-1 based on the filter # 'foo=bar&abc=xyz' path = self._url('/v2/images?foo=bar&abc=xyz') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # Image list should list only image-2 based on the filter # 'bar=foo&xyz=abc' path = self._url('/v2/images?bar=foo&xyz=abc') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image2_id, images[0]['id']) # Image list should not list anything as the filter 'foo=baz&abc=xyz' # is not satisfied by either images path = self._url('/v2/images?foo=baz&abc=xyz') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # Get the image using the returned Location header response = requests.get(image_location_header, headers=self._headers()) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) self.assertEqual(image_id, image['id']) self.assertIsNone(image['checksum']) self.assertIsNone(image['size']) self.assertIsNone(image['virtual_size']) self.assertEqual('bar', image['foo']) self.assertTrue(image['protected']) self.assertEqual('kernel', image['type']) self.assertTrue(image['created_at']) self.assertTrue(image['updated_at']) self.assertEqual(image['updated_at'], image['created_at']) # The URI file:// should return a 400 rather than a 500 path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) url = ('file://') changes = [{ 'op': 'add', 'path': '/locations/-', 'value': { 'url': url, 'metadata': {} } }] data = jsonutils.dumps(changes) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.BAD_REQUEST, response.status_code, response.text) # The image should be mutable, including adding and removing properties path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) data = jsonutils.dumps([ {'op': 'replace', 'path': '/name', 'value': 'image-2'}, {'op': 'replace', 'path': '/disk_format', 'value': 'vhd'}, {'op': 'replace', 'path': '/container_format', 'value': 'ami'}, {'op': 'replace', 'path': '/foo', 'value': 'baz'}, {'op': 'add', 'path': '/ping', 'value': 'pong'}, {'op': 'replace', 'path': '/protected', 'value': True}, {'op': 'remove', 'path': '/type'}, ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) # Returned image entity should reflect the changes image = jsonutils.loads(response.text) self.assertEqual('image-2', image['name']) self.assertEqual('vhd', image['disk_format']) self.assertEqual('baz', image['foo']) self.assertEqual('pong', image['ping']) self.assertTrue(image['protected']) self.assertNotIn('type', image, response.text) # Adding 11 image properties should fail since configured limit is 10 path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) changes = [] for i in range(11): changes.append({'op': 'add', 'path': '/ping%i' % i, 'value': 'pong'}) data = jsonutils.dumps(changes) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.REQUEST_ENTITY_TOO_LARGE, response.status_code, response.text) # Adding 3 image locations should fail since configured limit is 2 path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) changes = [] for i in range(3): url = ('http://127.0.0.1:%s/foo_image' % getattr(self, 'http_port%d' % i)) changes.append({'op': 'add', 'path': '/locations/-', 'value': {'url': url, 'metadata': {}}, }) data = jsonutils.dumps(changes) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.REQUEST_ENTITY_TOO_LARGE, response.status_code, response.text) # Ensure the v2.0 json-patch content type is accepted path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.0-json-patch' headers = self._headers({'content-type': media_type}) data = jsonutils.dumps([{'add': '/ding', 'value': 'dong'}]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) # Returned image entity should reflect the changes image = jsonutils.loads(response.text) self.assertEqual('dong', image['ding']) # Updates should persist across requests path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) self.assertEqual(image_id, image['id']) self.assertEqual('image-2', image['name']) self.assertEqual('baz', image['foo']) self.assertEqual('pong', image['ping']) self.assertTrue(image['protected']) self.assertNotIn('type', image, response.text) # Try to download data before its uploaded path = self._url('/v2/images/%s/file' % image_id) headers = self._headers() response = requests.get(path, headers=headers) self.assertEqual(http.NO_CONTENT, response.status_code) # Upload some image data path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) image_data = b'ZZZZZ' response = requests.put(path, headers=headers, data=image_data) self.assertEqual(http.NO_CONTENT, response.status_code) expect_c = str(md5(image_data, usedforsecurity=False).hexdigest()) expect_h = str(hashlib.sha512(image_data).hexdigest()) func_utils.verify_image_hashes_and_status(self, image_id, expect_c, expect_h, 'active', size=len(image_data)) # `disk_format` and `container_format` cannot # be replaced when the image is active. immutable_paths = ['/disk_format', '/container_format'] media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) path = self._url('/v2/images/%s' % image_id) for immutable_path in immutable_paths: data = jsonutils.dumps([ {'op': 'replace', 'path': immutable_path, 'value': 'ari'}, ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code) # Try to download the data that was just uploaded path = self._url('/v2/images/%s/file' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertEqual(expect_c, response.headers['Content-MD5']) self.assertEqual('ZZZZZ', response.text) # Uploading duplicate data should be rejected with a 409. The # original data should remain untouched. path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) response = requests.put(path, headers=headers, data='XXX') self.assertEqual(http.CONFLICT, response.status_code) func_utils.verify_image_hashes_and_status(self, image_id, expect_c, expect_h, 'active', size=len(image_data)) # Ensure the size is updated to reflect the data uploaded path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertEqual(5, jsonutils.loads(response.text)['size']) # Should be able to deactivate image path = self._url('/v2/images/%s/actions/deactivate' % image_id) response = requests.post(path, data={}, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Change the image to public so TENANT2 can see it path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.0-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'admin'}) data = jsonutils.dumps([{"replace": "/visibility", "value": "public"}]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) # Tenant2 should get Forbidden when deactivating the public image path = self._url('/v2/images/%s/actions/deactivate' % image_id) response = requests.post(path, data={}, headers=self._headers( {'X-Tenant-Id': TENANT2})) self.assertEqual(http.FORBIDDEN, response.status_code) # Tenant2 should get Forbidden when reactivating the public image path = self._url('/v2/images/%s/actions/reactivate' % image_id) response = requests.post(path, data={}, headers=self._headers( {'X-Tenant-Id': TENANT2})) self.assertEqual(http.FORBIDDEN, response.status_code) # Deactivating a deactivated image succeeds (no-op) path = self._url('/v2/images/%s/actions/deactivate' % image_id) response = requests.post(path, data={}, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Can't download a deactivated image path = self._url('/v2/images/%s/file' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.FORBIDDEN, response.status_code) # Deactivated image should still be in a listing path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(2, len(images)) self.assertEqual(image2_id, images[0]['id']) self.assertEqual(image_id, images[1]['id']) # Should be able to reactivate a deactivated image path = self._url('/v2/images/%s/actions/reactivate' % image_id) response = requests.post(path, data={}, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Reactivating an active image succeeds (no-op) path = self._url('/v2/images/%s/actions/reactivate' % image_id) response = requests.post(path, data={}, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Deletion should not work on protected images path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.FORBIDDEN, response.status_code) # Unprotect image for deletion path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) doc = [{'op': 'replace', 'path': '/protected', 'value': False}] data = jsonutils.dumps(doc) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) # Deletion should work. Deleting image-1 path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # This image should be no longer be directly accessible path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.NOT_FOUND, response.status_code) # And neither should its data path = self._url('/v2/images/%s/file' % image_id) headers = self._headers() response = requests.get(path, headers=headers) self.assertEqual(http.NOT_FOUND, response.status_code) # Image list should now contain just image-2 path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image2_id, images[0]['id']) # Deleting image-2 should work path = self._url('/v2/images/%s' % image2_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Image list should now be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # Create image that tries to send True should return 400 path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = 'true' response = requests.post(path, headers=headers, data=data) self.assertEqual(http.BAD_REQUEST, response.status_code) # Create image that tries to send a string should return 400 path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = '"hello"' response = requests.post(path, headers=headers, data=data) self.assertEqual(http.BAD_REQUEST, response.status_code) # Create image that tries to send 123 should return 400 path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = '123' response = requests.post(path, headers=headers, data=data) self.assertEqual(http.BAD_REQUEST, response.status_code) self.stop_servers() def _create_qcow(self, size): fn = tempfile.mktemp(prefix='glance-unittest-images-', suffix='.qcow2') subprocess.check_output( 'qemu-img create -f qcow2 %s %i' % (fn, size), shell=True) return fn def test_image_upload_qcow_virtual_size_calculation(self): self.start_servers(**self.__dict__.copy()) # Create an image headers = self._headers({'Content-Type': 'application/json'}) data = jsonutils.dumps({'name': 'myqcow', 'disk_format': 'qcow2', 'container_format': 'bare'}) response = requests.post(self._url('/v2/images'), headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code, 'Failed to create: %s' % response.text) image = response.json() # Upload a qcow fn = self._create_qcow(128 * units.Mi) raw_size = os.path.getsize(fn) headers = self._headers({'Content-Type': 'application/octet-stream'}) response = requests.put(self._url('/v2/images/%s/file' % image['id']), headers=headers, data=open(fn, 'rb').read()) os.remove(fn) self.assertEqual(http.NO_CONTENT, response.status_code) # Check the image attributes response = requests.get(self._url('/v2/images/%s' % image['id']), headers=self._headers()) self.assertEqual(http.OK, response.status_code) image = response.json() self.assertEqual(128 * units.Mi, image['virtual_size']) self.assertEqual(raw_size, image['size']) def test_image_import_qcow_virtual_size_calculation(self): self.start_servers(**self.__dict__.copy()) # Create an image headers = self._headers({'Content-Type': 'application/json'}) data = jsonutils.dumps({'name': 'myqcow', 'disk_format': 'qcow2', 'container_format': 'bare'}) response = requests.post(self._url('/v2/images'), headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code, 'Failed to create: %s' % response.text) image = response.json() # Stage a qcow fn = self._create_qcow(128 * units.Mi) raw_size = os.path.getsize(fn) headers = self._headers({'Content-Type': 'application/octet-stream'}) response = requests.put(self._url('/v2/images/%s/stage' % image['id']), headers=headers, data=open(fn, 'rb').read()) os.remove(fn) self.assertEqual(http.NO_CONTENT, response.status_code) # Verify image is in uploading state and checksum is None func_utils.verify_image_hashes_and_status(self, image['id'], status='uploading', size=raw_size) # Import image to store path = self._url('/v2/images/%s/import' % image['id']) headers = self._headers({ 'content-type': 'application/json', 'X-Roles': 'admin', }) data = jsonutils.dumps({'method': { 'name': 'glance-direct' }}) response = requests.post( self._url('/v2/images/%s/import' % image['id']), headers=headers, data=data) self.assertEqual(http.ACCEPTED, response.status_code) # Verify image is in active state and checksum is set # NOTE(abhishekk): As import is a async call we need to provide # some timelap to complete the call. path = self._url('/v2/images/%s' % image['id']) func_utils.wait_for_status(request_path=path, request_headers=self._headers(), status='active', max_sec=15, delay_sec=0.2) # Check the image attributes response = requests.get(self._url('/v2/images/%s' % image['id']), headers=self._headers()) self.assertEqual(http.OK, response.status_code) image = response.json() self.assertEqual(128 * units.Mi, image['virtual_size']) self.assertEqual(raw_size, image['size']) def test_hidden_images(self): # Image list should be empty self.api_server.show_multiple_locations = True self.start_servers(**self.__dict__.copy()) path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', 'disk_format': 'aki', 'container_format': 'aki', 'protected': False}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Returned image entity should have a generated id and status image = jsonutils.loads(response.text) image_id = image['id'] checked_keys = set([ u'status', u'name', u'tags', u'created_at', u'updated_at', u'visibility', u'self', u'protected', u'os_hidden', u'id', u'file', u'min_disk', u'type', u'min_ram', u'schema', u'disk_format', u'container_format', u'owner', u'checksum', u'os_hash_algo', u'os_hash_value', u'size', u'virtual_size', u'locations', ]) self.assertEqual(checked_keys, set(image.keys())) # Returned image entity should have os_hidden as False expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'os_hidden': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'type': 'kernel', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Image list should now have one entry path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # Create another image with hidden true path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-2', 'type': 'kernel', 'disk_format': 'aki', 'container_format': 'aki', 'os_hidden': True}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Returned image entity should have a generated id and status image = jsonutils.loads(response.text) image2_id = image['id'] checked_keys = set([ u'status', u'name', u'tags', u'created_at', u'updated_at', u'visibility', u'self', u'protected', u'os_hidden', u'id', u'file', u'min_disk', u'type', u'min_ram', u'schema', u'disk_format', u'container_format', u'owner', u'checksum', u'os_hash_algo', u'os_hash_value', u'size', u'virtual_size', u'locations', ]) self.assertEqual(checked_keys, set(image.keys())) # Returned image entity should have os_hidden as True expected_image = { 'status': 'queued', 'name': 'image-2', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image2_id, 'protected': False, 'os_hidden': True, 'file': '/v2/images/%s/file' % image2_id, 'min_disk': 0, 'type': 'kernel', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Image list should now have one entries path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # Image list should list should show one image based on the filter # 'hidden=false' path = self._url('/v2/images?os_hidden=false') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # Image list should list should show one image based on the filter # 'hidden=true' path = self._url('/v2/images?os_hidden=true') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image2_id, images[0]['id']) # Image list should return 400 based on the filter # 'hidden=abcd' path = self._url('/v2/images?os_hidden=abcd') response = requests.get(path, headers=self._headers()) self.assertEqual(http.BAD_REQUEST, response.status_code) # Upload some image data to image-1 path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) image_data = b'ZZZZZ' response = requests.put(path, headers=headers, data=image_data) self.assertEqual(http.NO_CONTENT, response.status_code) expect_c = str(md5(image_data, usedforsecurity=False).hexdigest()) expect_h = str(hashlib.sha512(image_data).hexdigest()) func_utils.verify_image_hashes_and_status(self, image_id, expect_c, expect_h, size=len(image_data), status='active') # Upload some image data to image-2 path = self._url('/v2/images/%s/file' % image2_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) image_data = b'WWWWW' response = requests.put(path, headers=headers, data=image_data) self.assertEqual(http.NO_CONTENT, response.status_code) expect_c = str(md5(image_data, usedforsecurity=False).hexdigest()) expect_h = str(hashlib.sha512(image_data).hexdigest()) func_utils.verify_image_hashes_and_status(self, image2_id, expect_c, expect_h, size=len(image_data), status='active') # Hide image-1 path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) data = jsonutils.dumps([ {'op': 'replace', 'path': '/os_hidden', 'value': True}, ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) # Returned image entity should reflect the changes image = jsonutils.loads(response.text) self.assertTrue(image['os_hidden']) # Image list should now have 0 entries path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # Image list should list should show image-1, and image-2 based # on the filter 'hidden=true' path = self._url('/v2/images?os_hidden=true') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(2, len(images)) self.assertEqual(image2_id, images[0]['id']) self.assertEqual(image_id, images[1]['id']) # Un-Hide image-1 path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) data = jsonutils.dumps([ {'op': 'replace', 'path': '/os_hidden', 'value': False}, ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) # Returned image entity should reflect the changes image = jsonutils.loads(response.text) self.assertFalse(image['os_hidden']) # Image list should now have 1 entry path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # Deleting image-1 should work path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Deleting image-2 should work path = self._url('/v2/images/%s' % image2_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Image list should now be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) self.stop_servers() def test_update_readonly_prop(self): self.start_servers(**self.__dict__.copy()) # Create an image (with two deployer-defined properties) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1'}) response = requests.post(path, headers=headers, data=data) image = jsonutils.loads(response.text) image_id = image['id'] path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) props = ['/id', '/file', '/location', '/schema', '/self'] for prop in props: doc = [{'op': 'replace', 'path': prop, 'value': 'value1'}] data = jsonutils.dumps(doc) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code) for prop in props: doc = [{'op': 'remove', 'path': prop, 'value': 'value1'}] data = jsonutils.dumps(doc) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code) for prop in props: doc = [{'op': 'add', 'path': prop, 'value': 'value1'}] data = jsonutils.dumps(doc) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code) self.stop_servers() def test_methods_that_dont_accept_illegal_bodies(self): # Check images can be reached self.start_servers(**self.__dict__.copy()) path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) # Test all the schemas schema_urls = [ '/v2/schemas/images', '/v2/schemas/image', '/v2/schemas/members', '/v2/schemas/member', ] for value in schema_urls: path = self._url(value) data = jsonutils.dumps(["body"]) response = requests.get(path, headers=self._headers(), data=data) self.assertEqual(http.BAD_REQUEST, response.status_code) # Create image for use with tests path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] test_urls = [ ('/v2/images/%s', 'get'), ('/v2/images/%s/actions/deactivate', 'post'), ('/v2/images/%s/actions/reactivate', 'post'), ('/v2/images/%s/tags/mytag', 'put'), ('/v2/images/%s/tags/mytag', 'delete'), ('/v2/images/%s/members', 'get'), ('/v2/images/%s/file', 'get'), ('/v2/images/%s', 'delete'), ] for link, method in test_urls: path = self._url(link % image_id) data = jsonutils.dumps(["body"]) response = getattr(requests, method)( path, headers=self._headers(), data=data) self.assertEqual(http.BAD_REQUEST, response.status_code) # DELETE /images/imgid without legal json path = self._url('/v2/images/%s' % image_id) data = '{"hello"]' response = requests.delete(path, headers=self._headers(), data=data) self.assertEqual(http.BAD_REQUEST, response.status_code) # POST /images/imgid/members path = self._url('/v2/images/%s/members' % image_id) data = jsonutils.dumps({'member': TENANT3}) response = requests.post(path, headers=self._headers(), data=data) self.assertEqual(http.OK, response.status_code) # GET /images/imgid/members/memid path = self._url('/v2/images/%s/members/%s' % (image_id, TENANT3)) data = jsonutils.dumps(["body"]) response = requests.get(path, headers=self._headers(), data=data) self.assertEqual(http.BAD_REQUEST, response.status_code) # DELETE /images/imgid/members/memid path = self._url('/v2/images/%s/members/%s' % (image_id, TENANT3)) data = jsonutils.dumps(["body"]) response = requests.delete(path, headers=self._headers(), data=data) self.assertEqual(http.BAD_REQUEST, response.status_code) self.stop_servers() def test_download_random_access_w_range_request(self): """ Test partial download 'Range' requests for images (random image access) """ self.start_servers(**self.__dict__.copy()) # Create an image (with two deployer-defined properties) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-2', 'type': 'kernel', 'bar': 'foo', 'disk_format': 'aki', 'container_format': 'aki', 'xyz': 'abc'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] # Upload data to image image_data = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) response = requests.put(path, headers=headers, data=image_data) self.assertEqual(http.NO_CONTENT, response.status_code) # test for success on satisfiable Range request. range_ = 'bytes=3-10' headers = self._headers({'Range': range_}) path = self._url('/v2/images/%s/file' % image_id) response = requests.get(path, headers=headers) self.assertEqual(http.PARTIAL_CONTENT, response.status_code) self.assertEqual('DEFGHIJK', response.text) # test for failure on unsatisfiable Range request. range_ = 'bytes=10-5' headers = self._headers({'Range': range_}) path = self._url('/v2/images/%s/file' % image_id) response = requests.get(path, headers=headers) self.assertEqual(http.REQUESTED_RANGE_NOT_SATISFIABLE, response.status_code) self.stop_servers() def test_download_random_access_w_content_range(self): """ Even though Content-Range is incorrect on requests, we support it for backward compatibility with clients written for pre-Pike Glance. The following test is for 'Content-Range' requests, which we have to ensure that we prevent regression. """ self.start_servers(**self.__dict__.copy()) # Create another image (with two deployer-defined properties) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-2', 'type': 'kernel', 'bar': 'foo', 'disk_format': 'aki', 'container_format': 'aki', 'xyz': 'abc'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] # Upload data to image image_data = 'Z' * 15 path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) response = requests.put(path, headers=headers, data=image_data) self.assertEqual(http.NO_CONTENT, response.status_code) result_body = '' for x in range(15): # NOTE(flaper87): Read just 1 byte. Content-Range is # 0-indexed and it specifies the first byte to read # and the last byte to read. content_range = 'bytes %s-%s/15' % (x, x) headers = self._headers({'Content-Range': content_range}) path = self._url('/v2/images/%s/file' % image_id) response = requests.get(path, headers=headers) self.assertEqual(http.PARTIAL_CONTENT, response.status_code) result_body += response.text self.assertEqual(result_body, image_data) # test for failure on unsatisfiable request for ContentRange. content_range = 'bytes 3-16/15' headers = self._headers({'Content-Range': content_range}) path = self._url('/v2/images/%s/file' % image_id) response = requests.get(path, headers=headers) self.assertEqual(http.REQUESTED_RANGE_NOT_SATISFIABLE, response.status_code) self.stop_servers() def test_download_policy_when_cache_is_not_enabled(self): rules = {'context_is_admin': 'role:admin', 'default': '', 'add_image': '', 'get_image': '', 'modify_image': '', 'upload_image': '', 'delete_image': '', 'download_image': '!'} self.set_policy_rules(rules) self.start_servers(**self.__dict__.copy()) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'reader,member'}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Returned image entity image = jsonutils.loads(response.text) image_id = image['id'] expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Upload data to image path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) response = requests.put(path, headers=headers, data='ZZZZZ') self.assertEqual(http.NO_CONTENT, response.status_code) # Get an image should fail path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) response = requests.get(path, headers=headers) self.assertEqual(http.FORBIDDEN, response.status_code) # Image Deletion should work path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # This image should be no longer be directly accessible path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.NOT_FOUND, response.status_code) self.stop_servers() def test_download_image_not_allowed_using_restricted_policy(self): rules = { "context_is_admin": "role:admin", "default": "", "add_image": "", "get_image": "", "modify_image": "", "upload_image": "", "delete_image": "", "restricted": "not ('aki':%(container_format)s and role:_member_)", "download_image": "role:admin or rule:restricted" } self.set_policy_rules(rules) self.start_servers(**self.__dict__.copy()) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'reader,member'}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Returned image entity image = jsonutils.loads(response.text) image_id = image['id'] expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Upload data to image path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) response = requests.put(path, headers=headers, data='ZZZZZ') self.assertEqual(http.NO_CONTENT, response.status_code) # Get an image should fail path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream', 'X-Roles': '_member_'}) response = requests.get(path, headers=headers) self.assertEqual(http.FORBIDDEN, response.status_code) # Image Deletion should work path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # This image should be no longer be directly accessible path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.NOT_FOUND, response.status_code) self.stop_servers() def test_download_image_allowed_using_restricted_policy(self): rules = { "context_is_admin": "role:admin", "default": "", "add_image": "", "get_image": "", "modify_image": "", "upload_image": "", "get_image_location": "", "delete_image": "", "restricted": "not ('aki':%(container_format)s and role:_member_)", "download_image": "role:admin or rule:restricted" } self.set_policy_rules(rules) self.start_servers(**self.__dict__.copy()) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'reader,member'}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Returned image entity image = jsonutils.loads(response.text) image_id = image['id'] expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Upload data to image path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) response = requests.put(path, headers=headers, data='ZZZZZ') self.assertEqual(http.NO_CONTENT, response.status_code) # Get an image should be allowed path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream', 'X-Roles': 'reader,member'}) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) # Image Deletion should work path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # This image should be no longer be directly accessible path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.NOT_FOUND, response.status_code) self.stop_servers() def test_download_image_raises_service_unavailable(self): """Test image download returns HTTPServiceUnavailable.""" self.api_server.show_multiple_locations = True self.start_servers(**self.__dict__.copy()) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Get image id image = jsonutils.loads(response.text) image_id = image['id'] # Update image locations via PATCH path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) thread, httpd, http_port = test_utils.start_http_server(image_id, "image-1") values = [{'url': 'http://127.0.0.1:%s/image-1' % http_port, 'metadata': {'idx': '0'}}] doc = [{'op': 'replace', 'path': '/locations', 'value': values}] data = jsonutils.dumps(doc) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code) # Download an image should work path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/json'}) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) # Stop http server used to update image location httpd.shutdown() httpd.server_close() # Download an image should raise HTTPServiceUnavailable path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/json'}) response = requests.get(path, headers=headers) self.assertEqual(http.SERVICE_UNAVAILABLE, response.status_code) # Image Deletion should work path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # This image should be no longer be directly accessible path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.NOT_FOUND, response.status_code) self.stop_servers() def test_image_modification_works_for_owning_tenant_id(self): rules = { "context_is_admin": "role:admin", "default": "", "add_image": "", "get_image": "", "modify_image": "project_id:%(owner)s", "upload_image": "", "get_image_location": "", "delete_image": "", "restricted": "not ('aki':%(container_format)s and role:_member_)", "download_image": "role:admin or rule:restricted" } self.set_policy_rules(rules) self.start_servers(**self.__dict__.copy()) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Get the image's ID image = jsonutils.loads(response.text) image_id = image['id'] path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers['content-type'] = media_type del headers['X-Roles'] data = jsonutils.dumps([ {'op': 'replace', 'path': '/name', 'value': 'new-name'}, ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code) self.stop_servers() def test_image_modification_fails_on_mismatched_tenant_ids(self): rules = { "context_is_admin": "role:admin", "default": "", "add_image": "", "get_image": "", "modify_image": "'A-Fake-Tenant-Id':%(owner)s", "upload_image": "", "get_image_location": "", "delete_image": "", "restricted": "not ('aki':%(container_format)s and role:_member_)", "download_image": "role:admin or rule:restricted" } self.set_policy_rules(rules) self.start_servers(**self.__dict__.copy()) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Get the image's ID image = jsonutils.loads(response.text) image_id = image['id'] path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers['content-type'] = media_type del headers['X-Roles'] data = jsonutils.dumps([ {'op': 'replace', 'path': '/name', 'value': 'new-name'}, ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code) self.stop_servers() def test_member_additions_works_for_owning_tenant_id(self): rules = { "context_is_admin": "role:admin", "default": "", "add_image": "", "get_image": "", "modify_image": "", "upload_image": "", "get_image_location": "", "delete_image": "", "restricted": "not ('aki':%(container_format)s and role:_member_)", "download_image": "role:admin or rule:restricted", "add_member": "project_id:%(owner)s", } self.set_policy_rules(rules) self.start_servers(**self.__dict__.copy()) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Get the image's ID image = jsonutils.loads(response.text) image_id = image['id'] # Get the image's members resource path = self._url('/v2/images/%s/members' % image_id) body = jsonutils.dumps({'member': TENANT3}) del headers['X-Roles'] response = requests.post(path, headers=headers, data=body) self.assertEqual(http.OK, response.status_code) self.stop_servers() def test_image_additions_works_only_for_specific_tenant_id(self): rules = { "context_is_admin": "role:admin", "default": "", "add_image": "'{0}':%(owner)s".format(TENANT1), "get_image": "", "modify_image": "", "upload_image": "", "get_image_location": "", "delete_image": "", "restricted": "not ('aki':%(container_format)s and role:_member_)", "download_image": "role:admin or rule:restricted", "add_member": "", } self.set_policy_rules(rules) self.start_servers(**self.__dict__.copy()) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin', 'X-Tenant-Id': TENANT1}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) headers['X-Tenant-Id'] = TENANT2 response = requests.post(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code) self.stop_servers() def test_owning_tenant_id_can_retrieve_image_information(self): rules = { "context_is_admin": "role:admin", "default": "", "add_image": "", "get_image": "project_id:%(owner)s", "modify_image": "", "upload_image": "", "get_image_location": "", "delete_image": "", "restricted": "not ('aki':%(container_format)s and role:_member_)", "download_image": "role:admin or rule:restricted", "add_member": "", } self.set_policy_rules(rules) self.start_servers(**self.__dict__.copy()) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin', 'X-Tenant-Id': TENANT1}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Replace the admin role with reader and member headers['X-Roles'] = 'reader,member' # Get the image's ID image = jsonutils.loads(response.text) image_id = image['id'] # Can retrieve the image as TENANT1 path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) # Can retrieve the image's members as TENANT1 path = self._url('/v2/images/%s/members' % image_id) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) headers['X-Tenant-Id'] = TENANT2 response = requests.get(path, headers=headers) self.assertEqual(http.NOT_FOUND, response.status_code) self.stop_servers() def test_owning_tenant_can_publicize_image(self): rules = { "context_is_admin": "role:admin", "default": "", "add_image": "", "publicize_image": "project_id:%(owner)s", "get_image": "project_id:%(owner)s", "modify_image": "", "upload_image": "", "get_image_location": "", "delete_image": "", "restricted": "not ('aki':%(container_format)s and role:_member_)", "download_image": "role:admin or rule:restricted", "add_member": "", } self.set_policy_rules(rules) self.start_servers(**self.__dict__.copy()) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin', 'X-Tenant-Id': TENANT1}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Get the image's ID image = jsonutils.loads(response.text) image_id = image['id'] path = self._url('/v2/images/%s' % image_id) headers = self._headers({ 'Content-Type': 'application/openstack-images-v2.1-json-patch', 'X-Tenant-Id': TENANT1, }) doc = [{'op': 'replace', 'path': '/visibility', 'value': 'public'}] data = jsonutils.dumps(doc) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code) def test_owning_tenant_can_communitize_image(self): rules = { "context_is_admin": "role:admin", "default": "", "add_image": "", "communitize_image": "project_id:%(owner)s", "get_image": "project_id:%(owner)s", "modify_image": "", "upload_image": "", "get_image_location": "", "delete_image": "", "restricted": "not ('aki':%(container_format)s and role:_member_)", "download_image": "role:admin or rule:restricted", "add_member": "", } self.set_policy_rules(rules) self.start_servers(**self.__dict__.copy()) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin', 'X-Tenant-Id': TENANT1}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(201, response.status_code) # Get the image's ID image = jsonutils.loads(response.text) image_id = image['id'] path = self._url('/v2/images/%s' % image_id) headers = self._headers({ 'Content-Type': 'application/openstack-images-v2.1-json-patch', 'X-Tenant-Id': TENANT1, }) doc = [{'op': 'replace', 'path': '/visibility', 'value': 'community'}] data = jsonutils.dumps(doc) response = requests.patch(path, headers=headers, data=data) self.assertEqual(200, response.status_code) def test_owning_tenant_can_delete_image(self): rules = { "context_is_admin": "role:admin", "default": "", "add_image": "", "publicize_image": "project_id:%(owner)s", "get_image": "project_id:%(owner)s", "modify_image": "", "upload_image": "", "get_image_location": "", "delete_image": "", "restricted": "not ('aki':%(container_format)s and role:_member_)", "download_image": "role:admin or rule:restricted", "add_member": "", } self.set_policy_rules(rules) self.start_servers(**self.__dict__.copy()) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin', 'X-Tenant-Id': TENANT1}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Get the image's ID image = jsonutils.loads(response.text) image_id = image['id'] path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=headers) self.assertEqual(http.NO_CONTENT, response.status_code) def test_list_show_ok_when_get_location_allowed_for_admins(self): self.api_server.show_image_direct_url = True self.api_server.show_multiple_locations = True # setup context to allow a list locations by admin only rules = { "context_is_admin": "role:admin", "default": "", "add_image": "", "get_image": "", "modify_image": "", "upload_image": "", "get_image_location": "role:admin", "delete_image": "", "restricted": "", "download_image": "", "add_member": "", } self.set_policy_rules(rules) self.start_servers(**self.__dict__.copy()) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Tenant-Id': TENANT1}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Get the image's ID image = jsonutils.loads(response.text) image_id = image['id'] # Can retrieve the image as TENANT1 path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) # Can list images as TENANT1 path = self._url('/v2/images') response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) self.stop_servers() def test_image_size_cap(self): self.api_server.image_size_cap = 128 self.start_servers(**self.__dict__.copy()) # create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-size-cap-test-image', 'type': 'kernel', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] # try to populate it with oversized data path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) class StreamSim(object): # Using a one-shot iterator to force chunked transfer in the PUT # request def __init__(self, size): self.size = size def __iter__(self): yield b'Z' * self.size response = requests.put(path, headers=headers, data=StreamSim( self.api_server.image_size_cap + 1)) self.assertEqual(http.REQUEST_ENTITY_TOO_LARGE, response.status_code) # hashlib.md5('Z'*129).hexdigest() # == '76522d28cb4418f12704dfa7acd6e7ee' # If the image has this checksum, it means that the whole stream was # accepted and written to the store, which should not be the case. path = self._url('/v2/images/{0}'.format(image_id)) headers = self._headers({'content-type': 'application/json'}) response = requests.get(path, headers=headers) image_checksum = jsonutils.loads(response.text).get('checksum') self.assertNotEqual(image_checksum, '76522d28cb4418f12704dfa7acd6e7ee') def test_permissions(self): self.start_servers(**self.__dict__.copy()) # Create an image that belongs to TENANT1 path = self._url('/v2/images') headers = self._headers({'Content-Type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'raw', 'container_format': 'bare'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image_id = jsonutils.loads(response.text)['id'] # Upload some image data path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) response = requests.put(path, headers=headers, data='ZZZZZ') self.assertEqual(http.NO_CONTENT, response.status_code) # TENANT1 should see the image in their list path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(image_id, images[0]['id']) # TENANT1 should be able to access the image directly path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) # TENANT2 should not see the image in their list path = self._url('/v2/images') headers = self._headers({'X-Tenant-Id': TENANT2}) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # TENANT2 should not be able to access the image directly path = self._url('/v2/images/%s' % image_id) headers = self._headers({'X-Tenant-Id': TENANT2}) response = requests.get(path, headers=headers) self.assertEqual(http.NOT_FOUND, response.status_code) # TENANT2 should not be able to modify the image, either path = self._url('/v2/images/%s' % image_id) headers = self._headers({ 'Content-Type': 'application/openstack-images-v2.1-json-patch', 'X-Tenant-Id': TENANT2, }) doc = [{'op': 'replace', 'path': '/name', 'value': 'image-2'}] data = jsonutils.dumps(doc) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.NOT_FOUND, response.status_code) # TENANT2 should not be able to delete the image, either path = self._url('/v2/images/%s' % image_id) headers = self._headers({'X-Tenant-Id': TENANT2}) response = requests.delete(path, headers=headers) self.assertEqual(http.NOT_FOUND, response.status_code) # Publicize the image as an admin of TENANT1 path = self._url('/v2/images/%s' % image_id) headers = self._headers({ 'Content-Type': 'application/openstack-images-v2.1-json-patch', 'X-Roles': 'admin', }) doc = [{'op': 'replace', 'path': '/visibility', 'value': 'public'}] data = jsonutils.dumps(doc) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code) # TENANT3 should now see the image in their list path = self._url('/v2/images') headers = self._headers({'X-Tenant-Id': TENANT3}) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(image_id, images[0]['id']) # TENANT3 should also be able to access the image directly path = self._url('/v2/images/%s' % image_id) headers = self._headers({'X-Tenant-Id': TENANT3}) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) # TENANT3 still should not be able to modify the image path = self._url('/v2/images/%s' % image_id) headers = self._headers({ 'Content-Type': 'application/openstack-images-v2.1-json-patch', 'X-Tenant-Id': TENANT3, }) doc = [{'op': 'replace', 'path': '/name', 'value': 'image-2'}] data = jsonutils.dumps(doc) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code) # TENANT3 should not be able to delete the image, either path = self._url('/v2/images/%s' % image_id) headers = self._headers({'X-Tenant-Id': TENANT3}) response = requests.delete(path, headers=headers) self.assertEqual(http.FORBIDDEN, response.status_code) # Image data should still be present after the failed delete path = self._url('/v2/images/%s/file' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertEqual(response.text, 'ZZZZZ') self.stop_servers() def test_property_protections_with_roles(self): # Enable property protection self.api_server.property_protection_file = self.property_file_roles self.start_servers(**self.__dict__.copy()) # Image list should be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # Create an image for role member with extra props # Raises 403 since user is not allowed to set 'foo' path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'reader,member'}) data = jsonutils.dumps({'name': 'image-1', 'foo': 'bar', 'disk_format': 'aki', 'container_format': 'aki', 'x_owner_foo': 'o_s_bar'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code) # Create an image for role member without 'foo' path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'reader,member'}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki', 'x_owner_foo': 'o_s_bar'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Returned image entity should have 'x_owner_foo' image = jsonutils.loads(response.text) image_id = image['id'] expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'x_owner_foo': 'o_s_bar', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Create an image for role spl_role with extra props path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'reader,member,spl_role'}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki', 'spl_create_prop': 'create_bar', 'spl_create_prop_policy': 'create_policy_bar', 'spl_read_prop': 'read_bar', 'spl_update_prop': 'update_bar', 'spl_delete_prop': 'delete_bar', 'spl_delete_empty_prop': ''}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] # Attempt to replace, add and remove properties which are forbidden path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'reader,member,spl_role'}) data = jsonutils.dumps([ {'op': 'replace', 'path': '/spl_read_prop', 'value': 'r'}, {'op': 'replace', 'path': '/spl_update_prop', 'value': 'u'}, ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code, response.text) # Attempt to replace, add and remove properties which are forbidden path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'reader,member,spl_role'}) data = jsonutils.dumps([ {'op': 'add', 'path': '/spl_new_prop', 'value': 'new'}, {'op': 'remove', 'path': '/spl_create_prop'}, {'op': 'remove', 'path': '/spl_delete_prop'}, ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code, response.text) # Attempt to replace properties path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'reader,member,spl_role'}) data = jsonutils.dumps([ # Updating an empty property to verify bug #1332103. {'op': 'replace', 'path': '/spl_update_prop', 'value': ''}, {'op': 'replace', 'path': '/spl_update_prop', 'value': 'u'}, ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) # Returned image entity should reflect the changes image = jsonutils.loads(response.text) # 'spl_update_prop' has update permission for spl_role # hence the value has changed self.assertEqual('u', image['spl_update_prop']) # Attempt to remove properties path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'reader,member,spl_role'}) data = jsonutils.dumps([ {'op': 'remove', 'path': '/spl_delete_prop'}, # Deleting an empty property to verify bug #1332103. {'op': 'remove', 'path': '/spl_delete_empty_prop'}, ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) # Returned image entity should reflect the changes image = jsonutils.loads(response.text) # 'spl_delete_prop' and 'spl_delete_empty_prop' have delete # permission for spl_role hence the property has been deleted self.assertNotIn('spl_delete_prop', image.keys()) self.assertNotIn('spl_delete_empty_prop', image.keys()) # Image Deletion should work path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # This image should be no longer be directly accessible path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.NOT_FOUND, response.status_code) self.stop_servers() def test_property_protections_with_policies(self): # Enable property protection rules = { "glance_creator": "role:admin or role:spl_role" } self.set_policy_rules(rules) self.api_server.property_protection_file = self.property_file_policies self.api_server.property_protection_rule_format = 'policies' self.start_servers(**self.__dict__.copy()) # Image list should be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # Create an image for role member with extra props # Raises 403 since user is not allowed to set 'foo' path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'reader,member'}) data = jsonutils.dumps({'name': 'image-1', 'foo': 'bar', 'disk_format': 'aki', 'container_format': 'aki', 'x_owner_foo': 'o_s_bar'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code) # Create an image for role member without 'foo' path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'reader,member'}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Returned image entity image = jsonutils.loads(response.text) image_id = image['id'] expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Create an image for role spl_role with extra props path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'reader,member,spl_role, admin'}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki', 'spl_creator_policy': 'creator_bar', 'spl_default_policy': 'default_bar'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] self.assertEqual('creator_bar', image['spl_creator_policy']) self.assertEqual('default_bar', image['spl_default_policy']) # Attempt to replace a property which is permitted path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'admin'}) data = jsonutils.dumps([ # Updating an empty property to verify bug #1332103. {'op': 'replace', 'path': '/spl_creator_policy', 'value': ''}, {'op': 'replace', 'path': '/spl_creator_policy', 'value': 'r'}, ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) # Returned image entity should reflect the changes image = jsonutils.loads(response.text) # 'spl_creator_policy' has update permission for admin # hence the value has changed self.assertEqual('r', image['spl_creator_policy']) # Attempt to replace a property which is forbidden path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'reader,member,spl_role'}) data = jsonutils.dumps([ {'op': 'replace', 'path': '/spl_creator_policy', 'value': 'z'}, ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code, response.text) # Attempt to read properties path = self._url('/v2/images/%s' % image_id) headers = self._headers({'content-type': media_type, 'X-Roles': 'reader,random_role'}) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) # 'random_role' is allowed read 'spl_default_policy'. self.assertEqual(image['spl_default_policy'], 'default_bar') # 'random_role' is forbidden to read 'spl_creator_policy'. self.assertNotIn('spl_creator_policy', image) # Attempt to replace and remove properties which are permitted path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'admin'}) data = jsonutils.dumps([ # Deleting an empty property to verify bug #1332103. {'op': 'replace', 'path': '/spl_creator_policy', 'value': ''}, {'op': 'remove', 'path': '/spl_creator_policy'}, ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) # Returned image entity should reflect the changes image = jsonutils.loads(response.text) # 'spl_creator_policy' has delete permission for admin # hence the value has been deleted self.assertNotIn('spl_creator_policy', image) # Attempt to read a property that is permitted path = self._url('/v2/images/%s' % image_id) headers = self._headers({'content-type': media_type, 'X-Roles': 'reader,random_role'}) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) # Returned image entity should reflect the changes image = jsonutils.loads(response.text) self.assertEqual(image['spl_default_policy'], 'default_bar') # Image Deletion should work path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # This image should be no longer be directly accessible path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.NOT_FOUND, response.status_code) self.stop_servers() def test_property_protections_special_chars_roles(self): # Enable property protection self.api_server.property_protection_file = self.property_file_roles self.start_servers(**self.__dict__.copy()) # Verify both admin and unknown role can create properties marked with # '@' path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) data = jsonutils.dumps({ 'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki', 'x_all_permitted_admin': '1' }) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'x_all_permitted_admin': '1', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'reader,member,joe_soap'}) data = jsonutils.dumps({ 'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki', 'x_all_permitted_joe_soap': '1' }) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'x_all_permitted_joe_soap': '1', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Verify both admin and unknown role can read properties marked with # '@' headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) self.assertEqual('1', image['x_all_permitted_joe_soap']) headers = self._headers({'content-type': 'application/json', 'X-Roles': 'reader,member,joe_soap'}) path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) self.assertEqual('1', image['x_all_permitted_joe_soap']) # Verify both admin and unknown role can update properties marked with # '@' path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'admin'}) data = jsonutils.dumps([ {'op': 'replace', 'path': '/x_all_permitted_joe_soap', 'value': '2'} ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) image = jsonutils.loads(response.text) self.assertEqual('2', image['x_all_permitted_joe_soap']) path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'reader,member,joe_soap'}) data = jsonutils.dumps([ {'op': 'replace', 'path': '/x_all_permitted_joe_soap', 'value': '3'} ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) image = jsonutils.loads(response.text) self.assertEqual('3', image['x_all_permitted_joe_soap']) # Verify both admin and unknown role can delete properties marked with # '@' path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) data = jsonutils.dumps({ 'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki', 'x_all_permitted_a': '1', 'x_all_permitted_b': '2' }) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'admin'}) data = jsonutils.dumps([ {'op': 'remove', 'path': '/x_all_permitted_a'} ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) image = jsonutils.loads(response.text) self.assertNotIn('x_all_permitted_a', image.keys()) path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'reader,member,joe_soap'}) data = jsonutils.dumps([ {'op': 'remove', 'path': '/x_all_permitted_b'} ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) image = jsonutils.loads(response.text) self.assertNotIn('x_all_permitted_b', image.keys()) # Verify neither admin nor unknown role can create a property protected # with '!' path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) data = jsonutils.dumps({ 'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki', 'x_none_permitted_admin': '1' }) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'reader,member,joe_soap'}) data = jsonutils.dumps({ 'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki', 'x_none_permitted_joe_soap': '1' }) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code) # Verify neither admin nor unknown role can read properties marked with # '!' path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) data = jsonutils.dumps({ 'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki', 'x_none_read': '1' }) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] self.assertNotIn('x_none_read', image.keys()) headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) self.assertNotIn('x_none_read', image.keys()) headers = self._headers({'content-type': 'application/json', 'X-Roles': 'reader,member,joe_soap'}) path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) self.assertNotIn('x_none_read', image.keys()) # Verify neither admin nor unknown role can update properties marked # with '!' path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) data = jsonutils.dumps({ 'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki', 'x_none_update': '1' }) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] self.assertEqual('1', image['x_none_update']) path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'admin'}) data = jsonutils.dumps([ {'op': 'replace', 'path': '/x_none_update', 'value': '2'} ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code, response.text) path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'reader,member,joe_soap'}) data = jsonutils.dumps([ {'op': 'replace', 'path': '/x_none_update', 'value': '3'} ]) response = requests.patch(path, headers=headers, data=data) # FIXME(danms): This was expecting CONFLICT, but ... should it # not be the same as the admin case above? self.assertEqual(http.FORBIDDEN, response.status_code, response.text) # Verify neither admin nor unknown role can delete properties marked # with '!' path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) data = jsonutils.dumps({ 'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki', 'x_none_delete': '1', }) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'admin'}) data = jsonutils.dumps([ {'op': 'remove', 'path': '/x_none_delete'} ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code, response.text) path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'reader,member,joe_soap'}) data = jsonutils.dumps([ {'op': 'remove', 'path': '/x_none_delete'} ]) response = requests.patch(path, headers=headers, data=data) # FIXME(danms): This was expecting CONFLICT, but ... should it # not be the same as the admin case above? self.assertEqual(http.FORBIDDEN, response.status_code, response.text) self.stop_servers() def test_property_protections_special_chars_policies(self): # Enable property protection self.api_server.property_protection_file = self.property_file_policies self.api_server.property_protection_rule_format = 'policies' self.start_servers(**self.__dict__.copy()) # Verify both admin and unknown role can create properties marked with # '@' path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) data = jsonutils.dumps({ 'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki', 'x_all_permitted_admin': '1' }) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'x_all_permitted_admin': '1', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'reader,member,joe_soap'}) data = jsonutils.dumps({ 'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki', 'x_all_permitted_joe_soap': '1' }) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'x_all_permitted_joe_soap': '1', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Verify both admin and unknown role can read properties marked with # '@' headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) self.assertEqual('1', image['x_all_permitted_joe_soap']) headers = self._headers({'content-type': 'application/json', 'X-Roles': 'reader,member,joe_soap'}) path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) self.assertEqual('1', image['x_all_permitted_joe_soap']) # Verify both admin and unknown role can update properties marked with # '@' path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'admin'}) data = jsonutils.dumps([ {'op': 'replace', 'path': '/x_all_permitted_joe_soap', 'value': '2'} ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) image = jsonutils.loads(response.text) self.assertEqual('2', image['x_all_permitted_joe_soap']) path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'reader,member,joe_soap'}) data = jsonutils.dumps([ {'op': 'replace', 'path': '/x_all_permitted_joe_soap', 'value': '3'} ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) image = jsonutils.loads(response.text) self.assertEqual('3', image['x_all_permitted_joe_soap']) # Verify both admin and unknown role can delete properties marked with # '@' path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) data = jsonutils.dumps({ 'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki', 'x_all_permitted_a': '1', 'x_all_permitted_b': '2' }) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'admin'}) data = jsonutils.dumps([ {'op': 'remove', 'path': '/x_all_permitted_a'} ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) image = jsonutils.loads(response.text) self.assertNotIn('x_all_permitted_a', image.keys()) path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'reader,member,joe_soap'}) data = jsonutils.dumps([ {'op': 'remove', 'path': '/x_all_permitted_b'} ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) image = jsonutils.loads(response.text) self.assertNotIn('x_all_permitted_b', image.keys()) # Verify neither admin nor unknown role can create a property protected # with '!' path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) data = jsonutils.dumps({ 'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki', 'x_none_permitted_admin': '1' }) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'reader,member,joe_soap'}) data = jsonutils.dumps({ 'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki', 'x_none_permitted_joe_soap': '1' }) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code) # Verify neither admin nor unknown role can read properties marked with # '!' path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) data = jsonutils.dumps({ 'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki', 'x_none_read': '1' }) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] self.assertNotIn('x_none_read', image.keys()) headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) self.assertNotIn('x_none_read', image.keys()) headers = self._headers({'content-type': 'application/json', 'X-Roles': 'reader,member,joe_soap'}) path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) self.assertNotIn('x_none_read', image.keys()) # Verify neither admin nor unknown role can update properties marked # with '!' path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) data = jsonutils.dumps({ 'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki', 'x_none_update': '1' }) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] self.assertEqual('1', image['x_none_update']) path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'admin'}) data = jsonutils.dumps([ {'op': 'replace', 'path': '/x_none_update', 'value': '2'} ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code, response.text) path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'reader,member,joe_soap'}) data = jsonutils.dumps([ {'op': 'replace', 'path': '/x_none_update', 'value': '3'} ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.CONFLICT, response.status_code, response.text) # Verify neither admin nor unknown role can delete properties marked # with '!' path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) data = jsonutils.dumps({ 'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki', 'x_none_delete': '1', }) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'admin'}) data = jsonutils.dumps([ {'op': 'remove', 'path': '/x_none_delete'} ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.FORBIDDEN, response.status_code, response.text) path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type, 'X-Roles': 'reader,member,joe_soap'}) data = jsonutils.dumps([ {'op': 'remove', 'path': '/x_none_delete'} ]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.CONFLICT, response.status_code, response.text) self.stop_servers() def test_tag_lifecycle(self): self.start_servers(**self.__dict__.copy()) # Create an image with a tag - duplicate should be ignored path = self._url('/v2/images') headers = self._headers({'Content-Type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'tags': ['sniff', 'sniff']}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image_id = jsonutils.loads(response.text)['id'] # Image should show a list with a single tag path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) tags = jsonutils.loads(response.text)['tags'] self.assertEqual(['sniff'], tags) # Delete all tags for tag in tags: path = self._url('/v2/images/%s/tags/%s' % (image_id, tag)) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Update image with too many tags via PUT # Configured limit is 10 tags for i in range(10): path = self._url('/v2/images/%s/tags/foo%i' % (image_id, i)) response = requests.put(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # 11th tag should fail path = self._url('/v2/images/%s/tags/fail_me' % image_id) response = requests.put(path, headers=self._headers()) self.assertEqual(http.REQUEST_ENTITY_TOO_LARGE, response.status_code) # Make sure the 11th tag was not added path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) tags = jsonutils.loads(response.text)['tags'] self.assertEqual(10, len(tags)) # Update image tags via PATCH path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) doc = [ { 'op': 'replace', 'path': '/tags', 'value': ['foo'], }, ] data = jsonutils.dumps(doc) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code) # Update image with too many tags via PATCH # Configured limit is 10 tags path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) tags = ['foo%d' % i for i in range(11)] doc = [ { 'op': 'replace', 'path': '/tags', 'value': tags, }, ] data = jsonutils.dumps(doc) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.REQUEST_ENTITY_TOO_LARGE, response.status_code) # Tags should not have changed since request was over limit path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) tags = jsonutils.loads(response.text)['tags'] self.assertEqual(['foo'], tags) # Update image with duplicate tag - it should be ignored path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) doc = [ { 'op': 'replace', 'path': '/tags', 'value': ['sniff', 'snozz', 'snozz'], }, ] data = jsonutils.dumps(doc) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code) tags = jsonutils.loads(response.text)['tags'] self.assertEqual(['sniff', 'snozz'], sorted(tags)) # Image should show the appropriate tags path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) tags = jsonutils.loads(response.text)['tags'] self.assertEqual(['sniff', 'snozz'], sorted(tags)) # Attempt to tag the image with a duplicate should be ignored path = self._url('/v2/images/%s/tags/snozz' % image_id) response = requests.put(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Create another more complex tag path = self._url('/v2/images/%s/tags/gabe%%40example.com' % image_id) response = requests.put(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Double-check that the tags container on the image is populated path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) tags = jsonutils.loads(response.text)['tags'] self.assertEqual(['gabe@example.com', 'sniff', 'snozz'], sorted(tags)) # Query images by single tag path = self._url('/v2/images?tag=sniff') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual('image-1', images[0]['name']) # Query images by multiple tags path = self._url('/v2/images?tag=sniff&tag=snozz') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual('image-1', images[0]['name']) # Query images by tag and other attributes path = self._url('/v2/images?tag=sniff&status=queued') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual('image-1', images[0]['name']) # Query images by tag and a nonexistent tag path = self._url('/v2/images?tag=sniff&tag=fake') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # The tag should be deletable path = self._url('/v2/images/%s/tags/gabe%%40example.com' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # List of tags should reflect the deletion path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) tags = jsonutils.loads(response.text)['tags'] self.assertEqual(['sniff', 'snozz'], sorted(tags)) # Deleting the same tag should return a 404 path = self._url('/v2/images/%s/tags/gabe%%40example.com' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NOT_FOUND, response.status_code) # The tags won't be able to query the images after deleting path = self._url('/v2/images?tag=gabe%%40example.com') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # Try to add a tag that is too long big_tag = 'a' * 300 path = self._url('/v2/images/%s/tags/%s' % (image_id, big_tag)) response = requests.put(path, headers=self._headers()) self.assertEqual(http.BAD_REQUEST, response.status_code) # Tags should not have changed since request was over limit path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) tags = jsonutils.loads(response.text)['tags'] self.assertEqual(['sniff', 'snozz'], sorted(tags)) self.stop_servers() def test_images_container(self): # Image list should be empty and no next link should be present self.start_servers(**self.__dict__.copy()) path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] first = jsonutils.loads(response.text)['first'] self.assertEqual(0, len(images)) self.assertNotIn('next', jsonutils.loads(response.text)) self.assertEqual('/v2/images', first) # Create 7 images images = [] fixtures = [ {'name': 'image-3', 'type': 'kernel', 'ping': 'pong', 'container_format': 'ami', 'disk_format': 'ami'}, {'name': 'image-4', 'type': 'kernel', 'ping': 'pong', 'container_format': 'bare', 'disk_format': 'ami'}, {'name': 'image-1', 'type': 'kernel', 'ping': 'pong'}, {'name': 'image-3', 'type': 'ramdisk', 'ping': 'pong'}, {'name': 'image-2', 'type': 'kernel', 'ping': 'ding'}, {'name': 'image-3', 'type': 'kernel', 'ping': 'pong'}, {'name': 'image-2,image-5', 'type': 'kernel', 'ping': 'pong'}, ] path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) for fixture in fixtures: data = jsonutils.dumps(fixture) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) images.append(jsonutils.loads(response.text)) # Image list should contain 7 images path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertEqual(7, len(body['images'])) self.assertEqual('/v2/images', body['first']) self.assertNotIn('next', jsonutils.loads(response.text)) # Image list filters by created_at time url_template = '/v2/images?created_at=lt:%s' path = self._url(url_template % images[0]['created_at']) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertEqual(0, len(body['images'])) self.assertEqual(url_template % images[0]['created_at'], urllib.parse.unquote(body['first'])) # Image list filters by updated_at time url_template = '/v2/images?updated_at=lt:%s' path = self._url(url_template % images[2]['updated_at']) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertGreaterEqual(3, len(body['images'])) self.assertEqual(url_template % images[2]['updated_at'], urllib.parse.unquote(body['first'])) # Image list filters by updated_at and created time with invalid value url_template = '/v2/images?%s=lt:invalid_value' for filter in ['updated_at', 'created_at']: path = self._url(url_template % filter) response = requests.get(path, headers=self._headers()) self.assertEqual(http.BAD_REQUEST, response.status_code) # Image list filters by updated_at and created_at with invalid operator url_template = '/v2/images?%s=invalid_operator:2015-11-19T12:24:02Z' for filter in ['updated_at', 'created_at']: path = self._url(url_template % filter) response = requests.get(path, headers=self._headers()) self.assertEqual(http.BAD_REQUEST, response.status_code) # Image list filters by non-'URL encoding' value path = self._url('/v2/images?name=%FF') response = requests.get(path, headers=self._headers()) self.assertEqual(http.BAD_REQUEST, response.status_code) # Image list filters by name with in operator url_template = '/v2/images?name=in:%s' filter_value = 'image-1,image-2' path = self._url(url_template % filter_value) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertGreaterEqual(3, len(body['images'])) # Image list filters by container_format with in operator url_template = '/v2/images?container_format=in:%s' filter_value = 'bare,ami' path = self._url(url_template % filter_value) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertGreaterEqual(2, len(body['images'])) # Image list filters by disk_format with in operator url_template = '/v2/images?disk_format=in:%s' filter_value = 'bare,ami,iso' path = self._url(url_template % filter_value) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertGreaterEqual(2, len(body['images'])) # Begin pagination after the first image template_url = ('/v2/images?limit=2&sort_dir=asc&sort_key=name' '&marker=%s&type=kernel&ping=pong') path = self._url(template_url % images[2]['id']) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertEqual(2, len(body['images'])) response_ids = [image['id'] for image in body['images']] self.assertEqual([images[6]['id'], images[0]['id']], response_ids) # Continue pagination using next link from previous request path = self._url(body['next']) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertEqual(2, len(body['images'])) response_ids = [image['id'] for image in body['images']] self.assertEqual([images[5]['id'], images[1]['id']], response_ids) # Continue pagination - expect no results path = self._url(body['next']) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertEqual(0, len(body['images'])) # Delete first image path = self._url('/v2/images/%s' % images[0]['id']) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Ensure bad request for using a deleted image as marker path = self._url('/v2/images?marker=%s' % images[0]['id']) response = requests.get(path, headers=self._headers()) self.assertEqual(http.BAD_REQUEST, response.status_code) self.stop_servers() def test_image_visibility_to_different_users(self): self.cleanup() self.api_server.deployment_flavor = 'fakeauth' kwargs = self.__dict__.copy() self.start_servers(**kwargs) owners = ['admin', 'tenant1', 'tenant2', 'none'] visibilities = ['public', 'private', 'shared', 'community'] for owner in owners: for visibility in visibilities: path = self._url('/v2/images') headers = self._headers({ 'content-type': 'application/json', 'X-Auth-Token': 'createuser:%s:admin' % owner, }) data = jsonutils.dumps({ 'name': '%s-%s' % (owner, visibility), 'visibility': visibility, }) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) def list_images(tenant, role='', visibility=None): auth_token = 'user:%s:%s' % (tenant, role) headers = {'X-Auth-Token': auth_token} path = self._url('/v2/images') if visibility is not None: path += '?visibility=%s' % visibility response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) return jsonutils.loads(response.text)['images'] # 1. Known user sees public and their own images images = list_images('tenant1', role='reader') self.assertEqual(7, len(images)) for image in images: self.assertTrue(image['visibility'] == 'public' or 'tenant1' in image['name']) # 2. Known user, visibility=public, sees all public images images = list_images('tenant1', role='reader', visibility='public') self.assertEqual(4, len(images)) for image in images: self.assertEqual('public', image['visibility']) # 3. Known user, visibility=private, sees only their private image images = list_images('tenant1', role='reader', visibility='private') self.assertEqual(1, len(images)) image = images[0] self.assertEqual('private', image['visibility']) self.assertIn('tenant1', image['name']) # 4. Known user, visibility=shared, sees only their shared image images = list_images('tenant1', role='reader', visibility='shared') self.assertEqual(1, len(images)) image = images[0] self.assertEqual('shared', image['visibility']) self.assertIn('tenant1', image['name']) # 5. Known user, visibility=community, sees all community images images = list_images('tenant1', role='reader', visibility='community') self.assertEqual(4, len(images)) for image in images: self.assertEqual('community', image['visibility']) # 6. Unknown user sees only public images images = list_images('none', role='reader') self.assertEqual(4, len(images)) for image in images: self.assertEqual('public', image['visibility']) # 7. Unknown user, visibility=public, sees only public images images = list_images('none', role='reader', visibility='public') self.assertEqual(4, len(images)) for image in images: self.assertEqual('public', image['visibility']) # 8. Unknown user, visibility=private, sees no images images = list_images('none', role='reader', visibility='private') self.assertEqual(0, len(images)) # 9. Unknown user, visibility=shared, sees no images images = list_images('none', role='reader', visibility='shared') self.assertEqual(0, len(images)) # 10. Unknown user, visibility=community, sees only community images images = list_images('none', role='reader', visibility='community') self.assertEqual(4, len(images)) for image in images: self.assertEqual('community', image['visibility']) # 11. Unknown admin sees all images except for community images images = list_images('none', role='admin') self.assertEqual(12, len(images)) # 12. Unknown admin, visibility=public, shows only public images images = list_images('none', role='admin', visibility='public') self.assertEqual(4, len(images)) for image in images: self.assertEqual('public', image['visibility']) # 13. Unknown admin, visibility=private, sees only private images images = list_images('none', role='admin', visibility='private') self.assertEqual(4, len(images)) for image in images: self.assertEqual('private', image['visibility']) # 14. Unknown admin, visibility=shared, sees only shared images images = list_images('none', role='admin', visibility='shared') self.assertEqual(4, len(images)) for image in images: self.assertEqual('shared', image['visibility']) # 15. Unknown admin, visibility=community, sees only community images images = list_images('none', role='admin', visibility='community') self.assertEqual(4, len(images)) for image in images: self.assertEqual('community', image['visibility']) # 16. Known admin sees all images, except community images owned by # others images = list_images('admin', role='admin') self.assertEqual(13, len(images)) # 17. Known admin, visibility=public, sees all public images images = list_images('admin', role='admin', visibility='public') self.assertEqual(4, len(images)) for image in images: self.assertEqual('public', image['visibility']) # 18. Known admin, visibility=private, sees all private images images = list_images('admin', role='admin', visibility='private') self.assertEqual(4, len(images)) for image in images: self.assertEqual('private', image['visibility']) # 19. Known admin, visibility=shared, sees all shared images images = list_images('admin', role='admin', visibility='shared') self.assertEqual(4, len(images)) for image in images: self.assertEqual('shared', image['visibility']) # 20. Known admin, visibility=community, sees all community images images = list_images('admin', role='admin', visibility='community') self.assertEqual(4, len(images)) for image in images: self.assertEqual('community', image['visibility']) self.stop_servers() def test_update_locations(self): self.api_server.show_multiple_locations = True self.start_servers(**self.__dict__.copy()) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Returned image entity should have a generated id and status image = jsonutils.loads(response.text) image_id = image['id'] self.assertEqual('queued', image['status']) self.assertIsNone(image['size']) self.assertIsNone(image['virtual_size']) # Update locations for the queued image path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) url = 'http://127.0.0.1:%s/foo_image' % self.http_port0 data = jsonutils.dumps([{'op': 'replace', 'path': '/locations', 'value': [{'url': url, 'metadata': {}}] }]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) # The image size should be updated path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) self.assertEqual(10, image['size']) def test_update_locations_with_restricted_sources(self): self.api_server.show_multiple_locations = True self.start_servers(**self.__dict__.copy()) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Returned image entity should have a generated id and status image = jsonutils.loads(response.text) image_id = image['id'] self.assertEqual('queued', image['status']) self.assertIsNone(image['size']) self.assertIsNone(image['virtual_size']) # Update locations for the queued image path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) data = jsonutils.dumps([{'op': 'replace', 'path': '/locations', 'value': [{'url': 'file:///foo_image', 'metadata': {}}] }]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.BAD_REQUEST, response.status_code, response.text) data = jsonutils.dumps([{'op': 'replace', 'path': '/locations', 'value': [{'url': 'swift+config:///foo_image', 'metadata': {}}] }]) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.BAD_REQUEST, response.status_code, response.text) class TestImagesIPv6(functional.FunctionalTest): """Verify that API and REG servers running IPv6 can communicate""" def setUp(self): """ First applying monkey patches of functions and methods which have IPv4 hardcoded. """ # Setting up initial monkey patch (1) test_utils.get_unused_port_ipv4 = test_utils.get_unused_port test_utils.get_unused_port_and_socket_ipv4 = ( test_utils.get_unused_port_and_socket) test_utils.get_unused_port = test_utils.get_unused_port_ipv6 test_utils.get_unused_port_and_socket = ( test_utils.get_unused_port_and_socket_ipv6) super(TestImagesIPv6, self).setUp() self.cleanup() # Setting up monkey patch (2), after object is ready... self.ping_server_ipv4 = self.ping_server self.ping_server = self.ping_server_ipv6 self.include_scrubber = False def tearDown(self): # Cleaning up monkey patch (2). self.ping_server = self.ping_server_ipv4 super(TestImagesIPv6, self).tearDown() # Cleaning up monkey patch (1). test_utils.get_unused_port = test_utils.get_unused_port_ipv4 test_utils.get_unused_port_and_socket = ( test_utils.get_unused_port_and_socket_ipv4) def _headers(self, custom_headers=None): base_headers = { 'X-Identity-Status': 'Confirmed', 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96', 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e', 'X-Tenant-Id': TENANT1, 'X-Roles': 'reader,member', } base_headers.update(custom_headers or {}) return base_headers def test_image_list_ipv6(self): # Image list should be empty self.api_server.deployment_flavor = "caching" self.api_server.send_identity_credentials = True self.start_servers(**self.__dict__.copy()) url = f'http://[::1]:{self.api_port}' path = '/' requests.get(url + path, headers=self._headers()) path = '/v2/images' response = requests.get(url + path, headers=self._headers()) self.assertEqual(200, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) class TestImageDirectURLVisibility(functional.FunctionalTest): def setUp(self): super(TestImageDirectURLVisibility, self).setUp() self.cleanup() self.include_scrubber = False self.api_server.deployment_flavor = 'noauth' def _headers(self, custom_headers=None): base_headers = { 'X-Identity-Status': 'Confirmed', 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96', 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e', 'X-Tenant-Id': TENANT1, 'X-Roles': 'reader,member', } base_headers.update(custom_headers or {}) return base_headers def test_image_direct_url_visible(self): self.api_server.show_image_direct_url = True self.start_servers(**self.__dict__.copy()) # Image list should be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json', 'X-Roles': 'admin'}) data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', 'foo': 'bar', 'disk_format': 'aki', 'container_format': 'aki', 'visibility': 'public'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Get the image id image = jsonutils.loads(response.text) image_id = image['id'] # Image direct_url should not be visible before location is set path = self._url('/v2/images/%s' % image_id) headers = self._headers({'Content-Type': 'application/json'}) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) self.assertNotIn('direct_url', image) # Upload some image data, setting the image location path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) response = requests.put(path, headers=headers, data='ZZZZZ') self.assertEqual(http.NO_CONTENT, response.status_code) # Image direct_url should be visible path = self._url('/v2/images/%s' % image_id) headers = self._headers({'Content-Type': 'application/json'}) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) self.assertIn('direct_url', image) # Image direct_url should be visible to non-owner, non-admin user path = self._url('/v2/images/%s' % image_id) headers = self._headers({'Content-Type': 'application/json', 'X-Tenant-Id': TENANT2}) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) self.assertIn('direct_url', image) # Image direct_url should be visible in a list path = self._url('/v2/images') headers = self._headers({'Content-Type': 'application/json'}) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text)['images'][0] self.assertIn('direct_url', image) self.stop_servers() def test_image_multiple_location_url_visible(self): self.api_server.show_multiple_locations = True self.start_servers(**self.__dict__.copy()) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', 'foo': 'bar', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Get the image id image = jsonutils.loads(response.text) image_id = image['id'] # Image locations should not be visible before location is set path = self._url('/v2/images/%s' % image_id) headers = self._headers({'Content-Type': 'application/json'}) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) self.assertIn('locations', image) self.assertEqual([], image["locations"]) # Upload some image data, setting the image location path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) response = requests.put(path, headers=headers, data='ZZZZZ') self.assertEqual(http.NO_CONTENT, response.status_code) # Image locations should be visible path = self._url('/v2/images/%s' % image_id) headers = self._headers({'Content-Type': 'application/json'}) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) self.assertIn('locations', image) loc = image['locations'] self.assertGreater(len(loc), 0) loc = loc[0] self.assertIn('url', loc) self.assertIn('metadata', loc) self.stop_servers() def test_image_direct_url_not_visible(self): self.api_server.show_image_direct_url = False self.start_servers(**self.__dict__.copy()) # Image list should be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', 'foo': 'bar', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Get the image id image = jsonutils.loads(response.text) image_id = image['id'] # Upload some image data, setting the image location path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) response = requests.put(path, headers=headers, data='ZZZZZ') self.assertEqual(http.NO_CONTENT, response.status_code) # Image direct_url should not be visible path = self._url('/v2/images/%s' % image_id) headers = self._headers({'Content-Type': 'application/json'}) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) self.assertNotIn('direct_url', image) # Image direct_url should not be visible in a list path = self._url('/v2/images') headers = self._headers({'Content-Type': 'application/json'}) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text)['images'][0] self.assertNotIn('direct_url', image) self.stop_servers() class TestImageLocationSelectionStrategy(functional.FunctionalTest): def setUp(self): super(TestImageLocationSelectionStrategy, self).setUp() self.cleanup() self.include_scrubber = False self.api_server.deployment_flavor = 'noauth' for i in range(3): ret = test_utils.start_http_server("foo_image_id%d" % i, "foo_image%d" % i) setattr(self, 'http_server%d' % i, ret[1]) setattr(self, 'http_port%d' % i, ret[2]) def tearDown(self): for i in range(3): httpd = getattr(self, 'http_server%d' % i, None) if httpd: httpd.shutdown() httpd.server_close() super(TestImageLocationSelectionStrategy, self).tearDown() def _headers(self, custom_headers=None): base_headers = { 'X-Identity-Status': 'Confirmed', 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96', 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e', 'X-Tenant-Id': TENANT1, 'X-Roles': 'reader,member', } base_headers.update(custom_headers or {}) return base_headers def test_image_locations_with_order_strategy(self): self.api_server.show_image_direct_url = True self.api_server.show_multiple_locations = True self.image_location_quota = 10 self.api_server.location_strategy = 'location_order' preference = "http, swift, filesystem" self.api_server.store_type_location_strategy_preference = preference self.start_servers(**self.__dict__.copy()) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', 'foo': 'bar', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Get the image id image = jsonutils.loads(response.text) image_id = image['id'] # Image locations should not be visible before location is set path = self._url('/v2/images/%s' % image_id) headers = self._headers({'Content-Type': 'application/json'}) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) self.assertIn('locations', image) self.assertEqual([], image["locations"]) # Update image locations via PATCH path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) values = [{'url': 'http://127.0.0.1:%s/foo_image' % self.http_port0, 'metadata': {}}, {'url': 'http://127.0.0.1:%s/foo_image' % self.http_port1, 'metadata': {}}] doc = [{'op': 'replace', 'path': '/locations', 'value': values}] data = jsonutils.dumps(doc) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code) # Image locations should be visible path = self._url('/v2/images/%s' % image_id) headers = self._headers({'Content-Type': 'application/json'}) response = requests.get(path, headers=headers) self.assertEqual(http.OK, response.status_code) image = jsonutils.loads(response.text) self.assertIn('locations', image) self.assertEqual(values, image['locations']) self.assertIn('direct_url', image) self.assertEqual(values[0]['url'], image['direct_url']) self.stop_servers() class TestImageMembers(functional.FunctionalTest): def setUp(self): super(TestImageMembers, self).setUp() self.cleanup() self.include_scrubber = False self.api_server.deployment_flavor = 'fakeauth' self.start_servers(**self.__dict__.copy()) def _headers(self, custom_headers=None): base_headers = { 'X-Identity-Status': 'Confirmed', 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96', 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e', 'X-Tenant-Id': TENANT1, 'X-Roles': 'reader,member', } base_headers.update(custom_headers or {}) return base_headers def test_image_member_lifecycle(self): # Image list should be empty path = self._url('/v2/images') response = requests.get(path, headers=get_auth_header('tenant1')) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) owners = ['tenant1', 'tenant2', 'admin'] visibilities = ['community', 'private', 'public', 'shared'] image_fixture = [] for owner in owners: for visibility in visibilities: path = self._url('/v2/images') role = 'member' if visibility == 'public': role = 'admin' headers = self._headers({ 'content-type': 'application/json', 'X-Auth-Token': 'createuser:%s:admin' % owner, 'X-Roles': role, }) data = jsonutils.dumps({ 'name': '%s-%s' % (owner, visibility), 'visibility': visibility, }) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image_fixture.append(jsonutils.loads(response.text)) # Image list should contain 6 images for tenant1 path = self._url('/v2/images') response = requests.get(path, headers=get_auth_header('tenant1')) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(6, len(images)) # Image list should contain 3 images for TENANT3 path = self._url('/v2/images') response = requests.get(path, headers=get_auth_header(TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(3, len(images)) # Add Image member for tenant1-shared image path = self._url('/v2/images/%s/members' % image_fixture[3]['id']) body = jsonutils.dumps({'member': TENANT3}) response = requests.post(path, headers=get_auth_header('tenant1'), data=body) self.assertEqual(http.OK, response.status_code) image_member = jsonutils.loads(response.text) self.assertEqual(image_fixture[3]['id'], image_member['image_id']) self.assertEqual(TENANT3, image_member['member_id']) self.assertIn('created_at', image_member) self.assertIn('updated_at', image_member) self.assertEqual('pending', image_member['status']) # Image list should contain 3 images for TENANT3 path = self._url('/v2/images') response = requests.get(path, headers=get_auth_header(TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(3, len(images)) # Image list should contain 0 shared images for TENANT3 # because default is accepted path = self._url('/v2/images?visibility=shared') response = requests.get(path, headers=get_auth_header(TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # Image list should contain 4 images for TENANT3 with status pending path = self._url('/v2/images?member_status=pending') response = requests.get(path, headers=get_auth_header(TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(4, len(images)) # Image list should contain 4 images for TENANT3 with status all path = self._url('/v2/images?member_status=all') response = requests.get(path, headers=get_auth_header(TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(4, len(images)) # Image list should contain 1 image for TENANT3 with status pending # and visibility shared path = self._url('/v2/images?member_status=pending&visibility=shared') response = requests.get(path, headers=get_auth_header(TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(images[0]['name'], 'tenant1-shared') # Image list should contain 0 image for TENANT3 with status rejected # and visibility shared path = self._url('/v2/images?member_status=rejected&visibility=shared') response = requests.get(path, headers=get_auth_header(TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # Image list should contain 0 image for TENANT3 with status accepted # and visibility shared path = self._url('/v2/images?member_status=accepted&visibility=shared') response = requests.get(path, headers=get_auth_header(TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # Image list should contain 0 image for TENANT3 with status accepted # and visibility private path = self._url('/v2/images?visibility=private') response = requests.get(path, headers=get_auth_header(TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # Image tenant2-shared's image members list should contain no members path = self._url('/v2/images/%s/members' % image_fixture[7]['id']) response = requests.get(path, headers=get_auth_header('tenant2')) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertEqual(0, len(body['members'])) # Tenant 1, who is the owner cannot change status of image member path = self._url('/v2/images/%s/members/%s' % (image_fixture[3]['id'], TENANT3)) body = jsonutils.dumps({'status': 'accepted'}) response = requests.put(path, headers=get_auth_header('tenant1'), data=body) self.assertEqual(http.FORBIDDEN, response.status_code) # Tenant 1, who is the owner can get status of its own image member path = self._url('/v2/images/%s/members/%s' % (image_fixture[3]['id'], TENANT3)) response = requests.get(path, headers=get_auth_header('tenant1')) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertEqual('pending', body['status']) self.assertEqual(image_fixture[3]['id'], body['image_id']) self.assertEqual(TENANT3, body['member_id']) # Tenant 3, who is the member can get status of its own status path = self._url('/v2/images/%s/members/%s' % (image_fixture[3]['id'], TENANT3)) response = requests.get(path, headers=get_auth_header(TENANT3)) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertEqual('pending', body['status']) self.assertEqual(image_fixture[3]['id'], body['image_id']) self.assertEqual(TENANT3, body['member_id']) # Tenant 2, who not the owner cannot get status of image member path = self._url('/v2/images/%s/members/%s' % (image_fixture[3]['id'], TENANT3)) response = requests.get(path, headers=get_auth_header('tenant2')) self.assertEqual(http.NOT_FOUND, response.status_code) # Tenant 3 can change status of image member path = self._url('/v2/images/%s/members/%s' % (image_fixture[3]['id'], TENANT3)) body = jsonutils.dumps({'status': 'accepted'}) response = requests.put(path, headers=get_auth_header(TENANT3), data=body) self.assertEqual(http.OK, response.status_code) image_member = jsonutils.loads(response.text) self.assertEqual(image_fixture[3]['id'], image_member['image_id']) self.assertEqual(TENANT3, image_member['member_id']) self.assertEqual('accepted', image_member['status']) # Image list should contain 4 images for TENANT3 because status is # accepted path = self._url('/v2/images') response = requests.get(path, headers=get_auth_header(TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(4, len(images)) # Tenant 3 invalid status change path = self._url('/v2/images/%s/members/%s' % (image_fixture[3]['id'], TENANT3)) body = jsonutils.dumps({'status': 'invalid-status'}) response = requests.put(path, headers=get_auth_header(TENANT3), data=body) self.assertEqual(http.BAD_REQUEST, response.status_code) # Owner cannot change status of image path = self._url('/v2/images/%s/members/%s' % (image_fixture[3]['id'], TENANT3)) body = jsonutils.dumps({'status': 'accepted'}) response = requests.put(path, headers=get_auth_header('tenant1'), data=body) self.assertEqual(http.FORBIDDEN, response.status_code) # Add Image member for tenant2-shared image path = self._url('/v2/images/%s/members' % image_fixture[7]['id']) body = jsonutils.dumps({'member': TENANT4}) response = requests.post(path, headers=get_auth_header('tenant2'), data=body) self.assertEqual(http.OK, response.status_code) image_member = jsonutils.loads(response.text) self.assertEqual(image_fixture[7]['id'], image_member['image_id']) self.assertEqual(TENANT4, image_member['member_id']) self.assertIn('created_at', image_member) self.assertIn('updated_at', image_member) # Add Image member to public image path = self._url('/v2/images/%s/members' % image_fixture[2]['id']) body = jsonutils.dumps({'member': TENANT2}) response = requests.post(path, headers=get_auth_header('tenant1'), data=body) self.assertEqual(http.FORBIDDEN, response.status_code) # Add Image member to private image path = self._url('/v2/images/%s/members' % image_fixture[1]['id']) body = jsonutils.dumps({'member': TENANT2}) response = requests.post(path, headers=get_auth_header('tenant1'), data=body) self.assertEqual(http.FORBIDDEN, response.status_code) # Add Image member to community image path = self._url('/v2/images/%s/members' % image_fixture[0]['id']) body = jsonutils.dumps({'member': TENANT2}) response = requests.post(path, headers=get_auth_header('tenant1'), data=body) self.assertEqual(http.FORBIDDEN, response.status_code) # Image tenant1-shared's members list should contain 1 member path = self._url('/v2/images/%s/members' % image_fixture[3]['id']) response = requests.get(path, headers=get_auth_header('tenant1')) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertEqual(1, len(body['members'])) # Admin can see any members path = self._url('/v2/images/%s/members' % image_fixture[3]['id']) response = requests.get(path, headers=get_auth_header('tenant1', role='admin')) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertEqual(1, len(body['members'])) # Image members not found for private image not owned by TENANT 1 path = self._url('/v2/images/%s/members' % image_fixture[7]['id']) response = requests.get(path, headers=get_auth_header('tenant1')) self.assertEqual(http.NOT_FOUND, response.status_code) # Image members forbidden for public image path = self._url('/v2/images/%s/members' % image_fixture[2]['id']) response = requests.get(path, headers=get_auth_header('tenant1')) self.assertIn("Only shared images have members", response.text) self.assertEqual(http.FORBIDDEN, response.status_code) # Image members forbidden for community image path = self._url('/v2/images/%s/members' % image_fixture[0]['id']) response = requests.get(path, headers=get_auth_header('tenant1')) self.assertIn("Only shared images have members", response.text) self.assertEqual(http.FORBIDDEN, response.status_code) # Image members forbidden for private image path = self._url('/v2/images/%s/members' % image_fixture[1]['id']) response = requests.get(path, headers=get_auth_header('tenant1')) self.assertIn("Only shared images have members", response.text) self.assertEqual(http.FORBIDDEN, response.status_code) # Image Member Cannot delete Image membership path = self._url('/v2/images/%s/members/%s' % (image_fixture[3]['id'], TENANT3)) response = requests.delete(path, headers=get_auth_header(TENANT3)) self.assertEqual(http.FORBIDDEN, response.status_code) # Delete Image member path = self._url('/v2/images/%s/members/%s' % (image_fixture[3]['id'], TENANT3)) response = requests.delete(path, headers=get_auth_header('tenant1')) self.assertEqual(http.NO_CONTENT, response.status_code) # Now the image has no members path = self._url('/v2/images/%s/members' % image_fixture[3]['id']) response = requests.get(path, headers=get_auth_header('tenant1')) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertEqual(0, len(body['members'])) # Adding 11 image members should fail since configured limit is 10 path = self._url('/v2/images/%s/members' % image_fixture[3]['id']) for i in range(10): body = jsonutils.dumps({'member': str(uuid.uuid4())}) response = requests.post(path, headers=get_auth_header('tenant1'), data=body) self.assertEqual(http.OK, response.status_code) body = jsonutils.dumps({'member': str(uuid.uuid4())}) response = requests.post(path, headers=get_auth_header('tenant1'), data=body) self.assertEqual(http.REQUEST_ENTITY_TOO_LARGE, response.status_code) # Get Image member should return not found for public image path = self._url('/v2/images/%s/members/%s' % (image_fixture[2]['id'], TENANT3)) response = requests.get(path, headers=get_auth_header('tenant1')) self.assertEqual(http.NOT_FOUND, response.status_code) # Get Image member should return not found for community image path = self._url('/v2/images/%s/members/%s' % (image_fixture[0]['id'], TENANT3)) response = requests.get(path, headers=get_auth_header('tenant1')) self.assertEqual(http.NOT_FOUND, response.status_code) # Get Image member should return not found for private image path = self._url('/v2/images/%s/members/%s' % (image_fixture[1]['id'], TENANT3)) response = requests.get(path, headers=get_auth_header('tenant1')) self.assertEqual(http.NOT_FOUND, response.status_code) # Delete Image member should return forbidden for public image path = self._url('/v2/images/%s/members/%s' % (image_fixture[2]['id'], TENANT3)) response = requests.delete(path, headers=get_auth_header('tenant1')) self.assertEqual(http.FORBIDDEN, response.status_code) # Delete Image member should return forbidden for community image path = self._url('/v2/images/%s/members/%s' % (image_fixture[0]['id'], TENANT3)) response = requests.delete(path, headers=get_auth_header('tenant1')) self.assertEqual(http.FORBIDDEN, response.status_code) # Delete Image member should return forbidden for private image path = self._url('/v2/images/%s/members/%s' % (image_fixture[1]['id'], TENANT3)) response = requests.delete(path, headers=get_auth_header('tenant1')) self.assertEqual(http.FORBIDDEN, response.status_code) self.stop_servers() class TestQuotas(functional.FunctionalTest): def setUp(self): super(TestQuotas, self).setUp() self.cleanup() self.include_scrubber = False self.api_server.deployment_flavor = 'noauth' self.user_storage_quota = 100 self.start_servers(**self.__dict__.copy()) def _headers(self, custom_headers=None): base_headers = { 'X-Identity-Status': 'Confirmed', 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96', 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e', 'X-Tenant-Id': TENANT1, 'X-Roles': 'reader,member', } base_headers.update(custom_headers or {}) return base_headers def _upload_image_test(self, data_src, expected_status): # Image list should be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # Create an image (with a deployer-defined property) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'testimg', 'type': 'kernel', 'foo': 'bar', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] # upload data path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) response = requests.put(path, headers=headers, data=data_src) self.assertEqual(expected_status, response.status_code) # Deletion should work path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) def test_image_upload_under_quota(self): data = b'x' * (self.user_storage_quota - 1) self._upload_image_test(data, http.NO_CONTENT) def test_image_upload_exceed_quota(self): data = b'x' * (self.user_storage_quota + 1) self._upload_image_test(data, http.REQUEST_ENTITY_TOO_LARGE) def test_chunked_image_upload_under_quota(self): def data_gen(): yield b'x' * (self.user_storage_quota - 1) self._upload_image_test(data_gen(), http.NO_CONTENT) def test_chunked_image_upload_exceed_quota(self): def data_gen(): yield b'x' * (self.user_storage_quota + 1) self._upload_image_test(data_gen(), http.REQUEST_ENTITY_TOO_LARGE) class TestImagesMultipleBackend(functional.MultipleBackendFunctionalTest): def setUp(self): super(TestImagesMultipleBackend, self).setUp() self.cleanup() self.include_scrubber = False self.api_server_multiple_backend.deployment_flavor = 'noauth' for i in range(3): ret = test_utils.start_http_server("foo_image_id%d" % i, "foo_image%d" % i) setattr(self, 'http_server%d' % i, ret[1]) setattr(self, 'http_port%d' % i, ret[2]) def tearDown(self): for i in range(3): httpd = getattr(self, 'http_server%d' % i, None) if httpd: httpd.shutdown() httpd.server_close() super(TestImagesMultipleBackend, self).tearDown() def _headers(self, custom_headers=None): base_headers = { 'X-Identity-Status': 'Confirmed', 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96', 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e', 'X-Tenant-Id': TENANT1, 'X-Roles': 'reader,member', } base_headers.update(custom_headers or {}) return base_headers def test_image_import_using_glance_direct(self): self.start_servers(**self.__dict__.copy()) # Image list should be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # glance-direct should be available in discovery response path = self._url('/v2/info/import') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['import-methods']['value'] self.assertIn("glance-direct", discovery_calls) # file1 and file2 should be available in discovery response available_stores = ['file1', 'file2', 'file3'] path = self._url('/v2/info/stores') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['stores'] # os_glance_staging_store should not be available in discovery response for stores in discovery_calls: self.assertIn('id', stores) self.assertIn(stores['id'], available_stores) self.assertFalse(stores["id"].startswith("os_glance_")) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Check 'OpenStack-image-store-ids' header present in response self.assertIn('OpenStack-image-store-ids', response.headers) for store in available_stores: self.assertIn(store, response.headers['OpenStack-image-store-ids']) # Returned image entity should have a generated id and status image = jsonutils.loads(response.text) image_id = image['id'] checked_keys = set([ u'status', u'name', u'tags', u'created_at', u'updated_at', u'visibility', u'self', u'protected', u'id', u'file', u'min_disk', u'type', u'min_ram', u'schema', u'disk_format', u'container_format', u'owner', u'checksum', u'size', u'virtual_size', u'os_hidden', u'os_hash_algo', u'os_hash_value' ]) self.assertEqual(checked_keys, set(image.keys())) expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'type': 'kernel', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Image list should now have one entry path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # Upload some image data to staging area image_data = b'QQQQQ' path = self._url('/v2/images/%s/stage' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) response = requests.put(path, headers=headers, data=image_data) self.assertEqual(http.NO_CONTENT, response.status_code) # Verify image is in uploading state and checksum is None func_utils.verify_image_hashes_and_status(self, image_id, size=len(image_data), status='uploading') # Import image to store path = self._url('/v2/images/%s/import' % image_id) headers = self._headers({ 'content-type': 'application/json', 'X-Roles': 'admin', }) data = jsonutils.dumps({'method': { 'name': 'glance-direct' }}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.ACCEPTED, response.status_code) # Verify image is in active state and checksum is set # NOTE(abhishekk): As import is a async call we need to provide # some timelap to complete the call. path = self._url('/v2/images/%s' % image_id) func_utils.wait_for_status(request_path=path, request_headers=self._headers(), status='active', max_sec=15, delay_sec=0.2) expect_c = str(md5(image_data, usedforsecurity=False).hexdigest()) expect_h = str(hashlib.sha512(image_data).hexdigest()) func_utils.verify_image_hashes_and_status(self, image_id, checksum=expect_c, os_hash_value=expect_h, size=len(image_data), status='active') # Ensure the size is updated to reflect the data uploaded path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertEqual(len(image_data), jsonutils.loads(response.text)['size']) # Ensure image is created in default backend self.assertIn('file1', jsonutils.loads(response.text)['stores']) # Deleting image should work path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Image list should now be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) self.stop_servers() def test_image_import_using_glance_direct_different_backend(self): self.start_servers(**self.__dict__.copy()) # Image list should be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # glance-direct should be available in discovery response path = self._url('/v2/info/import') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['import-methods']['value'] self.assertIn("glance-direct", discovery_calls) # file1 and file2 should be available in discovery response available_stores = ['file1', 'file2', 'file3'] path = self._url('/v2/info/stores') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['stores'] # os_glance_staging_store should not be available in discovery response for stores in discovery_calls: self.assertIn('id', stores) self.assertIn(stores['id'], available_stores) self.assertFalse(stores["id"].startswith("os_glance_")) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Check 'OpenStack-image-store-ids' header present in response self.assertIn('OpenStack-image-store-ids', response.headers) for store in available_stores: self.assertIn(store, response.headers['OpenStack-image-store-ids']) # Returned image entity should have a generated id and status image = jsonutils.loads(response.text) image_id = image['id'] checked_keys = set([ u'status', u'name', u'tags', u'created_at', u'updated_at', u'visibility', u'self', u'protected', u'id', u'file', u'min_disk', u'type', u'min_ram', u'schema', u'disk_format', u'container_format', u'owner', u'checksum', u'size', u'virtual_size', u'os_hidden', u'os_hash_algo', u'os_hash_value' ]) self.assertEqual(checked_keys, set(image.keys())) expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'type': 'kernel', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Image list should now have one entry path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # Upload some image data to staging area image_data = b'GLANCE IS DEAD SEXY' path = self._url('/v2/images/%s/stage' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) response = requests.put(path, headers=headers, data=image_data) self.assertEqual(http.NO_CONTENT, response.status_code) # Verify image is in uploading state and checksum is None func_utils.verify_image_hashes_and_status(self, image_id, size=len(image_data), status='uploading') # Import image to file2 store (other than default backend) path = self._url('/v2/images/%s/import' % image_id) headers = self._headers({ 'content-type': 'application/json', 'X-Roles': 'admin', 'X-Image-Meta-Store': 'file2' }) data = jsonutils.dumps({'method': { 'name': 'glance-direct' }}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.ACCEPTED, response.status_code) # Verify image is in active state and checksum is set # NOTE(abhishekk): As import is a async call we need to provide # some timelap to complete the call. path = self._url('/v2/images/%s' % image_id) func_utils.wait_for_status(request_path=path, request_headers=self._headers(), status='active', max_sec=15, delay_sec=0.2) expect_c = str(md5(image_data, usedforsecurity=False).hexdigest()) expect_h = str(hashlib.sha512(image_data).hexdigest()) func_utils.verify_image_hashes_and_status(self, image_id, checksum=expect_c, os_hash_value=expect_h, size=len(image_data), status='active') # Ensure the size is updated to reflect the data uploaded path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertEqual(len(image_data), jsonutils.loads(response.text)['size']) # Ensure image is created in different backend self.assertIn('file2', jsonutils.loads(response.text)['stores']) # Deleting image should work path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Image list should now be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) self.stop_servers() def test_image_import_using_web_download(self): self.start_servers(**self.__dict__.copy()) # Image list should be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # web-download should be available in discovery response path = self._url('/v2/info/import') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['import-methods']['value'] self.assertIn("web-download", discovery_calls) # file1 and file2 should be available in discovery response available_stores = ['file1', 'file2', 'file3'] path = self._url('/v2/info/stores') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['stores'] # os_glance_staging_store should not be available in discovery response for stores in discovery_calls: self.assertIn('id', stores) self.assertIn(stores['id'], available_stores) self.assertFalse(stores["id"].startswith("os_glance_")) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Check 'OpenStack-image-store-ids' header present in response self.assertIn('OpenStack-image-store-ids', response.headers) for store in available_stores: self.assertIn(store, response.headers['OpenStack-image-store-ids']) # Returned image entity should have a generated id and status image = jsonutils.loads(response.text) image_id = image['id'] checked_keys = set([ u'status', u'name', u'tags', u'created_at', u'updated_at', u'visibility', u'self', u'protected', u'id', u'file', u'min_disk', u'type', u'min_ram', u'schema', u'disk_format', u'container_format', u'owner', u'checksum', u'size', u'virtual_size', u'os_hidden', u'os_hash_algo', u'os_hash_value' ]) self.assertEqual(checked_keys, set(image.keys())) expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'type': 'kernel', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Image list should now have one entry path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # Verify image is in queued state and checksum is None func_utils.verify_image_hashes_and_status(self, image_id, status='queued') # Import image to store path = self._url('/v2/images/%s/import' % image_id) headers = self._headers({ 'content-type': 'application/json', 'X-Roles': 'admin', }) # Start http server locally thread, httpd, port = test_utils.start_standalone_http_server() image_data_uri = 'http://localhost:%s/' % port data = jsonutils.dumps({'method': { 'name': 'web-download', 'uri': image_data_uri }}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.ACCEPTED, response.status_code) # Verify image is in active state and checksum is set # NOTE(abhishekk): As import is a async call we need to provide # some timelap to complete the call. path = self._url('/v2/images/%s' % image_id) func_utils.wait_for_status(request_path=path, request_headers=self._headers(), status='active', max_sec=20, delay_sec=0.2, start_delay_sec=1) with requests.get(image_data_uri) as r: expect_c = str(md5(r.content, usedforsecurity=False).hexdigest()) expect_h = str(hashlib.sha512(r.content).hexdigest()) func_utils.verify_image_hashes_and_status(self, image_id, checksum=expect_c, os_hash_value=expect_h, size=len(r.content), status='active') # kill the local http server httpd.shutdown() httpd.server_close() # Ensure image is created in default backend path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertIn('file1', jsonutils.loads(response.text)['stores']) # Deleting image should work path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Image list should now be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) self.stop_servers() def test_image_import_using_web_download_different_backend(self): self.start_servers(**self.__dict__.copy()) # Image list should be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # web-download should be available in discovery response path = self._url('/v2/info/import') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['import-methods']['value'] self.assertIn("web-download", discovery_calls) # file1 and file2 should be available in discovery response available_stores = ['file1', 'file2', 'file3'] path = self._url('/v2/info/stores') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['stores'] # os_glance_staging_store should not be available in discovery response for stores in discovery_calls: self.assertIn('id', stores) self.assertIn(stores['id'], available_stores) self.assertFalse(stores["id"].startswith("os_glance_")) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Check 'OpenStack-image-store-ids' header present in response self.assertIn('OpenStack-image-store-ids', response.headers) for store in available_stores: self.assertIn(store, response.headers['OpenStack-image-store-ids']) # Returned image entity should have a generated id and status image = jsonutils.loads(response.text) image_id = image['id'] checked_keys = set([ u'status', u'name', u'tags', u'created_at', u'updated_at', u'visibility', u'self', u'protected', u'id', u'file', u'min_disk', u'type', u'min_ram', u'schema', u'disk_format', u'container_format', u'owner', u'checksum', u'size', u'virtual_size', u'os_hidden', u'os_hash_algo', u'os_hash_value' ]) self.assertEqual(checked_keys, set(image.keys())) expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'type': 'kernel', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Image list should now have one entry path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # Verify image is in queued state and checksum is None func_utils.verify_image_hashes_and_status(self, image_id, status='queued') # Import image to store path = self._url('/v2/images/%s/import' % image_id) headers = self._headers({ 'content-type': 'application/json', 'X-Roles': 'admin', 'X-Image-Meta-Store': 'file2' }) # Start http server locally thread, httpd, port = test_utils.start_standalone_http_server() image_data_uri = 'http://localhost:%s/' % port data = jsonutils.dumps({'method': { 'name': 'web-download', 'uri': image_data_uri }}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.ACCEPTED, response.status_code) # Verify image is in active state and checksum is set # NOTE(abhishekk): As import is a async call we need to provide # some timelap to complete the call. path = self._url('/v2/images/%s' % image_id) func_utils.wait_for_status(request_path=path, request_headers=self._headers(), status='active', max_sec=20, delay_sec=0.2, start_delay_sec=1) with requests.get(image_data_uri) as r: expect_c = str(md5(r.content, usedforsecurity=False).hexdigest()) expect_h = str(hashlib.sha512(r.content).hexdigest()) func_utils.verify_image_hashes_and_status(self, image_id, checksum=expect_c, os_hash_value=expect_h, size=len(r.content), status='active') # kill the local http server httpd.shutdown() httpd.server_close() # Ensure image is created in different backend path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertIn('file2', jsonutils.loads(response.text)['stores']) # Deleting image should work path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Image list should now be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) self.stop_servers() def test_image_import_multi_stores(self): self.start_servers(**self.__dict__.copy()) # Image list should be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # web-download should be available in discovery response path = self._url('/v2/info/import') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['import-methods']['value'] self.assertIn("web-download", discovery_calls) # file1 and file2 should be available in discovery response available_stores = ['file1', 'file2', 'file3'] path = self._url('/v2/info/stores') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['stores'] # os_glance_staging_store should not be available in discovery response for stores in discovery_calls: self.assertIn('id', stores) self.assertIn(stores['id'], available_stores) self.assertFalse(stores["id"].startswith("os_glance_")) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Check 'OpenStack-image-store-ids' header present in response self.assertIn('OpenStack-image-store-ids', response.headers) for store in available_stores: self.assertIn(store, response.headers['OpenStack-image-store-ids']) # Returned image entity should have a generated id and status image = jsonutils.loads(response.text) image_id = image['id'] checked_keys = set([ u'status', u'name', u'tags', u'created_at', u'updated_at', u'visibility', u'self', u'protected', u'id', u'file', u'min_disk', u'type', u'min_ram', u'schema', u'disk_format', u'container_format', u'owner', u'checksum', u'size', u'virtual_size', u'os_hidden', u'os_hash_algo', u'os_hash_value' ]) self.assertEqual(checked_keys, set(image.keys())) expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'type': 'kernel', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Image list should now have one entry path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # Verify image is in queued state and checksum is None func_utils.verify_image_hashes_and_status(self, image_id, status='queued') # Import image to multiple stores path = self._url('/v2/images/%s/import' % image_id) headers = self._headers({ 'content-type': 'application/json', 'X-Roles': 'admin' }) # Start http server locally thread, httpd, port = test_utils.start_standalone_http_server() image_data_uri = 'http://localhost:%s/' % port data = jsonutils.dumps( {'method': {'name': 'web-download', 'uri': image_data_uri}, 'stores': ['file1', 'file2']}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.ACCEPTED, response.status_code) # Verify image is in active state and checksum is set # NOTE(abhishekk): As import is a async call we need to provide # some timelap to complete the call. path = self._url('/v2/images/%s' % image_id) func_utils.wait_for_status(request_path=path, request_headers=self._headers(), status='active', max_sec=40, delay_sec=0.2, start_delay_sec=1) with requests.get(image_data_uri) as r: expect_c = str(md5(r.content, usedforsecurity=False).hexdigest()) expect_h = str(hashlib.sha512(r.content).hexdigest()) func_utils.verify_image_hashes_and_status(self, image_id, checksum=expect_c, os_hash_value=expect_h, size=len(r.content), status='active') # kill the local http server httpd.shutdown() httpd.server_close() # Ensure image is created in the two stores path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertIn('file2', jsonutils.loads(response.text)['stores']) self.assertIn('file1', jsonutils.loads(response.text)['stores']) # Deleting image should work path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Image list should now be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) self.stop_servers() def test_copy_image_lifecycle(self): self.start_servers(**self.__dict__.copy()) # Image list should be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # copy-image should be available in discovery response path = self._url('/v2/info/import') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['import-methods']['value'] self.assertIn("copy-image", discovery_calls) # file1 and file2 should be available in discovery response available_stores = ['file1', 'file2', 'file3'] path = self._url('/v2/info/stores') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['stores'] # os_glance_staging_store should not be available in discovery response for stores in discovery_calls: self.assertIn('id', stores) self.assertIn(stores['id'], available_stores) self.assertFalse(stores["id"].startswith("os_glance_")) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Check 'OpenStack-image-store-ids' header present in response self.assertIn('OpenStack-image-store-ids', response.headers) for store in available_stores: self.assertIn(store, response.headers['OpenStack-image-store-ids']) # Returned image entity should have a generated id and status image = jsonutils.loads(response.text) image_id = image['id'] checked_keys = set([ u'status', u'name', u'tags', u'created_at', u'updated_at', u'visibility', u'self', u'protected', u'id', u'file', u'min_disk', u'type', u'min_ram', u'schema', u'disk_format', u'container_format', u'owner', u'checksum', u'size', u'virtual_size', u'os_hidden', u'os_hash_algo', u'os_hash_value' ]) self.assertEqual(checked_keys, set(image.keys())) expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'type': 'kernel', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Image list should now have one entry path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # Verify image is in queued state and checksum is None func_utils.verify_image_hashes_and_status(self, image_id, status='queued') # Import image to multiple stores path = self._url('/v2/images/%s/import' % image_id) headers = self._headers({ 'content-type': 'application/json', 'X-Roles': 'admin' }) # Start http server locally thread, httpd, port = test_utils.start_standalone_http_server() image_data_uri = 'http://localhost:%s/' % port data = jsonutils.dumps( {'method': {'name': 'web-download', 'uri': image_data_uri}, 'stores': ['file1']}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.ACCEPTED, response.status_code) import_reqid = response.headers['X-Openstack-Request-Id'] # Verify image is in active state and checksum is set # NOTE(abhishekk): As import is a async call we need to provide # some timelap to complete the call. path = self._url('/v2/images/%s' % image_id) func_utils.wait_for_status(request_path=path, request_headers=self._headers(), status='active', max_sec=40, delay_sec=0.2, start_delay_sec=1) with requests.get(image_data_uri) as r: expect_c = str(md5(r.content, usedforsecurity=False).hexdigest()) expect_h = str(hashlib.sha512(r.content).hexdigest()) func_utils.verify_image_hashes_and_status(self, image_id, checksum=expect_c, os_hash_value=expect_h, size=len(r.content), status='active') # kill the local http server httpd.shutdown() httpd.server_close() # Ensure image is created in the one store path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertIn('file1', jsonutils.loads(response.text)['stores']) # Ensure image has one task associated with it path = self._url('/v2/images/%s/tasks' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) tasks = jsonutils.loads(response.text)['tasks'] self.assertEqual(1, len(tasks)) for task in tasks: self.assertEqual(image_id, task['image_id']) user_id = response.request.headers.get( 'X-User-Id') self.assertEqual(user_id, task['user_id']) self.assertEqual(import_reqid, task['request_id']) # Copy newly created image to file2 and file3 stores path = self._url('/v2/images/%s/import' % image_id) headers = self._headers({ 'content-type': 'application/json', 'X-Roles': 'admin' }) data = jsonutils.dumps( {'method': {'name': 'copy-image'}, 'stores': ['file2', 'file3']}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.ACCEPTED, response.status_code) copy_reqid = response.headers['X-Openstack-Request-Id'] # Verify image is copied # NOTE(abhishekk): As import is a async call we need to provide # some timelap to complete the call. path = self._url('/v2/images/%s' % image_id) func_utils.wait_for_copying(request_path=path, request_headers=self._headers(), stores=['file2', 'file3'], max_sec=40, delay_sec=0.2, start_delay_sec=1) # Ensure image is copied to the file2 and file3 store path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertIn('file2', jsonutils.loads(response.text)['stores']) self.assertIn('file3', jsonutils.loads(response.text)['stores']) # Ensure image has two tasks associated with it path = self._url('/v2/images/%s/tasks' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) tasks = jsonutils.loads(response.text)['tasks'] self.assertEqual(2, len(tasks)) expected_reqids = [copy_reqid, import_reqid] for task in tasks: self.assertEqual(image_id, task['image_id']) user_id = response.request.headers.get( 'X-User-Id') self.assertEqual(user_id, task['user_id']) self.assertEqual(expected_reqids.pop(), task['request_id']) # Deleting image should work path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Image list should now be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) self.stop_servers() def test_copy_image_revert_lifecycle(self): # Test if copying task fails in between then the rollback # should delete the data from only stores to which it is # copied and not from the existing stores. self.start_servers(**self.__dict__.copy()) # Image list should be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # copy-image should be available in discovery response path = self._url('/v2/info/import') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['import-methods']['value'] self.assertIn("copy-image", discovery_calls) # file1 and file2 should be available in discovery response available_stores = ['file1', 'file2', 'file3'] path = self._url('/v2/info/stores') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['stores'] # os_glance_staging_store should not be available in discovery response for stores in discovery_calls: self.assertIn('id', stores) self.assertIn(stores['id'], available_stores) self.assertFalse(stores["id"].startswith("os_glance_")) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Check 'OpenStack-image-store-ids' header present in response self.assertIn('OpenStack-image-store-ids', response.headers) for store in available_stores: self.assertIn(store, response.headers['OpenStack-image-store-ids']) # Returned image entity should have a generated id and status image = jsonutils.loads(response.text) image_id = image['id'] checked_keys = set([ u'status', u'name', u'tags', u'created_at', u'updated_at', u'visibility', u'self', u'protected', u'id', u'file', u'min_disk', u'type', u'min_ram', u'schema', u'disk_format', u'container_format', u'owner', u'checksum', u'size', u'virtual_size', u'os_hidden', u'os_hash_algo', u'os_hash_value' ]) self.assertEqual(checked_keys, set(image.keys())) expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'type': 'kernel', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Image list should now have one entry path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # Verify image is in queued state and checksum is None func_utils.verify_image_hashes_and_status(self, image_id, status='queued') # Import image to multiple stores path = self._url('/v2/images/%s/import' % image_id) headers = self._headers({ 'content-type': 'application/json', 'X-Roles': 'admin' }) # Start http server locally thread, httpd, port = test_utils.start_standalone_http_server() image_data_uri = 'http://localhost:%s/' % port data = jsonutils.dumps( {'method': {'name': 'web-download', 'uri': image_data_uri}, 'stores': ['file1']}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.ACCEPTED, response.status_code) # Verify image is in active state and checksum is set # NOTE(abhishekk): As import is a async call we need to provide # some timelap to complete the call. path = self._url('/v2/images/%s' % image_id) func_utils.wait_for_status(request_path=path, request_headers=self._headers(), status='active', max_sec=40, delay_sec=0.2, start_delay_sec=1) with requests.get(image_data_uri) as r: expect_c = str(md5(r.content, usedforsecurity=False).hexdigest()) expect_h = str(hashlib.sha512(r.content).hexdigest()) func_utils.verify_image_hashes_and_status(self, image_id, checksum=expect_c, os_hash_value=expect_h, size=len(r.content), status='active') # kill the local http server httpd.shutdown() httpd.server_close() # Ensure image is created in the one store path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertIn('file1', jsonutils.loads(response.text)['stores']) # Copy newly created image to file2 and file3 stores path = self._url('/v2/images/%s/import' % image_id) headers = self._headers({ 'content-type': 'application/json', 'X-Roles': 'admin' }) # NOTE(abhishekk): Deleting file3 image directory to trigger the # failure, so that we can verify that revert call does not delete # the data from existing stores # NOTE(danms): Do this before we start the import, on a later store, # which will cause that store to fail after we have already completed # the first one. os.rmdir(self.test_dir + "/images_3") data = jsonutils.dumps( {'method': {'name': 'copy-image'}, 'stores': ['file2', 'file3']}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.ACCEPTED, response.status_code) def poll_callback(image): # NOTE(danms): We need to wait for the specific # arrangement we're expecting, which is that file3 has # failed, nothing else is importing, and file2 has been # removed from stores by the revert. return not (image['os_glance_importing_to_stores'] == '' and image['os_glance_failed_import'] == 'file3' and image['stores'] == 'file1') func_utils.poll_entity(self._url('/v2/images/%s' % image_id), self._headers(), poll_callback) # Here we check that the failure of 'file3' caused 'file2' to # be removed from image['stores'], and that 'file3' is reported # as failed in the appropriate status list. Since the import # started with 'store1' being populated, that should remain, # but 'store2' should be reverted/removed. path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertIn('file1', jsonutils.loads(response.text)['stores']) self.assertNotIn('file2', jsonutils.loads(response.text)['stores']) self.assertNotIn('file3', jsonutils.loads(response.text)['stores']) fail_key = 'os_glance_failed_import' pend_key = 'os_glance_importing_to_stores' self.assertEqual('file3', jsonutils.loads(response.text)[fail_key]) self.assertEqual('', jsonutils.loads(response.text)[pend_key]) # Copy newly created image to file2 and file3 stores and # all_stores_must_succeed set to false. path = self._url('/v2/images/%s/import' % image_id) headers = self._headers({ 'content-type': 'application/json', 'X-Roles': 'admin' }) data = jsonutils.dumps( {'method': {'name': 'copy-image'}, 'stores': ['file2', 'file3'], 'all_stores_must_succeed': False}) for i in range(0, 5): response = requests.post(path, headers=headers, data=data) if response.status_code != http.CONFLICT: break # We might race with the revert of the previous task and do not # really have a good way to make sure that it's done. In order # to make sure we tolerate the 409 possibility when import # locking is added, gracefully wait a few times before failing. time.sleep(1) self.assertEqual(http.ACCEPTED, response.status_code) # Verify image is copied # NOTE(abhishekk): As import is a async call we need to provide # some timelap to complete the call. path = self._url('/v2/images/%s' % image_id) func_utils.wait_for_copying(request_path=path, request_headers=self._headers(), stores=['file2'], max_sec=10, delay_sec=0.2, start_delay_sec=1, failure_scenario=True) # Ensure data is not deleted from existing stores as well as # from the stores where it is copied successfully path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertIn('file1', jsonutils.loads(response.text)['stores']) self.assertIn('file2', jsonutils.loads(response.text)['stores']) self.assertNotIn('file3', jsonutils.loads(response.text)['stores']) # Deleting image should work path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Image list should now be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) self.stop_servers() def test_image_import_multi_stores_specifying_all_stores(self): self.start_servers(**self.__dict__.copy()) # Image list should be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # web-download should be available in discovery response path = self._url('/v2/info/import') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['import-methods']['value'] self.assertIn("web-download", discovery_calls) # file1 and file2 should be available in discovery response available_stores = ['file1', 'file2', 'file3'] path = self._url('/v2/info/stores') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['stores'] # os_glance_staging_store should not be available in discovery response for stores in discovery_calls: self.assertIn('id', stores) self.assertIn(stores['id'], available_stores) self.assertFalse(stores["id"].startswith("os_glance_")) # Create an image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Check 'OpenStack-image-store-ids' header present in response self.assertIn('OpenStack-image-store-ids', response.headers) for store in available_stores: self.assertIn(store, response.headers['OpenStack-image-store-ids']) # Returned image entity should have a generated id and status image = jsonutils.loads(response.text) image_id = image['id'] checked_keys = set([ u'status', u'name', u'tags', u'created_at', u'updated_at', u'visibility', u'self', u'protected', u'id', u'file', u'min_disk', u'type', u'min_ram', u'schema', u'disk_format', u'container_format', u'owner', u'checksum', u'size', u'virtual_size', u'os_hidden', u'os_hash_algo', u'os_hash_value' ]) self.assertEqual(checked_keys, set(image.keys())) expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': False, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'type': 'kernel', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Image list should now have one entry path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # Verify image is in queued state and checksum is None func_utils.verify_image_hashes_and_status(self, image_id, status='queued') # Import image to multiple stores path = self._url('/v2/images/%s/import' % image_id) headers = self._headers({ 'content-type': 'application/json', 'X-Roles': 'admin' }) # Start http server locally thread, httpd, port = test_utils.start_standalone_http_server() image_data_uri = 'http://localhost:%s/' % port data = jsonutils.dumps( {'method': {'name': 'web-download', 'uri': image_data_uri}, 'all_stores': True}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.ACCEPTED, response.status_code) # Verify image is in active state and checksum is set # NOTE(abhishekk): As import is a async call we need to provide # some timelap to complete the call. path = self._url('/v2/images/%s' % image_id) func_utils.wait_for_status(request_path=path, request_headers=self._headers(), status='active', max_sec=40, delay_sec=0.2, start_delay_sec=1) with requests.get(image_data_uri) as r: expect_c = str(md5(r.content, usedforsecurity=False).hexdigest()) expect_h = str(hashlib.sha512(r.content).hexdigest()) func_utils.verify_image_hashes_and_status(self, image_id, checksum=expect_c, os_hash_value=expect_h, size=len(r.content), status='active') # kill the local http server httpd.shutdown() httpd.server_close() # Ensure image is created in the two stores path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertIn('file3', jsonutils.loads(response.text)['stores']) self.assertIn('file2', jsonutils.loads(response.text)['stores']) self.assertIn('file1', jsonutils.loads(response.text)['stores']) # Deleting image should work path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # Image list should now be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) self.stop_servers() def test_image_lifecycle(self): # Image list should be empty self.start_servers(**self.__dict__.copy()) path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # file1 and file2 should be available in discovery response available_stores = ['file1', 'file2', 'file3'] path = self._url('/v2/info/stores') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['stores'] # os_glance_staging_store should not be available in discovery response for stores in discovery_calls: self.assertIn('id', stores) self.assertIn(stores['id'], available_stores) self.assertFalse(stores["id"].startswith("os_glance_")) # Create an image (with two deployer-defined properties) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', 'foo': 'bar', 'disk_format': 'aki', 'container_format': 'aki', 'abc': 'xyz', 'protected': True}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Check 'OpenStack-image-store-ids' header present in response self.assertIn('OpenStack-image-store-ids', response.headers) for store in available_stores: self.assertIn(store, response.headers['OpenStack-image-store-ids']) # Returned image entity should have a generated id and status image = jsonutils.loads(response.text) image_id = image['id'] checked_keys = set([ u'status', u'name', u'tags', u'created_at', u'updated_at', u'visibility', u'self', u'protected', u'id', u'file', u'min_disk', u'foo', u'abc', u'type', u'min_ram', u'schema', u'disk_format', u'container_format', u'owner', u'checksum', u'size', u'virtual_size', u'os_hidden', u'os_hash_algo', u'os_hash_value' ]) self.assertEqual(checked_keys, set(image.keys())) expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': True, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'foo': 'bar', 'abc': 'xyz', 'type': 'kernel', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Image list should now have one entry path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # Try to download data before its uploaded path = self._url('/v2/images/%s/file' % image_id) headers = self._headers() response = requests.get(path, headers=headers) self.assertEqual(http.NO_CONTENT, response.status_code) # Upload some image data image_data = b'OpenStack Rules, Other Clouds Drool' path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({'Content-Type': 'application/octet-stream'}) response = requests.put(path, headers=headers, data=image_data) self.assertEqual(http.NO_CONTENT, response.status_code) expect_c = str(md5(image_data, usedforsecurity=False).hexdigest()) expect_h = str(hashlib.sha512(image_data).hexdigest()) func_utils.verify_image_hashes_and_status(self, image_id, checksum=expect_c, os_hash_value=expect_h, size=len(image_data), status='active') # Ensure image is created in default backend path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertIn('file1', jsonutils.loads(response.text)['stores']) # Try to download the data that was just uploaded path = self._url('/v2/images/%s/file' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertEqual(expect_c, response.headers['Content-MD5']) self.assertEqual(image_data.decode('utf-8'), response.text) # Ensure the size is updated to reflect the data uploaded path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertEqual(len(image_data), jsonutils.loads(response.text)['size']) # Unprotect image for deletion path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) doc = [{'op': 'replace', 'path': '/protected', 'value': False}] data = jsonutils.dumps(doc) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) # Deletion should work. Deleting image path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # This image should be no longer be directly accessible path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.NOT_FOUND, response.status_code) # And neither should its data path = self._url('/v2/images/%s/file' % image_id) headers = self._headers() response = requests.get(path, headers=headers) self.assertEqual(http.NOT_FOUND, response.status_code) # Image list should now be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) self.stop_servers() def test_image_lifecycle_different_backend(self): # Image list should be empty self.start_servers(**self.__dict__.copy()) path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # file1 and file2 should be available in discovery response available_stores = ['file1', 'file2', 'file3'] path = self._url('/v2/info/stores') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) discovery_calls = jsonutils.loads( response.text)['stores'] # os_glance_staging_store should not be available in discovery response for stores in discovery_calls: self.assertIn('id', stores) self.assertIn(stores['id'], available_stores) self.assertFalse(stores["id"].startswith("os_glance_")) # Create an image (with two deployer-defined properties) path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', 'foo': 'bar', 'disk_format': 'aki', 'container_format': 'aki', 'abc': 'xyz', 'protected': True}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) # Check 'OpenStack-image-store-ids' header present in response self.assertIn('OpenStack-image-store-ids', response.headers) for store in available_stores: self.assertIn(store, response.headers['OpenStack-image-store-ids']) # Returned image entity should have a generated id and status image = jsonutils.loads(response.text) image_id = image['id'] checked_keys = set([ u'status', u'name', u'tags', u'created_at', u'updated_at', u'visibility', u'self', u'protected', u'id', u'file', u'min_disk', u'foo', u'abc', u'type', u'min_ram', u'schema', u'disk_format', u'container_format', u'owner', u'checksum', u'size', u'virtual_size', u'os_hidden', u'os_hash_algo', u'os_hash_value' ]) self.assertEqual(checked_keys, set(image.keys())) expected_image = { 'status': 'queued', 'name': 'image-1', 'tags': [], 'visibility': 'shared', 'self': '/v2/images/%s' % image_id, 'protected': True, 'file': '/v2/images/%s/file' % image_id, 'min_disk': 0, 'foo': 'bar', 'abc': 'xyz', 'type': 'kernel', 'min_ram': 0, 'schema': '/v2/schemas/image', } for key, value in expected_image.items(): self.assertEqual(value, image[key], key) # Image list should now have one entry path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(image_id, images[0]['id']) # Try to download data before its uploaded path = self._url('/v2/images/%s/file' % image_id) headers = self._headers() response = requests.get(path, headers=headers) self.assertEqual(http.NO_CONTENT, response.status_code) # Upload some image data image_data = b'just a passing glance' path = self._url('/v2/images/%s/file' % image_id) headers = self._headers({ 'Content-Type': 'application/octet-stream', 'X-Image-Meta-Store': 'file2' }) response = requests.put(path, headers=headers, data=image_data) self.assertEqual(http.NO_CONTENT, response.status_code) expect_c = str(md5(image_data, usedforsecurity=False).hexdigest()) expect_h = str(hashlib.sha512(image_data).hexdigest()) func_utils.verify_image_hashes_and_status(self, image_id, checksum=expect_c, os_hash_value=expect_h, size=len(image_data), status='active') # Ensure image is created in different backend path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertIn('file2', jsonutils.loads(response.text)['stores']) # Try to download the data that was just uploaded path = self._url('/v2/images/%s/file' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertEqual(expect_c, response.headers['Content-MD5']) self.assertEqual(image_data.decode('utf-8'), response.text) # Ensure the size is updated to reflect the data uploaded path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertEqual(len(image_data), jsonutils.loads(response.text)['size']) # Unprotect image for deletion path = self._url('/v2/images/%s' % image_id) media_type = 'application/openstack-images-v2.1-json-patch' headers = self._headers({'content-type': media_type}) doc = [{'op': 'replace', 'path': '/protected', 'value': False}] data = jsonutils.dumps(doc) response = requests.patch(path, headers=headers, data=data) self.assertEqual(http.OK, response.status_code, response.text) # Deletion should work. Deleting image path = self._url('/v2/images/%s' % image_id) response = requests.delete(path, headers=self._headers()) self.assertEqual(http.NO_CONTENT, response.status_code) # This image should be no longer be directly accessible path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.NOT_FOUND, response.status_code) # And neither should its data path = self._url('/v2/images/%s/file' % image_id) headers = self._headers() response = requests.get(path, headers=headers) self.assertEqual(http.NOT_FOUND, response.status_code) # Image list should now be empty path = self._url('/v2/images') response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) self.stop_servers() class TestMultiStoreImageMembers(functional.MultipleBackendFunctionalTest): def setUp(self): super(TestMultiStoreImageMembers, self).setUp() self.cleanup() self.include_scrubber = False self.api_server_multiple_backend.deployment_flavor = 'noauth' for i in range(3): ret = test_utils.start_http_server("foo_image_id%d" % i, "foo_image%d" % i) setattr(self, 'http_server%d' % i, ret[1]) setattr(self, 'http_port%d' % i, ret[2]) def tearDown(self): for i in range(3): httpd = getattr(self, 'http_server%d' % i, None) if httpd: httpd.shutdown() httpd.server_close() super(TestMultiStoreImageMembers, self).tearDown() def _headers(self, custom_headers=None): base_headers = { 'X-Identity-Status': 'Confirmed', 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96', 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e', 'X-Tenant-Id': TENANT1, 'X-Roles': 'reader,member', } base_headers.update(custom_headers or {}) return base_headers def test_image_member_lifecycle_for_multiple_stores(self): self.start_servers(**self.__dict__.copy()) try: def get_header(tenant, tenant_id=None, role=''): return self._headers(custom_headers=get_auth_header( tenant, tenant_id, role)) # Image list should be empty path = self._url('/v2/images') response = requests.get(path, headers=get_header('tenant1')) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) owners = ['tenant1', 'tenant2', 'admin'] visibilities = ['community', 'private', 'public', 'shared'] image_fixture = [] for owner in owners: for visibility in visibilities: path = self._url('/v2/images') role = 'member' if visibility == 'public': role = 'admin' headers = self._headers(custom_headers={ 'content-type': 'application/json', 'X-Auth-Token': 'createuser:%s:admin' % owner, 'X-Roles': role, }) data = jsonutils.dumps({ 'name': '%s-%s' % (owner, visibility), 'visibility': visibility, }) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image_fixture.append(jsonutils.loads(response.text)) # Image list should contain 12 images for tenant1 path = self._url('/v2/images') response = requests.get(path, headers=get_header('tenant1')) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(12, len(images)) # Image list should contain 3 images for TENANT3 path = self._url('/v2/images') response = requests.get(path, headers=get_header( TENANT3, tenant_id=TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(3, len(images)) # Add Image member for tenant1-shared image path = self._url('/v2/images/%s/members' % image_fixture[3]['id']) body = jsonutils.dumps({'member': TENANT3}) response = requests.post(path, headers=get_header( 'tenant1', tenant_id=TENANT1), data=body) self.assertEqual(http.OK, response.status_code) image_member = jsonutils.loads(response.text) self.assertEqual(image_fixture[3]['id'], image_member['image_id']) self.assertEqual(TENANT3, image_member['member_id']) self.assertIn('created_at', image_member) self.assertIn('updated_at', image_member) self.assertEqual('pending', image_member['status']) # Image list should contain 3 images for TENANT3 path = self._url('/v2/images') response = requests.get(path, headers=get_header( TENANT3, tenant_id=TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(3, len(images)) # Image list should contain 0 shared images for TENANT3 # because default is accepted path = self._url('/v2/images?visibility=shared') response = requests.get(path, headers=get_header( TENANT3, tenant_id=TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # Image list should contain 4 images for TENANT3 with status # pending path = self._url('/v2/images?member_status=pending') response = requests.get(path, headers=get_header( TENANT3, tenant_id=TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(4, len(images)) # Image list should contain 4 images for TENANT3 with status all path = self._url('/v2/images?member_status=all') response = requests.get(path, headers=get_header( TENANT3, tenant_id=TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(4, len(images)) # Image list should contain 1 image for TENANT3 with status pending # and visibility shared path = self._url( '/v2/images?member_status=pending&visibility=shared') response = requests.get(path, headers=get_header( TENANT3, tenant_id=TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(1, len(images)) self.assertEqual(images[0]['name'], 'tenant1-shared') # Image list should contain 0 image for TENANT3 with status # rejected and visibility shared path = self._url( '/v2/images?member_status=rejected&visibility=shared') response = requests.get(path, headers=get_header( TENANT3, tenant_id=TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # Image list should contain 0 image for TENANT3 with status # accepted and visibility shared path = self._url( '/v2/images?member_status=accepted&visibility=shared') response = requests.get(path, headers=get_header( TENANT3, tenant_id=TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # Image list should contain 0 image for TENANT3 with status # accepted and visibility private path = self._url('/v2/images?visibility=private') response = requests.get(path, headers=get_header( TENANT3, tenant_id=TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(0, len(images)) # Image tenant2-shared's image members list should contain # no members path = self._url('/v2/images/%s/members' % image_fixture[7]['id']) response = requests.get(path, headers=get_header('tenant2')) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertEqual(0, len(body['members'])) # Tenant 1, who is the owner cannot change status of image member path = self._url('/v2/images/%s/members/%s' % ( image_fixture[3]['id'], TENANT3)) body = jsonutils.dumps({'status': 'accepted'}) response = requests.put(path, headers=get_header( 'tenant1', tenant_id=TENANT1), data=body) self.assertEqual(http.FORBIDDEN, response.status_code) # Tenant 1, who is the owner can get status of its own image member path = self._url('/v2/images/%s/members/%s' % ( image_fixture[3]['id'], TENANT3)) response = requests.get(path, headers=get_header( 'tenant1', tenant_id=TENANT1)) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertEqual('pending', body['status']) self.assertEqual(image_fixture[3]['id'], body['image_id']) self.assertEqual(TENANT3, body['member_id']) # Tenant 3, who is the member can get status of its own status path = self._url('/v2/images/%s/members/%s' % ( image_fixture[3]['id'], TENANT3)) response = requests.get(path, headers=get_header( TENANT3, tenant_id=TENANT3)) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertEqual('pending', body['status']) self.assertEqual(image_fixture[3]['id'], body['image_id']) self.assertEqual(TENANT3, body['member_id']) # Tenant 2, who not the owner cannot get status of image member path = self._url('/v2/images/%s/members/%s' % ( image_fixture[3]['id'], TENANT3)) response = requests.get(path, headers=get_header( 'tenant2', tenant_id=TENANT2)) self.assertEqual(http.NOT_FOUND, response.status_code) # Tenant 3 can change status of image member path = self._url('/v2/images/%s/members/%s' % ( image_fixture[3]['id'], TENANT3)) body = jsonutils.dumps({'status': 'accepted'}) response = requests.put(path, headers=get_header( TENANT3, tenant_id=TENANT3), data=body) self.assertEqual(http.OK, response.status_code) image_member = jsonutils.loads(response.text) self.assertEqual(image_fixture[3]['id'], image_member['image_id']) self.assertEqual(TENANT3, image_member['member_id']) self.assertEqual('accepted', image_member['status']) # Image list should contain 4 images for TENANT3 because status is # accepted path = self._url('/v2/images') response = requests.get(path, headers=get_header( TENANT3, tenant_id=TENANT3)) self.assertEqual(http.OK, response.status_code) images = jsonutils.loads(response.text)['images'] self.assertEqual(4, len(images)) # Tenant 3 invalid status change path = self._url('/v2/images/%s/members/%s' % ( image_fixture[3]['id'], TENANT3)) body = jsonutils.dumps({'status': 'invalid-status'}) response = requests.put(path, headers=get_header( TENANT3, tenant_id=TENANT3), data=body) self.assertEqual(http.BAD_REQUEST, response.status_code) # Owner cannot change status of image path = self._url('/v2/images/%s/members/%s' % ( image_fixture[3]['id'], TENANT3)) body = jsonutils.dumps({'status': 'accepted'}) response = requests.put(path, headers=get_header( 'tenant1', tenant_id=TENANT1), data=body) self.assertEqual(http.FORBIDDEN, response.status_code) # Add Image member for tenant2-shared image path = self._url('/v2/images/%s/members' % image_fixture[7]['id']) body = jsonutils.dumps({'member': TENANT4}) response = requests.post(path, headers=get_header('tenant2'), data=body) self.assertEqual(http.OK, response.status_code) image_member = jsonutils.loads(response.text) self.assertEqual(image_fixture[7]['id'], image_member['image_id']) self.assertEqual(TENANT4, image_member['member_id']) self.assertIn('created_at', image_member) self.assertIn('updated_at', image_member) # Add Image member to public image path = self._url('/v2/images/%s/members' % image_fixture[2]['id']) body = jsonutils.dumps({'member': TENANT2}) response = requests.post(path, headers=get_header( 'tenant1', tenant_id=TENANT1), data=body) self.assertEqual(http.FORBIDDEN, response.status_code) # Add Image member to private image path = self._url('/v2/images/%s/members' % image_fixture[1]['id']) body = jsonutils.dumps({'member': TENANT2}) response = requests.post(path, headers=get_header( 'tenant1', tenant_id=TENANT1), data=body) self.assertEqual(http.FORBIDDEN, response.status_code) # Add Image member to community image path = self._url('/v2/images/%s/members' % image_fixture[0]['id']) body = jsonutils.dumps({'member': TENANT2}) response = requests.post(path, headers=get_header( 'tenant1', tenant_id=TENANT1), data=body) self.assertEqual(http.FORBIDDEN, response.status_code) # Image tenant1-shared's members list should contain 1 member path = self._url('/v2/images/%s/members' % image_fixture[3]['id']) response = requests.get(path, headers=get_header( 'tenant1', tenant_id=TENANT1)) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertEqual(1, len(body['members'])) # Admin can see any members path = self._url('/v2/images/%s/members' % image_fixture[3]['id']) response = requests.get(path, headers=get_header('tenant1', tenant_id=TENANT1, role='admin')) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertEqual(1, len(body['members'])) # Image members forbidden for public image path = self._url('/v2/images/%s/members' % image_fixture[2]['id']) response = requests.get(path, headers=get_header( 'tenant1', tenant_id=TENANT1)) self.assertIn("Only shared images have members", response.text) self.assertEqual(http.FORBIDDEN, response.status_code) # Image members forbidden for community image path = self._url('/v2/images/%s/members' % image_fixture[0]['id']) response = requests.get(path, headers=get_header( 'tenant1', tenant_id=TENANT1)) self.assertIn("Only shared images have members", response.text) self.assertEqual(http.FORBIDDEN, response.status_code) # Image members forbidden for private image path = self._url('/v2/images/%s/members' % image_fixture[1]['id']) response = requests.get(path, headers=get_header( 'tenant1', tenant_id=TENANT1)) self.assertIn("Only shared images have members", response.text) self.assertEqual(http.FORBIDDEN, response.status_code) # Image Member Cannot delete Image membership path = self._url('/v2/images/%s/members/%s' % ( image_fixture[3]['id'], TENANT3)) response = requests.delete(path, headers=get_header( TENANT3, tenant_id=TENANT3)) self.assertEqual(http.FORBIDDEN, response.status_code) # Delete Image member path = self._url('/v2/images/%s/members/%s' % ( image_fixture[3]['id'], TENANT3)) response = requests.delete(path, headers=get_header( 'tenant1', tenant_id=TENANT1)) self.assertEqual(http.NO_CONTENT, response.status_code) # Now the image has no members path = self._url('/v2/images/%s/members' % image_fixture[3]['id']) response = requests.get(path, headers=get_header( 'tenant1', tenant_id=TENANT1)) self.assertEqual(http.OK, response.status_code) body = jsonutils.loads(response.text) self.assertEqual(0, len(body['members'])) # Adding 11 image members should fail since configured limit is 10 path = self._url('/v2/images/%s/members' % image_fixture[3]['id']) for i in range(10): body = jsonutils.dumps({'member': str(uuid.uuid4())}) response = requests.post(path, headers=get_header( 'tenant1', tenant_id=TENANT1), data=body) self.assertEqual(http.OK, response.status_code) body = jsonutils.dumps({'member': str(uuid.uuid4())}) response = requests.post(path, headers=get_header( 'tenant1', tenant_id=TENANT1), data=body) self.assertEqual(http.REQUEST_ENTITY_TOO_LARGE, response.status_code) # Get Image member should return not found for public image path = self._url('/v2/images/%s/members/%s' % ( image_fixture[2]['id'], TENANT3)) response = requests.get(path, headers=get_header( 'tenant1', tenant_id=TENANT1)) self.assertEqual(http.NOT_FOUND, response.status_code) # Get Image member should return not found for community image path = self._url('/v2/images/%s/members/%s' % ( image_fixture[0]['id'], TENANT3)) response = requests.get(path, headers=get_header( 'tenant1', tenant_id=TENANT1)) self.assertEqual(http.NOT_FOUND, response.status_code) # Get Image member should return not found for private image path = self._url('/v2/images/%s/members/%s' % ( image_fixture[1]['id'], TENANT3)) response = requests.get(path, headers=get_header( 'tenant1', tenant_id=TENANT1)) self.assertEqual(http.NOT_FOUND, response.status_code) # Delete Image member should return forbidden for public image path = self._url('/v2/images/%s/members/%s' % ( image_fixture[2]['id'], TENANT3)) response = requests.delete(path, headers=get_header( 'tenant1', tenant_id=TENANT1)) self.assertEqual(http.FORBIDDEN, response.status_code) # Delete Image member should return forbidden for community image path = self._url('/v2/images/%s/members/%s' % ( image_fixture[0]['id'], TENANT3)) response = requests.delete(path, headers=get_header( 'tenant1', tenant_id=TENANT1)) self.assertEqual(http.FORBIDDEN, response.status_code) # Delete Image member should return forbidden for private image path = self._url('/v2/images/%s/members/%s' % ( image_fixture[1]['id'], TENANT3)) response = requests.delete(path, headers=get_header( 'tenant1', tenant_id=TENANT1)) self.assertEqual(http.FORBIDDEN, response.status_code) except requests.exceptions.ConnectionError as e: # NOTE(abhishekk): This test fails intermittently for py37 # environment refer, # https://bugs.launchpad.net/glance/+bug/1873735 self.skipTest("Remote connection closed abruptly: %s" % e.args[0]) self.stop_servers() class TestCopyImagePermissions(functional.MultipleBackendFunctionalTest): def setUp(self): super(TestCopyImagePermissions, self).setUp() self.cleanup() self.include_scrubber = False self.api_server_multiple_backend.deployment_flavor = 'noauth' def _headers(self, custom_headers=None): base_headers = { 'X-Identity-Status': 'Confirmed', 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96', 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e', 'X-Tenant-Id': TENANT1, 'X-Roles': 'reader,member', } base_headers.update(custom_headers or {}) return base_headers def _create_and_import_image_data(self): # Create a public image path = self._url('/v2/images') headers = self._headers({'content-type': 'application/json'}) data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', 'visibility': 'public', 'disk_format': 'aki', 'container_format': 'aki'}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.CREATED, response.status_code) image = jsonutils.loads(response.text) image_id = image['id'] path = self._url('/v2/images/%s/import' % image_id) headers = self._headers({ 'content-type': 'application/json', 'X-Roles': 'admin' }) # Start http server locally thread, httpd, port = test_utils.start_standalone_http_server() image_data_uri = 'http://localhost:%s/' % port data = jsonutils.dumps( {'method': {'name': 'web-download', 'uri': image_data_uri}, 'stores': ['file1']}) response = requests.post(path, headers=headers, data=data) self.assertEqual(http.ACCEPTED, response.status_code) # Verify image is in active state and checksum is set # NOTE(abhishekk): As import is a async call we need to provide # some timelap to complete the call. path = self._url('/v2/images/%s' % image_id) func_utils.wait_for_status(request_path=path, request_headers=self._headers(), status='active', max_sec=40, delay_sec=0.2, start_delay_sec=1) with requests.get(image_data_uri) as r: expect_c = str(md5(r.content, usedforsecurity=False).hexdigest()) expect_h = str(hashlib.sha512(r.content).hexdigest()) func_utils.verify_image_hashes_and_status(self, image_id, checksum=expect_c, os_hash_value=expect_h, size=len(r.content), status='active') # kill the local http server httpd.shutdown() httpd.server_close() return image_id def _test_copy_public_image_as_non_admin(self): self.start_servers(**self.__dict__.copy()) # Create a publicly-visible image as TENANT1 image_id = self._create_and_import_image_data() # Ensure image is created in the one store path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertEqual('file1', jsonutils.loads(response.text)['stores']) # Copy newly created image to file2 store as TENANT2 path = self._url('/v2/images/%s/import' % image_id) headers = self._headers({ 'content-type': 'application/json', }) headers = get_auth_header(TENANT2, TENANT2, role='reader,member', headers=headers) data = jsonutils.dumps( {'method': {'name': 'copy-image'}, 'stores': ['file2']}) response = requests.post(path, headers=headers, data=data) return image_id, response def test_copy_public_image_as_non_admin(self): rules = { "context_is_admin": "role:admin", "default": "", "add_image": "", "get_image": "", "modify_image": "", "upload_image": "", "get_image_location": "", "delete_image": "", "restricted": "", "download_image": "", "add_member": "", "publicize_image": "", "copy_image": "role:admin", } self.set_policy_rules(rules) image_id, response = self._test_copy_public_image_as_non_admin() # Expect failure to copy another user's image self.assertEqual(http.FORBIDDEN, response.status_code) def test_copy_public_image_as_non_admin_permitted(self): rules = { "context_is_admin": "role:admin", "default": "", "add_image": "", "get_image": "", "modify_image": "", "upload_image": "", "get_image_location": "", "delete_image": "", "restricted": "", "download_image": "", "add_member": "", "publicize_image": "", "copy_image": "'public':%(visibility)s", } self.set_policy_rules(rules) image_id, response = self._test_copy_public_image_as_non_admin() # Expect success because image is public self.assertEqual(http.ACCEPTED, response.status_code) # Verify image is copied # NOTE(abhishekk): As import is a async call we need to provide # some timelap to complete the call. path = self._url('/v2/images/%s' % image_id) func_utils.wait_for_copying(request_path=path, request_headers=self._headers(), stores=['file2'], max_sec=40, delay_sec=0.2, start_delay_sec=1) # Ensure image is copied to the file2 and file3 store path = self._url('/v2/images/%s' % image_id) response = requests.get(path, headers=self._headers()) self.assertEqual(http.OK, response.status_code) self.assertIn('file2', jsonutils.loads(response.text)['stores']) class TestImportProxy(functional.SynchronousAPIBase): """Test the image import proxy-to-stage-worker behavior. This is done as a SynchronousAPIBase test with one mock for a couple of reasons: 1. The main functional tests can't handle a call with a token inside because of their paste config. Even if they did, they would not be able to validate it. 2. The main functional tests don't support multiple API workers with separate config and making them work that way is non-trivial. Functional tests are fairly synthetic and fixing or hacking over the above push us only further so. Using theh Synchronous API method is vastly easier, easier to verify, and tests the integration across the API calls, which is what is important. """ def setUp(self): super(TestImportProxy, self).setUp() # Emulate a keystoneauth1 client for service-to-service communication self.ksa_client = self.useFixture( fixtures.MockPatch('glance.context.get_ksa_client')).mock def test_import_proxy(self): resp = requests.Response() resp.status_code = 202 resp.headers['x-openstack-request-id'] = 'req-remote' self.ksa_client.return_value.post.return_value = resp # Stage it on worker1 self.config(worker_self_reference_url='http://worker1') self.start_server() image_id = self._create_and_stage() # Make sure we can't see the stage host key image = self.api_get('/v2/images/%s' % image_id).json self.assertIn('container_format', image) self.assertNotIn('os_glance_stage_host', image) # Import call goes to worker2 self.config(worker_self_reference_url='http://worker2') self.start_server() r = self._import_direct(image_id, ['store1']) # Assert that it was proxied back to worker1 self.assertEqual(202, r.status_code) self.assertEqual('req-remote', r.headers['x-openstack-request-id']) self.ksa_client.return_value.post.assert_called_once_with( 'http://worker1/v2/images/%s/import' % image_id, timeout=60, json={'method': {'name': 'glance-direct'}, 'stores': ['store1'], 'all_stores': False}) def test_import_proxy_fail_on_remote(self): resp = requests.Response() resp.url = '/v2' resp.status_code = 409 resp.reason = 'Something Failed (tm)' self.ksa_client.return_value.post.return_value = resp self.ksa_client.return_value.delete.return_value = resp # Stage it on worker1 self.config(worker_self_reference_url='http://worker1') self.start_server() image_id = self._create_and_stage() # Import call goes to worker2 self.config(worker_self_reference_url='http://worker2') self.start_server() r = self._import_direct(image_id, ['store1']) # Make sure we see the relevant details from worker1 self.assertEqual(409, r.status_code) self.assertEqual('409 Something Failed (tm)', r.status) # For a 40x, we should get the same on delete r = self.api_delete('/v2/images/%s' % image_id) self.assertEqual(409, r.status_code) self.assertEqual('409 Something Failed (tm)', r.status) def _test_import_proxy_fail_requests(self, error, status): self.ksa_client.return_value.post.side_effect = error self.ksa_client.return_value.delete.side_effect = error # Stage it on worker1 self.config(worker_self_reference_url='http://worker1') self.start_server() image_id = self._create_and_stage() # Import call goes to worker2 self.config(worker_self_reference_url='http://worker2') self.start_server() r = self._import_direct(image_id, ['store1']) self.assertEqual(status, r.status) self.assertIn(b'Stage host is unavailable', r.body) # Make sure we can still delete it r = self.api_delete('/v2/images/%s' % image_id) self.assertEqual(204, r.status_code) r = self.api_get('/v2/images/%s' % image_id) self.assertEqual(404, r.status_code) def test_import_proxy_connection_refused(self): self._test_import_proxy_fail_requests( requests.exceptions.ConnectionError(), '504 Gateway Timeout') def test_import_proxy_connection_timeout(self): self._test_import_proxy_fail_requests( requests.exceptions.ConnectTimeout(), '504 Gateway Timeout') def test_import_proxy_connection_unknown_error(self): self._test_import_proxy_fail_requests( requests.exceptions.RequestException(), '502 Bad Gateway') def get_enforcer_class(limits): class FakeEnforcer: def __init__(self, callback): self._callback = callback def enforce(self, project_id, values): for name, delta in values.items(): current = self._callback(project_id, values.keys()) if current.get(name) + delta > limits.get(name, 0): raise ol_exc.ProjectOverLimit( project_id=project_id, over_limit_info_list=[ol_exc.OverLimitInfo( name, limits.get(name), current.get(name), delta)]) return FakeEnforcer class TestKeystoneQuotas(functional.SynchronousAPIBase): def setUp(self): super(TestKeystoneQuotas, self).setUp() self.config(use_keystone_limits=True) self.config(filesystem_store_datadir='/tmp/foo', group='os_glance_tasks_store') self.enforcer_mock = self.useFixture( fixtures.MockPatchObject(ks_quota, 'limit')).mock def set_limit(self, limits): self.enforcer_mock.Enforcer = get_enforcer_class(limits) def test_upload(self): # Set a quota of 5MiB self.set_limit({'image_size_total': 5, 'image_count_total': 10, 'image_count_uploading': 10}) self.start_server() # First upload of 3MiB is good image_id = self._create_and_upload( data_iter=test_utils.FakeData(3 * units.Mi)) # Second upload of 3MiB is allowed to complete, but leaves us # over-quota self._create_and_upload( data_iter=test_utils.FakeData(3 * units.Mi)) # Third upload of any size fails because we are now over quota self._create_and_upload(expected_code=413) # Delete one image, which should put us under quota self.api_delete('/v2/images/%s' % image_id) # Upload should now succeed self._create_and_upload() def test_import(self): # Set a quota of 5MiB self.set_limit({'image_size_total': 5, 'image_count_total': 10, 'image_count_uploading': 10}) self.start_server() # First upload of 3MiB is good image_id = self._create_and_upload( data_iter=test_utils.FakeData(3 * units.Mi)) # Second upload of 3MiB is allowed to complete, but leaves us # over-quota self._create_and_upload(data_iter=test_utils.FakeData(3 * units.Mi)) # Attempt to import of any size fails because we are now over quota self._create_and_import(stores=['store1'], expected_code=413) # Delete one image, which should put us under quota self.api_delete('/v2/images/%s' % image_id) # Import should now succeed self._create_and_import(stores=['store1']) def test_import_would_go_over(self): # Set a quota limit of 5MiB self.set_limit({'image_size_total': 5, 'image_count_total': 10, 'image_count_uploading': 10}) self.start_server() # First upload of 3MiB is good image_id = self._create_and_upload( data_iter=test_utils.FakeData(3 * units.Mi)) # Stage a 3MiB image for later import import_id = self._create_and_stage( data_iter=test_utils.FakeData(3 * units.Mi)) # Import should fail the task because it would put us over our # 5MiB quota self._import_direct(import_id, ['store1']) image = self._wait_for_import(import_id) task = self._get_latest_task(import_id) self.assertEqual('failure', task['status']) self.assertIn(('image_size_total is over limit of 5 due to ' 'current usage 3 and delta 3'), task['message']) # Delete the first image to make space resp = self.api_delete('/v2/images/%s' % image_id) self.assertEqual(204, resp.status_code) # Stage a 3MiB image for later import (this must be done # because a failed import cannot go back to 'uploading' status) import_id = self._create_and_stage( data_iter=test_utils.FakeData(3 * units.Mi)) # Make sure the import is possible now resp = self._import_direct(import_id, ['store1']) self.assertEqual(202, resp.status_code) image = self._wait_for_import(import_id) self.assertEqual('active', image['status']) task = self._get_latest_task(import_id) self.assertEqual('success', task['status']) def test_copy(self): # Set a size quota of 5MiB, with more staging quota than we need. self.set_limit({'image_size_total': 5, 'image_count_total': 10, 'image_stage_total': 15, 'image_count_uploading': 10}) self.start_server() # First import of 3MiB is good image_id = self._create_and_import( stores=['store1'], data_iter=test_utils.FakeData(3 * units.Mi)) # Second copy is allowed to complete, but leaves us us at # 6MiB of total usage, over quota req = self._import_copy(image_id, ['store2']) self.assertEqual(202, req.status_code) self._wait_for_import(image_id) self.assertEqual('success', self._get_latest_task(image_id)['status']) # Third copy should fail because we're over total size quota. req = self._import_copy(image_id, ['store3']) self.assertEqual(413, req.status_code) # Set our size quota to have enough space, but restrict our # staging quota to below the required size to stage the image # before copy. This request should succeed, but the copy task # should fail the staging quota check. self.set_limit({'image_size_total': 15, 'image_count_total': 10, 'image_stage_total': 5, 'image_count_uploading': 10}) req = self._import_copy(image_id, ['store3']) self.assertEqual(202, req.status_code) self._wait_for_import(image_id) self.assertEqual('failure', self._get_latest_task(image_id)['status']) # If we increase our stage quota, we should now be able to copy. self.set_limit({'image_size_total': 15, 'image_count_total': 10, 'image_stage_total': 10, 'image_count_uploading': 10}) req = self._import_copy(image_id, ['store3']) self.assertEqual(202, req.status_code) self._wait_for_import(image_id) self.assertEqual('success', self._get_latest_task(image_id)['status']) def test_stage(self): # Set a quota of 5MiB self.set_limit({'image_size_total': 15, 'image_stage_total': 5, 'image_count_total': 10, 'image_count_uploading': 10}) self.start_server() # Stage 6MiB, which is allowed to complete, but leaves us over # quota image_id = self._create_and_stage( data_iter=test_utils.FakeData(6 * units.Mi)) # Second stage fails because we are out of quota self._create_and_stage(expected_code=413) # Make sure that a web-download fails to actually run. image_id2 = self._create().json['id'] req = self._import_web_download(image_id2, ['store1'], 'http://example.com/foo.img') self.assertEqual(202, req.status_code) self._wait_for_import(image_id2) task = self._get_latest_task(image_id2) self.assertEqual('failure', task['status']) self.assertIn('image_stage_total is over limit', task['message']) # Finish importing one of the images, which should put us under quota # for staging req = self._import_direct(image_id, ['store1']) self.assertEqual(202, req.status_code) self._wait_for_import(image_id) # Stage should now succeed because we have freed up quota self._create_and_stage( data_iter=test_utils.FakeData(6 * units.Mi)) def test_create(self): # Set a quota of 2 images self.set_limit({'image_size_total': 15, 'image_count_total': 2, 'image_count_uploading': 10}) self.start_server() # Create one image image_id = self._create().json['id'] # Create a second. This leaves us *at* quota self._create() # Attempt to create a third is rejected as OverLimit resp = self._create() self.assertEqual(413, resp.status_code) # Delete one image, which should put us under quota self.api_delete('/v2/images/%s' % image_id) # Now we can create that third image self._create() def test_uploading_methods(self): self.set_limit({'image_size_total': 100, 'image_stage_total': 100, 'image_count_total': 100, 'image_count_uploading': 1}) self.start_server() # Create and stage one image. We are now at quota for count_uploading. image_id = self._create_and_stage() # Make sure we can not stage any more images. self._create_and_stage(expected_code=413) # Make sure we can not upload any more images. self._create_and_upload(expected_code=413) # Finish importing one of the images, which should put us under quota # for count_uploading. resp = self._import_direct(image_id, ['store1']) self.assertEqual(202, resp.status_code) self.assertEqual('active', self._wait_for_import(image_id)['status']) # Make sure we can upload now. self._create_and_upload() # Stage another, which should put us at quota for count_uploading. image_id2 = self._create_and_stage() # Start a copy. The request should succeed (because async) but # the task should ultimately fail because we are over quota. # NOTE(danms): It would be nice to try to do another copy or # upload while this is running, but since the task is fully # async and the copy happens quickly, we can't really time it # to avoid an unstable test (without some mocking). resp = self._import_copy(image_id, ['store2']) self.assertEqual(202, resp.status_code) self._wait_for_import(image_id) task = self._get_latest_task(image_id) self.assertEqual('failure', task['status']) self.assertIn('Resource image_count_uploading is over limit', task['message']) # Finish the staged import. self._import_direct(image_id2, ['store1']) self.assertEqual(202, resp.status_code) self._wait_for_import(image_id2) # Make sure we can upload again after the import finishes. self._create_and_upload() # Re-try the copy that should now succeed and wait for it to # finish. resp = self._import_copy(image_id, ['store2']) self.assertEqual(202, resp.status_code) self._wait_for_import(image_id) task = self._get_latest_task(image_id) self.assertEqual('success', task['status']) # Make sure we can still upload. self._create_and_upload() # Make sure we can still import. self._create_and_import(stores=['store1'])