diff options
-rw-r--r-- | tempest_lib/api_schema/response/compute/v2_1/images.py | 154 | ||||
-rw-r--r-- | tempest_lib/services/compute/images_client.py | 122 | ||||
-rw-r--r-- | tempest_lib/tests/services/compute/test_images_client.py | 240 |
3 files changed, 516 insertions, 0 deletions
diff --git a/tempest_lib/api_schema/response/compute/v2_1/images.py b/tempest_lib/api_schema/response/compute/v2_1/images.py new file mode 100644 index 0000000..b8c4151 --- /dev/null +++ b/tempest_lib/api_schema/response/compute/v2_1/images.py @@ -0,0 +1,154 @@ +# Copyright 2014 NEC Corporation. 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 copy + +from tempest_lib.api_schema.response.compute.v2_1 import parameter_types + +image_links = copy.deepcopy(parameter_types.links) +image_links['items']['properties'].update({'type': {'type': 'string'}}) + +common_image_schema = { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'status': {'type': 'string'}, + 'updated': {'type': 'string'}, + 'links': image_links, + 'name': {'type': 'string'}, + 'created': {'type': 'string'}, + 'minDisk': {'type': 'integer'}, + 'minRam': {'type': 'integer'}, + 'progress': {'type': 'integer'}, + 'metadata': {'type': 'object'}, + 'server': { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'links': parameter_types.links + }, + 'additionalProperties': False, + 'required': ['id', 'links'] + }, + 'OS-EXT-IMG-SIZE:size': {'type': 'integer'}, + 'OS-DCF:diskConfig': {'type': 'string'} + }, + 'additionalProperties': False, + # 'server' attributes only comes in response body if image is + # associated with any server. 'OS-EXT-IMG-SIZE:size' & 'OS-DCF:diskConfig' + # are API extension, So those are not defined as 'required'. + 'required': ['id', 'status', 'updated', 'links', 'name', + 'created', 'minDisk', 'minRam', 'progress', + 'metadata'] +} + +get_image = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'image': common_image_schema + }, + 'additionalProperties': False, + 'required': ['image'] + } +} + +list_images = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'images': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'links': image_links, + 'name': {'type': 'string'} + }, + 'additionalProperties': False, + 'required': ['id', 'links', 'name'] + } + }, + 'images_links': parameter_types.links + }, + 'additionalProperties': False, + # NOTE(gmann): images_links attribute is not necessary to be + # present always So it is not 'required'. + 'required': ['images'] + } +} + +create_image = { + 'status_code': [202], + 'response_header': { + 'type': 'object', + 'properties': parameter_types.response_header + } +} +create_image['response_header']['properties'].update( + {'location': { + 'type': 'string', + 'format': 'uri'} + } +) +create_image['response_header']['required'] = ['location'] + +delete = { + 'status_code': [204] +} + +image_metadata = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'metadata': {'type': 'object'} + }, + 'additionalProperties': False, + 'required': ['metadata'] + } +} + +image_meta_item = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'meta': {'type': 'object'} + }, + 'additionalProperties': False, + 'required': ['meta'] + } +} + +list_images_details = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'images': { + 'type': 'array', + 'items': common_image_schema + }, + 'images_links': parameter_types.links + }, + 'additionalProperties': False, + # NOTE(gmann): images_links attribute is not necessary to be + # present always So it is not 'required'. + 'required': ['images'] + } +} diff --git a/tempest_lib/services/compute/images_client.py b/tempest_lib/services/compute/images_client.py new file mode 100644 index 0000000..8232f22 --- /dev/null +++ b/tempest_lib/services/compute/images_client.py @@ -0,0 +1,122 @@ +# 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. + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urllib + +from tempest_lib.api_schema.response.compute.v2_1 import images as schema +from tempest_lib.common import rest_client +from tempest_lib import exceptions as lib_exc + + +class ImagesClient(rest_client.RestClient): + + def create_image(self, server_id, **kwargs): + """Creates an image of the original server.""" + + post_body = {'createImage': kwargs} + post_body = json.dumps(post_body) + resp, body = self.post('servers/%s/action' % server_id, + post_body) + self.validate_response(schema.create_image, resp, body) + return rest_client.ResponseBody(resp, body) + + def list_images(self, detail=False, **params): + """Returns a list of all images filtered by any parameters.""" + url = 'images' + _schema = schema.list_images + if detail: + url += '/detail' + _schema = schema.list_images_details + + if params: + url += '?%s' % urllib.urlencode(params) + + resp, body = self.get(url) + body = json.loads(body) + self.validate_response(_schema, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_image(self, image_id): + """Returns the details of a single image.""" + resp, body = self.get("images/%s" % image_id) + self.expected_success(200, resp.status) + body = json.loads(body) + self.validate_response(schema.get_image, resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_image(self, image_id): + """Deletes the provided image.""" + resp, body = self.delete("images/%s" % image_id) + self.validate_response(schema.delete, resp, body) + return rest_client.ResponseBody(resp, body) + + def list_image_metadata(self, image_id): + """Lists all metadata items for an image.""" + resp, body = self.get("images/%s/metadata" % image_id) + body = json.loads(body) + self.validate_response(schema.image_metadata, resp, body) + return rest_client.ResponseBody(resp, body) + + def set_image_metadata(self, image_id, meta): + """Sets the metadata for an image.""" + post_body = json.dumps({'metadata': meta}) + resp, body = self.put('images/%s/metadata' % image_id, post_body) + body = json.loads(body) + self.validate_response(schema.image_metadata, resp, body) + return rest_client.ResponseBody(resp, body) + + def update_image_metadata(self, image_id, meta): + """Updates the metadata for an image.""" + post_body = json.dumps({'metadata': meta}) + resp, body = self.post('images/%s/metadata' % image_id, post_body) + body = json.loads(body) + self.validate_response(schema.image_metadata, resp, body) + return rest_client.ResponseBody(resp, body) + + def show_image_metadata_item(self, image_id, key): + """Returns the value for a specific image metadata key.""" + resp, body = self.get("images/%s/metadata/%s" % (image_id, key)) + body = json.loads(body) + self.validate_response(schema.image_meta_item, resp, body) + return rest_client.ResponseBody(resp, body) + + def set_image_metadata_item(self, image_id, key, meta): + """Sets the value for a specific image metadata key.""" + post_body = json.dumps({'meta': meta}) + resp, body = self.put('images/%s/metadata/%s' % (image_id, key), + post_body) + body = json.loads(body) + self.validate_response(schema.image_meta_item, resp, body) + return rest_client.ResponseBody(resp, body) + + def delete_image_metadata_item(self, image_id, key): + """Deletes a single image metadata key/value pair.""" + resp, body = self.delete("images/%s/metadata/%s" % + (image_id, key)) + self.validate_response(schema.delete, resp, body) + return rest_client.ResponseBody(resp, body) + + def is_resource_deleted(self, id): + try: + self.show_image(id) + except lib_exc.NotFound: + return True + return False + + @property + def resource_type(self): + """Returns the primary type of resource this client works with.""" + return 'image' diff --git a/tempest_lib/tests/services/compute/test_images_client.py b/tempest_lib/tests/services/compute/test_images_client.py new file mode 100644 index 0000000..2172842 --- /dev/null +++ b/tempest_lib/tests/services/compute/test_images_client.py @@ -0,0 +1,240 @@ +# Copyright 2015 NEC Corporation. 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 copy + +from oslotest import mockpatch + +from tempest_lib import exceptions as lib_exc +from tempest_lib.services.compute import images_client +from tempest_lib.tests import fake_auth_provider +from tempest_lib.tests.services.compute import base + + +class TestImagesClient(base.BaseComputeServiceTest): + # Data Dictionaries used for testing # + FAKE_IMAGE_METADATA = { + "list": + {"metadata": { + "auto_disk_config": "True", + "Label": "Changed" + }}, + "set_item": + {"meta": { + "auto_disk_config": "True" + }}, + "show_item": + {"meta": { + "kernel_id": "nokernel", + }}, + "update": + {"metadata": { + "kernel_id": "False", + "Label": "UpdatedImage" + }}, + "set": + {"metadata": { + "Label": "Changed", + "auto_disk_config": "True" + }}, + "delete_item": {} + } + + FAKE_IMAGE_DATA = { + "list": + {"images": [ + {"id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + {"href": "http://openstack.example.com/v2/openstack" + + "/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "self" + } + ], + "name": "fakeimage7" + }]}, + "show": {"image": { + "created": "2011-01-01T01:02:03Z", + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/v2/openstack" + + "/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "self" + }, + ], + "metadata": { + "architecture": "x86_64", + "auto_disk_config": "True", + "kernel_id": "nokernel", + "ramdisk_id": "nokernel" + }, + "minDisk": 0, + "minRam": 0, + "name": "fakeimage7", + "progress": 100, + "status": "ACTIVE", + "updated": "2011-01-01T01:02:03Z"}}, + "delete": {} + } + func2mock = { + 'get': 'tempest_lib.common.rest_client.RestClient.get', + 'post': 'tempest_lib.common.rest_client.RestClient.post', + 'put': 'tempest_lib.common.rest_client.RestClient.put', + 'delete': 'tempest_lib.common.rest_client.RestClient.delete'} + # Variable definition + FAKE_IMAGE_ID = FAKE_IMAGE_DATA['show']['image']['id'] + FAKE_CREATE_INFO = {'location': 'None'} + FAKE_METADATA = FAKE_IMAGE_METADATA['show_item']['meta'] + + def setUp(self): + super(TestImagesClient, self).setUp() + fake_auth = fake_auth_provider.FakeAuthProvider() + self.client = images_client.ImagesClient(fake_auth, + "compute", "regionOne") + + def _test_image_operation(self, operation="delete", bytes_body=False): + response_code = 200 + mock_operation = self.func2mock['get'] + expected_op = self.FAKE_IMAGE_DATA[operation] + params = {"image_id": self.FAKE_IMAGE_ID} + if operation == 'list': + function = self.client.list_images + elif operation == 'show': + function = self.client.show_image + else: + function = self.client.delete_image + mock_operation = self.func2mock['delete'] + response_code = 204 + + self.check_service_client_function( + function, mock_operation, expected_op, + bytes_body, response_code, **params) + + def _test_image_metadata(self, operation="set_item", bytes_body=False): + response_code = 200 + expected_op = self.FAKE_IMAGE_METADATA[operation] + if operation == 'list': + function = self.client.list_image_metadata + mock_operation = self.func2mock['get'] + params = {"image_id": self.FAKE_IMAGE_ID} + + elif operation == 'set': + function = self.client.set_image_metadata + mock_operation = self.func2mock['put'] + params = {"image_id": "_dummy_data", + "meta": self.FAKE_METADATA} + + elif operation == 'update': + function = self.client.update_image_metadata + mock_operation = self.func2mock['post'] + params = {"image_id": self.FAKE_IMAGE_ID, + "meta": self.FAKE_METADATA} + + elif operation == 'show_item': + mock_operation = self.func2mock['get'] + function = self.client.show_image_metadata_item + params = {"image_id": self.FAKE_IMAGE_ID, + "key": "123"} + + elif operation == 'delete_item': + function = self.client.delete_image_metadata_item + mock_operation = self.func2mock['delete'] + response_code = 204 + params = {"image_id": self.FAKE_IMAGE_ID, + "key": "123"} + + else: + function = self.client.set_image_metadata_item + mock_operation = self.func2mock['put'] + params = {"image_id": self.FAKE_IMAGE_ID, + "key": "123", + "meta": self.FAKE_METADATA} + + self.check_service_client_function( + function, mock_operation, expected_op, + bytes_body, response_code, **params) + + def _test_resource_deleted(self, bytes_body=False): + params = {"id": self.FAKE_IMAGE_ID} + expected_op = self.FAKE_IMAGE_DATA['show']['image'] + self.useFixture(mockpatch.Patch('tempest_lib.services.compute' + '.images_client.ImagesClient.show_image', + side_effect=lib_exc.NotFound)) + self.assertEqual(True, self.client.is_resource_deleted(**params)) + tempdata = copy.deepcopy(self.FAKE_IMAGE_DATA['show']) + tempdata['image']['id'] = None + self.useFixture(mockpatch.Patch('tempest_lib.services.compute' + '.images_client.ImagesClient.show_image', + return_value=expected_op)) + self.assertEqual(False, self.client.is_resource_deleted(**params)) + + def test_list_images_with_str_body(self): + self._test_image_operation('list') + + def test_list_images_with_bytes_body(self): + self._test_image_operation('list', True) + + def test_show_image_with_str_body(self): + self._test_image_operation('show') + + def test_show_image_with_bytes_body(self): + self._test_image_operation('show', True) + + def test_delete_image_with_str_body(self): + self._test_image_operation('delete') + + def test_delete_image_with_bytes_body(self): + self._test_image_operation('delete', True) + + def test_list_image_metadata_with_str_body(self): + self._test_image_metadata('list') + + def test_list_image_metadata_with_bytes_body(self): + self._test_image_metadata('list', True) + + def test_set_image_metadata_with_str_body(self): + self._test_image_metadata('set') + + def test_set_image_metadata_with_bytes_body(self): + self._test_image_metadata('set', True) + + def test_update_image_metadata_with_str_body(self): + self._test_image_metadata('update') + + def test_update_image_metadata_with_bytes_body(self): + self._test_image_metadata('update', True) + + def test_set_image_metadata_item_with_str_body(self): + self._test_image_metadata() + + def test_set_image_metadata_item_with_bytes_body(self): + self._test_image_metadata(bytes_body=True) + + def test_show_image_metadata_item_with_str_body(self): + self._test_image_metadata('show_item') + + def test_show_image_metadata_item_with_bytes_body(self): + self._test_image_metadata('show_item', True) + + def test_delete_image_metadata_item_with_str_body(self): + self._test_image_metadata('delete_item') + + def test_delete_image_metadata_item_with_bytes_body(self): + self._test_image_metadata('delete_item', True) + + def test_resource_delete_with_str_body(self): + self._test_resource_deleted() + + def test_resource_delete_with_bytes_body(self): + self._test_resource_deleted(True) |