Add image task validation

Glance is adding a /image/$image/tasks API to make it easier to
consume the import API. This fetches the tasks after glance-direct
import, and validates that they look like we expect.

Because this is only supported after v2.12, we check for that before
doing the validation. To make that easier, this adds a has_version()
helper to the VersionsClient.

Change-Id: I2850f0659e82bf5c5a1005de0a063e7fcacadb51
diff --git a/releasenotes/notes/image-client-add-versions-and-tasks-ac289dbfe1c899cc.yaml b/releasenotes/notes/image-client-add-versions-and-tasks-ac289dbfe1c899cc.yaml
new file mode 100644
index 0000000..fde6193
--- /dev/null
+++ b/releasenotes/notes/image-client-add-versions-and-tasks-ac289dbfe1c899cc.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    Adds a method to images_client to get tasks relevant to a given image. Also adds
+    has_version() method to image versions_client to probe for availability of a given
+    API version.
diff --git a/tempest/api/image/v2/test_images.py b/tempest/api/image/v2/test_images.py
index 8dba311..efa23bb 100644
--- a/tempest/api/image/v2/test_images.py
+++ b/tempest/api/image/v2/test_images.py
@@ -96,9 +96,26 @@
 
         image_id = self._stage_and_check()
         # import image from staging to backend
-        self.client.image_import(image_id, method='glance-direct')
+        resp = self.client.image_import(image_id, method='glance-direct')
         waiters.wait_for_image_imported_to_stores(self.client, image_id)
 
+        if not self.versions_client.has_version('2.12'):
+            # API is not new enough to support image/tasks API
+            LOG.info('Glance does not support v2.12, so I am unable to '
+                     'validate the image/tasks API.')
+            return
+
+        # Make sure we can access the task and that some of the key
+        # fields look legit.
+        tasks = self.client.show_image_tasks(image_id)
+        self.assertEqual(1, len(tasks['tasks']))
+        task = tasks['tasks'][0]
+        self.assertEqual('success', task['status'])
+        self.assertEqual(resp.response['x-openstack-request-id'],
+                         task['request_id'])
+        self.assertEqual('glance-direct',
+                         task['input']['import_req']['method']['name'])
+
     @decorators.idempotent_id('f6feb7a4-b04f-4706-a011-206129f83e62')
     def test_image_web_download_import(self):
         """Test 'web-download' import functionalities
diff --git a/tempest/lib/services/image/v2/images_client.py b/tempest/lib/services/image/v2/images_client.py
index fa3bb8c..abf427c 100644
--- a/tempest/lib/services/image/v2/images_client.py
+++ b/tempest/lib/services/image/v2/images_client.py
@@ -121,6 +121,14 @@
         body = json.loads(body)
         return rest_client.ResponseBody(resp, body)
 
+    def show_image_tasks(self, image_id):
+        """Show image tasks."""
+        url = 'images/%s/tasks' % image_id
+        resp, body = self.get(url)
+        self.expected_success(200, resp.status)
+        body = json.loads(body)
+        return rest_client.ResponseBody(resp, body)
+
     def is_resource_deleted(self, id):
         try:
             self.show_image(id)
diff --git a/tempest/lib/services/image/v2/versions_client.py b/tempest/lib/services/image/v2/versions_client.py
index 1b7f806..98b4fb6 100644
--- a/tempest/lib/services/image/v2/versions_client.py
+++ b/tempest/lib/services/image/v2/versions_client.py
@@ -30,3 +30,13 @@
         self.expected_success(300, resp.status)
         body = json.loads(body)
         return rest_client.ResponseBody(resp, body)
+
+    def has_version(self, version):
+        """Return True if a version is supported."""
+        version = 'v%s' % version
+        supported = ['SUPPORTED', 'CURRENT']
+        versions = self.list_versions()
+        for version_struct in versions['versions']:
+            if version_struct['id'] == version:
+                return version_struct['status'] in supported
+        return False
diff --git a/tempest/tests/lib/services/image/v2/test_images_client.py b/tempest/tests/lib/services/image/v2/test_images_client.py
index 7ee61d2..5b162f8 100644
--- a/tempest/tests/lib/services/image/v2/test_images_client.py
+++ b/tempest/tests/lib/services/image/v2/test_images_client.py
@@ -105,6 +105,44 @@
         "first": "/v2/images"
     }
 
+    FAKE_SHOW_IMAGE_TASKS = {
+        "tasks": [
+            {
+                "id": "ee22890e-8948-4ea6-9668-831f973c84f5",
+                "image_id": "dddddddd-dddd-dddd-dddd-dddddddddddd",
+                "request-id": "rrrrrrr-rrrr-rrrr-rrrr-rrrrrrrrrrrr",
+                "user": "uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu",
+                "type": "api_image_import",
+                "status": "processing",
+                "owner": "64f0efc9955145aeb06f297a8a6fe402",
+                "expires_at": None,
+                "created_at": "2020-12-18T05:20:38.000000",
+                "updated_at": "2020-12-18T05:25:39.000000",
+                "deleted_at": None,
+                "deleted": False,
+                "input": {
+                    "image_id": "829c729b-ebc4-4cc7-a164-6f43f1149b17",
+                    "import_req": {
+                        "method": {
+                            "name": "copy-image",
+                        },
+                        "all_stores": True,
+                        "all_stores_must_succeed": False,
+                    },
+                    "backend": [
+                        "fast",
+                        "cheap",
+                        "slow",
+                        "reliable",
+                        "common",
+                    ]
+                },
+                "result": None,
+                "message": "Copied 15 MiB",
+            }
+        ]
+    }
+
     FAKE_TAG_NAME = "fake tag"
 
     def setUp(self):
@@ -230,3 +268,11 @@
 
     def test_list_images_with_bytes_body(self):
         self._test_list_images(bytes_body=True)
+
+    def test_show_image_tasks(self):
+        self.check_service_client_function(
+            self.client.show_image_tasks,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_SHOW_IMAGE_TASKS,
+            True,
+            image_id="e485aab9-0907-4973-921c-bb6da8a8fcf8")
diff --git a/tempest/tests/lib/services/image/v2/test_versions_client.py b/tempest/tests/lib/services/image/v2/test_versions_client.py
index 6234b06..98c558a 100644
--- a/tempest/tests/lib/services/image/v2/test_versions_client.py
+++ b/tempest/tests/lib/services/image/v2/test_versions_client.py
@@ -12,6 +12,8 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+import fixtures
+
 from tempest.lib.services.image.v2 import versions_client
 from tempest.tests.lib import fake_auth_provider
 from tempest.tests.lib.services import base
@@ -92,3 +94,13 @@
 
     def test_list_versions_with_bytes_body(self):
         self._test_list_versions(bytes_body=True)
+
+    def test_has_version(self):
+        mocked_r = self.create_response(self.FAKE_VERSIONS_INFO, False,
+                                        300, None)
+        self.useFixture(fixtures.MockPatch(
+            'tempest.lib.common.rest_client.RestClient.raw_request',
+            return_value=mocked_r))
+
+        self.assertTrue(self.client.has_version('2.1'))
+        self.assertFalse(self.client.has_version('9.9'))