Move common wait_for_image_status from compute images_client to waiters

The wait_for_image_status method in the compute json/xml images_clients
was copy/paste and needed a better error message when it times out, so
rather than write a better error message and copy it in both places,
move the common code into waiters and make the json/xml clients call
that instead, like we do for wait_for_server_status.

The improved error message should help in debugging that failure when
it's hit.

Related-Bug: #1260982

Change-Id: I5e3e33310a91da71467fa744972f1a0e4c0bdb50
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index 44198f0..d2b40c9 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -88,3 +88,34 @@
             raise exceptions.TimeoutException(message)
         old_status = server_status
         old_task_state = task_state
+
+
+def wait_for_image_status(client, image_id, status):
+    """Waits for an image to reach a given status.
+
+    The client should have a get_image(image_id) method to get the image.
+    The client should also have build_interval and build_timeout attributes.
+    """
+    resp, image = client.get_image(image_id)
+    start = int(time.time())
+
+    while image['status'] != status:
+        time.sleep(client.build_interval)
+        resp, image = client.get_image(image_id)
+        if image['status'] == 'ERROR':
+            raise exceptions.AddImageException(image_id=image_id)
+
+        # check the status again to avoid a false negative where we hit
+        # the timeout at the same time that the image reached the expected
+        # status
+        if image['status'] == status:
+            return
+
+        if int(time.time()) - start >= client.build_timeout:
+            message = ('Image %(image_id)s failed to reach %(status)s '
+                       'status within the required time (%(timeout)s s).' %
+                       {'image_id': image_id,
+                        'status': status,
+                        'timeout': client.build_timeout})
+            message += ' Current status: %s.' % image['status']
+            raise exceptions.TimeoutException(message)
diff --git a/tempest/services/compute/json/images_client.py b/tempest/services/compute/json/images_client.py
index 5f17894..c05571a 100644
--- a/tempest/services/compute/json/images_client.py
+++ b/tempest/services/compute/json/images_client.py
@@ -16,10 +16,10 @@
 #    under the License.
 
 import json
-import time
 import urllib
 
 from tempest.common.rest_client import RestClient
+from tempest.common import waiters
 from tempest import exceptions
 
 
@@ -82,18 +82,7 @@
 
     def wait_for_image_status(self, image_id, status):
         """Waits for an image to reach a given status."""
-        resp, image = self.get_image(image_id)
-        start = int(time.time())
-
-        while image['status'] != status:
-            time.sleep(self.build_interval)
-            resp, image = self.get_image(image_id)
-
-            if image['status'] == 'ERROR':
-                raise exceptions.AddImageException(image_id=image_id)
-
-            if int(time.time()) - start >= self.build_timeout:
-                raise exceptions.TimeoutException
+        waiters.wait_for_image_status(self, image_id, status)
 
     def list_image_metadata(self, image_id):
         """Lists all metadata items for an image."""
diff --git a/tempest/services/compute/xml/images_client.py b/tempest/services/compute/xml/images_client.py
index b17ae78..20fcc9b 100644
--- a/tempest/services/compute/xml/images_client.py
+++ b/tempest/services/compute/xml/images_client.py
@@ -15,12 +15,12 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-import time
 import urllib
 
 from lxml import etree
 
 from tempest.common.rest_client import RestClientXML
+from tempest.common import waiters
 from tempest import exceptions
 from tempest.services.compute.xml.common import Document
 from tempest.services.compute.xml.common import Element
@@ -140,17 +140,7 @@
 
     def wait_for_image_status(self, image_id, status):
         """Waits for an image to reach a given status."""
-        resp, image = self.get_image(image_id)
-        start = int(time.time())
-
-        while image['status'] != status:
-            time.sleep(self.build_interval)
-            resp, image = self.get_image(image_id)
-            if image['status'] == 'ERROR':
-                raise exceptions.AddImageException(image_id=image_id)
-
-            if int(time.time()) - start >= self.build_timeout:
-                raise exceptions.TimeoutException
+        waiters.wait_for_image_status(self, image_id, status)
 
     def _metadata_body(self, meta):
         post_body = Element('metadata')