Rework exceptions in Tempest

* Add base exception class similar to other OS projects
* Catch certain HTTP errors and raise exceptions in base
  client classes
* Fixes LP Bug#899701 by adding tearDownClass() method
  to the test_list_images.ListImagesTest class to destroy
  images and instances the test case creates

Change-Id: I0f616813539b31da27e5106a59c2ca3765b1919f
diff --git a/tempest/common/rest_client.py b/tempest/common/rest_client.py
index 690738f..ea09cc0 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -105,6 +105,10 @@
         req_url = "%s/%s" % (self.base_url, url)
         resp, body = self.http_obj.request(req_url, method,
                                            headers=headers, body=body)
+
+        if resp.status == 404:
+            raise exceptions.NotFound(body)
+
         if resp.status == 400:
             body = json.loads(body)
             raise exceptions.BadRequest(body['badRequest']['message'])
diff --git a/tempest/exceptions.py b/tempest/exceptions.py
index 73798eb..f1becda 100644
--- a/tempest/exceptions.py
+++ b/tempest/exceptions.py
@@ -1,56 +1,64 @@
-class TimeoutException(Exception):
-    """Exception on timeout"""
-    def __init__(self, message):
-        self.message = message
+class TempestException(Exception):
+    """
+    Base Tempest Exception
+
+    To correctly use this class, inherit from it and define
+    a 'message' property. That message will get printf'd
+    with the keyword arguments provided to the constructor.
+    """
+    message = "An unknown exception occurred"
+
+    def __init__(self, *args, **kwargs):
+        try:
+            self._error_string = self.message % kwargs
+        except Exception:
+            # at least get the core message out if something happened
+            self._error_string = self.message
+        if len(args) > 0:
+            # If there is a non-kwarg parameter, assume it's the error
+            # message or reason description and tack it on to the end
+            # of the exception message
+            # Convert all arguments into their string representations...
+            args = ["%s" % arg for arg in args]
+            self._error_string = (self._error_string +
+                                  "\nDetails: %s" % '\n'.join(args))
 
     def __str__(self):
-        return repr(self.message)
+        return self._error_string
 
 
-class BuildErrorException(Exception):
-    """Server entered ERROR status unintentionally"""
-    def __init__(self, message):
-        self.message = message
-
-    def __str__(self):
-        return repr(self.message)
+class NotFound(TempestException):
+    message = "Object not found"
 
 
-class BadRequest(Exception):
-    def __init__(self, message):
-        self.message = message
-
-    def __str__(self):
-        return repr(self.message)
+class TimeoutException(TempestException):
+    message = "Request timed out"
 
 
-class AuthenticationFailure(Exception):
-    msg = ("Authentication with user %(user)s and password "
-           "%(password)s failed.")
-
-    def __init__(self, **kwargs):
-        self.message = self.msg % kwargs
+class BuildErrorException(TempestException):
+    message = "Server %(server_id)s failed to build and is in ERROR status"
 
 
-class EndpointNotFound(Exception):
-    def __init__(self, message):
-        self.message = message
-
-    def __str__(self):
-        return repr(self.message)
+class BadRequest(TempestException):
+    message = "Bad request"
 
 
-class OverLimit(Exception):
-    def __init__(self, message):
-        self.message = message
-
-    def __str__(self):
-        return repr(self.message)
+class AuthenticationFailure(TempestException):
+    message = ("Authentication with user %(user)s and password "
+               "%(password)s failed")
 
 
-class ComputeFault(Exception):
-    def __init__(self, message):
-        self.message = message
+class EndpointNotFound(TempestException):
+    message = "Endpoint not found"
 
-    def __str__(self):
-        return repr(self.message)
+
+class OverLimit(TempestException):
+    message = "Quota exceeded"
+
+
+class ComputeFault(TempestException):
+    message = "Got compute fault"
+
+
+class Duplicate(TempestException):
+    message = "An object with that identifier already exists"
diff --git a/tempest/services/nova/json/servers_client.py b/tempest/services/nova/json/servers_client.py
index 2be30c5..4f2e257 100644
--- a/tempest/services/nova/json/servers_client.py
+++ b/tempest/services/nova/json/servers_client.py
@@ -55,6 +55,7 @@
 
         post_body = json.dumps({'server': post_body})
         resp, body = self.client.post('servers', post_body, self.headers)
+
         body = json.loads(body)
         return resp, body['server']
 
@@ -140,11 +141,10 @@
             resp, body = self.get_server(server_id)
             server_status = body['status']
 
-            if(server_status == 'ERROR'):
-                message = 'Server %s entered ERROR status.' % server_id
-                raise exceptions.BuildErrorException(message)
+            if server_status == 'ERROR':
+                raise exceptions.BuildErrorException(server_id=server_id)
 
-            if (int(time.time()) - start >= self.build_timeout):
+            if int(time.time()) - start >= self.build_timeout:
                 message = 'Server %s failed to reach ACTIVE status within the \
                 required time (%s s).' % (server_id, self.build_timeout)
                 raise exceptions.TimeoutException(message)
diff --git a/tempest/tests/test_flavors.py b/tempest/tests/test_flavors.py
index aadcc17..70ae9db 100644
--- a/tempest/tests/test_flavors.py
+++ b/tempest/tests/test_flavors.py
@@ -1,7 +1,10 @@
-from nose.plugins.attrib import attr
-from tempest import openstack
 import unittest2 as unittest
 
