diff --git a/glance/tests/functional/v2/test_images.py b/glance/tests/functional/v2/test_images.py index cca59e8980..07625647a4 100644 --- a/glance/tests/functional/v2/test_images.py +++ b/glance/tests/functional/v2/test_images.py @@ -15,6 +15,7 @@ import os import signal +import time import uuid from oslo_serialization import jsonutils @@ -135,6 +136,269 @@ class TestImages(functional.FunctionalTest): self.assertEqual(http.UNAUTHORIZED, response.status_code) self.stop_servers() + def test_image_import_using_glance_direct(self): + self.api_server.enable_image_import = 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)) + + # 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'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', + ]) + 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']) + + def _verify_image_checksum_and_status(checksum=None, status=None): + # Checksum should be populated and status should be active + 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(checksum, image['checksum']) + self.assertEqual(status, image['status']) + + # Upload some image data to staging area + path = self._url('/v2/images/%s/stage' % 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) + + # Verify image is in uploading state and checksum is None + _verify_image_checksum_and_status(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. + time.sleep(0.5) + _verify_image_checksum_and_status( + checksum='8f113e38d28a79a5a451b16048cc2b72', + 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.api_server.enable_image_import = True + self.config(node_staging_uri="file:///tmp/staging/") + 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'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', + ]) + 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']) + + def _verify_image_checksum_and_status(checksum=None, status=None): + # Checksum should be populated and status should be active + 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(checksum, image['checksum']) + self.assertEqual(status, image['status']) + + # Verify image is in queued state and checksum is None + _verify_image_checksum_and_status(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', + }) + data = jsonutils.dumps({'method': { + 'name': 'web-download', + 'uri': 'https://www.openstack.org/assets/openstack-logo/' + '2016R/OpenStack-Logo-Horizontal.eps.zip' + }}) + 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. + time.sleep(5) + _verify_image_checksum_and_status( + checksum='bcd65f8922f61a9e6a20572ad7aa2bdd', + status='active') + + # 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