Merge "Add image task client and image tests task APIs."
diff --git a/releasenotes/notes/add-image-task-apis-as-tempest-clients-228ccba01f59cbf3.yaml b/releasenotes/notes/add-image-task-apis-as-tempest-clients-228ccba01f59cbf3.yaml
new file mode 100644
index 0000000..cb99a29
--- /dev/null
+++ b/releasenotes/notes/add-image-task-apis-as-tempest-clients-228ccba01f59cbf3.yaml
@@ -0,0 +1,54 @@
+---
+features:
+ - |
+ The following ``tasks_client`` tempest client for glance v2 image
+ task API is implemented in this release.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tempest/api/image/v2/admin/test_image_task.py b/tempest/api/image/v2/admin/test_image_task.py
new file mode 100644
index 0000000..8cebdae
--- /dev/null
+++ b/tempest/api/image/v2/admin/test_image_task.py
@@ -0,0 +1,139 @@
+# Copyright 2023 Red Hat, Inc.
+# 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 tempest.api.image import base
+from tempest.common import waiters
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+class ImageTaskCreate(base.BaseV2ImageAdminTest):
+ """Test image task operations"""
+
+ @classmethod
+ def skip_checks(cls):
+ # TODO(msava): Add additional skipcheck with task conversion_format and
+ # glance ceph backend then will be available
+ # in tempest image service config options.
+ super(ImageTaskCreate, cls).skip_checks()
+ if not CONF.image.http_image:
+ skip_msg = ("%s skipped as http_image is not available " %
+ cls.__name__)
+ raise cls.skipException(skip_msg)
+
+ @classmethod
+ def resource_setup(cls):
+ super(ImageTaskCreate, cls).resource_setup()
+
+ @staticmethod
+ def _prepare_image_tasks_param(type="import",
+ disk_format=['qcow2'],
+ image_from_format=['qcow2'],
+ image_location=CONF.image.http_image):
+ # TODO(msava): Need to add additional disk formats then
+ # task conversion_format and glance Ceph backend will be
+ # available in image service options
+ """Prepare image task params.
+ By default, will create task type 'import'
+
+ The same index is used for both params and creates a task
+ :param type Type of the task.
+ :param disk_format: Each format in the list is a different task.
+ :param image_from_format: Each format in the list is a different task.
+ :param image_location Location to import image from.
+ :return: A list with all task.
+ """
+ i = 0
+ tasks = list()
+ while i < len(disk_format):
+ image_name = data_utils.rand_name("task_image")
+ image_property = {"container_format": "bare",
+ "disk_format": disk_format[0],
+ "visibility": "public",
+ "name": image_name
+ }
+ task = {
+ "type": type,
+ "input": {
+ "image_properties": image_property,
+ "import_from_format": image_from_format[0],
+ "import_from": image_location
+ }
+ }
+ tasks.append(task)
+ i += 1
+ return tasks
+
+ def _verify_disk_format(self, task_body):
+ expected_disk_format = \
+ task_body['input']['image_properties']['disk_format']
+ image_id = task_body['result']['image_id']
+ observed_disk_format = self.admin_client.show_image(
+ image_id)['disk_format']
+ # If glance backend storage is Ceph glance will convert
+ # image to raw format.
+ # TODO(msava): Need to change next lines once task conversion_format
+ # and glance ceph backend will be available in image service options
+ if observed_disk_format == 'raw':
+ return
+ self.assertEqual(observed_disk_format, expected_disk_format,
+ message="Expected disk format not match ")
+
+ @decorators.idempotent_id('669d5387-0340-4abf-b62d-7cc89f539c8c')
+ def test_image_tasks_create(self):
+ """Test task type 'import' image """
+
+ # Prepare params for task type 'import'
+ tasks = self._prepare_image_tasks_param()
+
+ # Create task type 'import'
+ body = self.os_admin.tasks_client.create_task(**tasks[0])
+ task_id = body['id']
+ task_body = waiters.wait_for_tasks_status(self.os_admin.tasks_client,
+ task_id, 'success')
+ self.addCleanup(self.admin_client.delete_image,
+ task_body['result']['image_id'])
+ task_image_id = task_body['result']['image_id']
+ waiters.wait_for_image_status(self.client, task_image_id, 'active')
+ self._verify_disk_format(task_body)
+
+ # Verify disk format
+ image_body = self.client.show_image(task_image_id)
+ task_disk_format = \
+ task_body['input']['image_properties']['disk_format']
+ image_disk_format = image_body['disk_format']
+ self.assertEqual(
+ image_disk_format, task_disk_format,
+ message="Image Disc format %s not match to expected %s"
+ % (image_disk_format, task_disk_format))
+
+ @decorators.idempotent_id("ad6450c6-7060-4ee7-a2d1-41c2604b446c")
+ @decorators.attr(type=['negative'])
+ def test_task_create_fake_image_location(self):
+ http_fake_url = ''.join(
+ ["http://", data_utils.rand_name('dummy-img-file'), ".qcow2"])
+ task = self._prepare_image_tasks_param(
+ image_from_format=['qcow2'],
+ disk_format=['qcow2'],
+ image_location=http_fake_url)
+ body = self.os_admin.tasks_client.create_task(**task[0])
+ task_observed = \
+ waiters.wait_for_tasks_status(self.os_admin.tasks_client,
+ body['id'], 'failure')
+ task_observed = task_observed['status']
+ self.assertEqual(task_observed, 'failure')
diff --git a/tempest/clients.py b/tempest/clients.py
index 1aa34d0..99e114c 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -97,6 +97,7 @@
self.image_v2.NamespacePropertiesClient()
self.namespace_tags_client = self.image_v2.NamespaceTagsClient()
self.image_versions_client = self.image_v2.VersionsClient()
+ self.tasks_client = self.image_v2.TaskClient()
# NOTE(danms): If no alternate endpoint is configured,
# this client will work the same as the base self.images_client.
# If your test needs to know if these are different, check the
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index d88bc05..291f201 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -224,6 +224,24 @@
raise lib_exc.TimeoutException(message)
+def wait_for_tasks_status(client, task_id, status):
+ start = int(time.time())
+ while int(time.time()) - start < client.build_timeout:
+ task = client.show_tasks(task_id)
+ if task['status'] == status:
+ return task
+ time.sleep(client.build_interval)
+ message = ('Task %(task_id)s tasks: '
+ 'failed to reach %(status)s state within the required '
+ 'time (%(timeout)s s).' % {'task_id': task_id,
+ 'status': status,
+ 'timeout': client.build_timeout})
+ caller = test_utils.find_test_caller()
+ if caller:
+ message = '(%s) %s' % (caller, message)
+ raise lib_exc.TimeoutException(message)
+
+
def wait_for_image_imported_to_stores(client, image_id, stores=None):
"""Waits for an image to be imported to all requested stores.
diff --git a/tempest/lib/services/image/v2/__init__.py b/tempest/lib/services/image/v2/__init__.py
index a2f5bdc..5e303e3 100644
--- a/tempest/lib/services/image/v2/__init__.py
+++ b/tempest/lib/services/image/v2/__init__.py
@@ -27,9 +27,11 @@
from tempest.lib.services.image.v2.resource_types_client import \
ResourceTypesClient
from tempest.lib.services.image.v2.schemas_client import SchemasClient
+from tempest.lib.services.image.v2.tasks_client import TaskClient
from tempest.lib.services.image.v2.versions_client import VersionsClient
+
__all__ = ['ImageMembersClient', 'ImagesClient', 'ImageCacheClient',
'NamespaceObjectsClient', 'NamespacePropertiesClient',
'NamespaceTagsClient', 'NamespacesClient', 'ResourceTypesClient',
- 'SchemasClient', 'VersionsClient']
+ 'SchemasClient', 'TaskClient', 'VersionsClient']
diff --git a/tempest/lib/services/image/v2/tasks_client.py b/tempest/lib/services/image/v2/tasks_client.py
new file mode 100644
index 0000000..2cb33eb
--- /dev/null
+++ b/tempest/lib/services/image/v2/tasks_client.py
@@ -0,0 +1,70 @@
+# Copyright 2023 Red Hat, Inc.
+# 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 urllib import parse as urllib
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+CHUNKSIZE = 1024 * 64 # 64kB
+
+
+class TaskClient(rest_client.RestClient):
+ api_version = "v2"
+
+ def create_task(self, **kwargs):
+ """Create a task.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/image/v2/#create-task
+ """
+ data = json.dumps(kwargs)
+ resp, body = self.post('tasks', data)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_tasks(self, **kwargs):
+ """List tasks.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/image/v2/#list-tasks
+ """
+ url = 'tasks'
+
+ if kwargs:
+ url += '?%s' % urllib.urlencode(kwargs)
+
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_tasks(self, task_id):
+ """Show task details.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://docs.openstack.org/api-ref/image/v2/#show-task-details
+ """
+ url = 'tasks/%s' % task_id
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/tests/lib/services/image/v2/test_image_tasks_client.py b/tempest/tests/lib/services/image/v2/test_image_tasks_client.py
new file mode 100644
index 0000000..6e3b3b5
--- /dev/null
+++ b/tempest/tests/lib/services/image/v2/test_image_tasks_client.py
@@ -0,0 +1,86 @@
+# Copyright 2023 Red Hat, Inc. 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 tempest.lib.services.image.v2 import tasks_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestImageTaskClient(base.BaseServiceTest):
+ def setUp(self):
+ super(TestImageTaskClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = tasks_client.TaskClient(
+ fake_auth, 'image', 'regionOne')
+
+ def test_list_task(self):
+ fake_result = {
+
+ "first": "/v2/tasks",
+ "schema": "/v2/schemas/tasks",
+ "tasks": [
+ {
+ "id": "08b7e1c8-3821-4f54-b3b8-d6655d178cdf",
+ "owner": "fa6c8c1600f4444281658a23ee6da8e8",
+ "schema": "/v2/schemas/task",
+ "self": "/v2/tasks/08b7e1c8-3821-4f54-b3b8-d6655d178cdf",
+ "status": "processing",
+ "type": "import"
+ },
+ {
+ "id": "231c311d-3557-4e23-afc4-6d98af1419e7",
+ "owner": "fa6c8c1600f4444281658a23ee6da8e8",
+ "schema": "/v2/schemas/task",
+ "self": "/v2/tasks/231c311d-3557-4e23-afc4-6d98af1419e7",
+ "status": "processing",
+ "type": "import"
+ }
+ ]
+ }
+ self.check_service_client_function(
+ self.client.list_tasks,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ fake_result,
+ mock_args=['tasks'])
+
+ def test_create_task(self):
+ fake_result = {
+ "type": "import",
+ "input": {
+ "import_from":
+ "http://download.cirros-cloud.net/0.6.1/ \
+ cirros-0.6.1-x86_64-disk.img",
+ "import_from_format": "qcow2",
+ "image_properties": {
+ "disk_format": "qcow2",
+ "container_format": "bare"
+ }
+ }
+ }
+ self.check_service_client_function(
+ self.client.create_task,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ fake_result,
+ status=201)
+
+ def test_show_task(self):
+ fake_result = {
+ "task_id": "08b7e1c8-3821-4f54-b3b8-d6655d178cdf"
+ }
+ self.check_service_client_function(
+ self.client.show_tasks,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ fake_result,
+ status=200,
+ task_id="e485aab9-0907-4973-921c-bb6da8a8fcf8")