+from nose.plugins.attrib import attr
+
+from tempest import exceptions
+from tempest import openstack
+
 
 class FlavorsTest(unittest.TestCase):
 
@@ -40,9 +43,5 @@
     @attr(type='negative')
     def test_get_non_existant_flavor(self):
         """flavor details are not returned for non existant flavors"""
-        try:
-            resp, flavor = self.client.get_flavor_details(999)
-        except:
-            pass
-        else:
-            self.fail('Should not get details for a non-existant flavor')
+        self.assertRaises(exceptions.NotFound, self.client.get_flavor_details,
+                          999)
diff --git a/tempest/tests/test_list_images.py b/tempest/tests/test_list_images.py
index 363678b..1e30651 100644
--- a/tempest/tests/test_list_images.py
+++ b/tempest/tests/test_list_images.py
@@ -1,7 +1,10 @@
+import unittest2 as unittest
+
 from nose.plugins.attrib import attr
+
+from tempest import exceptions
 from tempest import openstack
 from tempest.common.utils.data_utils import rand_name
-import unittest2 as unittest
 
 
 def _parse_image_id(image_ref):
@@ -32,7 +35,7 @@
                                                               cls.flavor_ref)
         cls.servers_client.wait_for_server_status(cls.server2['id'], 'ACTIVE')
 
-        #Create images to be used in the filter tests
+        # Create images to be used in the filter tests
         image1_name = rand_name('image')
         resp, body = cls.client.create_image(cls.server1['id'], image1_name)
         cls.image1_id = _parse_image_id(resp['location'])
@@ -54,12 +57,26 @@
         cls.client.wait_for_image_status(cls.image3_id, 'ACTIVE')
         resp, cls.image3 = cls.client.get_image(cls.image3_id)
 
+    @classmethod
+    def tearDownClass(cls):
+        cls.client.delete_image(cls.image1_id)
+        cls.client.delete_image(cls.image2_id)
+        cls.client.delete_image(cls.image3_id)
+        cls.servers_client.delete_server(cls.server1['id'])
+        cls.servers_client.delete_server(cls.server2['id'])
+
     @attr(type='smoke')
     def test_get_image(self):
         """Returns the correct details for a single image"""
         resp, image = self.client.get_image(self.image_ref)
         self.assertEqual(self.image_ref, image['id'])
 
+    @attr(type='negative')
+    def test_get_image_not_existing(self):
+        """Check raises a NotFound"""
+        self.assertRaises(exceptions.NotFound, self.client.get_image,
+                          "nonexistingimageid")
+
     @attr(type='smoke')
     def test_list_images(self):
         """The list of all images should contain the image"""
@@ -91,13 +108,16 @@
         self.assertFalse(any([i for i in images if i['id'] == self.image2_id]))
         self.assertFalse(any([i for i in images if i['id'] == self.image3_id]))
 
+    @unittest.skip('Skipping until Nova Bug 912837 is fixed')
     @attr(type='positive')
     def test_list_images_filter_by_server_id(self):
         """The images should contain images filtered by server id"""
         params = {'server': self.server1['id']}
         resp, images = self.client.list_images(params)
 
-        self.assertTrue(any([i for i in images if i['id'] == self.image1_id]))
+        self.assertTrue(any([i for i in images if i['id'] == self.image1_id]),
+                        "Failed to find image %s in images. Got images %s" %
+                        (self.image1_id, images))
         self.assertTrue(any([i for i in images if i['id'] == self.image2_id]))
         self.assertFalse(any([i for i in images if i['id'] == self.image3_id]))
 
diff --git a/tempest/tests/test_list_servers.py b/tempest/tests/test_list_servers.py
index 59c31a6..8917f48 100644
--- a/tempest/tests/test_list_servers.py
+++ b/tempest/tests/test_list_servers.py
@@ -1,7 +1,9 @@
 import unittest2 as unittest
 
-from tempest import exceptions
+import nose.plugins.skip
+
 from tempest import openstack
+from tempest import exceptions
 from tempest.common.utils.data_utils import rand_name
 from tempest.tests import utils
 
@@ -12,6 +14,7 @@
     def setUpClass(cls):
         cls.os = openstack.Manager()
         cls.client = cls.os.servers_client
+        cls.images_client = cls.os.images_client
         cls.config = cls.os.config
         cls.image_ref = cls.config.env.image_ref
         cls.flavor_ref = cls.config.env.flavor_ref
@@ -28,6 +31,24 @@
         else:
             cls.image_ref_alt = cls.image_ref
 
+        # Do some sanity checks here. If one of the images does
+        # not exist, or image_ref and image_ref_alt are the same,
+        # fail early since the tests won't work...
+        if cls.image_ref != cls.image_ref_alt:
+            cls.image_ref_alt_different = True
+
+        try:
+            cls.images_client.get_image(cls.image_ref)
+        except exceptions.NotFound:
+            raise RuntimeError("Image %s (image_ref) was not found!" %
+                               cls.image_ref)
+
+        try:
+            cls.images_client.get_image(cls.image_ref_alt)
+        except exceptions.NotFound:
+            raise RuntimeError("Image %s (image_ref_alt) was not found!" %
+                               cls.image_ref_alt)
+
         cls.s1_name = rand_name('server')
         resp, server = cls.client.create_server(cls.s1_name, cls.image_ref,
                                                 cls.flavor_ref)