Add image task client and image tests task APIs.

This patch add task APIs as in the doc following and tests for task APIs.

[doc]https://docs.openstack.org/api-ref/image/v2/#tasks

Change-Id: I237e0467a0e8edde0b2858551d8bc01e8ba410e0
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 c5da412..59c66b5 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -222,6 +222,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")