Merge "Fix index link in footer bar"
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index db6a7bd..0af8e9b 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -229,6 +229,10 @@
 multi_backend_enabled = false
 backend1_name = BACKEND_1
 backend2_name = BACKEND_2
+# Protocol and vendor of volume backend to target when testing volume-types.
+# You should update to reflect those exported by configured backend driver.
+storage_protocol = iSCSI
+vendor_name = Open Source
 
 [object-storage]
 # This section contains configuration options used when executing tests
@@ -332,8 +336,8 @@
 # ssh username for the image file
 ssh_user = cirros
 
-[CLI]
+[cli]
 # Enable cli tests
 enabled = True
 # directory where python client binaries are located
-cli_dir = /usr/local/bin/
+cli_dir = /usr/local/bin
diff --git a/requirements.txt b/requirements.txt
index df9951d..606d7ae 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -19,4 +19,3 @@
 oslo.config>=1.1.0
 # Needed for whitebox testing
 sqlalchemy
-MySQL-python
diff --git a/tempest/api/compute/admin/test_fixed_ips.py b/tempest/api/compute/admin/test_fixed_ips.py
index f201cf7..34f96ba 100644
--- a/tempest/api/compute/admin/test_fixed_ips.py
+++ b/tempest/api/compute/admin/test_fixed_ips.py
@@ -15,7 +15,10 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import testtools
+
 from tempest.api.compute import base
+from tempest import config
 from tempest import exceptions
 from tempest.test import attr
 
@@ -51,6 +54,10 @@
 class FixedIPsTestJson(FixedIPsBase):
     _interface = 'json'
 
+    CONF = config.TempestConfig()
+
+    @testtools.skipIf(CONF.network.quantum_available, "This feature is not" +
+                      "implemented by Quantum. See bug: #1194569")
     @attr(type='gate')
     def test_list_fixed_ip_details(self):
         resp, fixed_ip = self.client.get_fixed_ip_details(self.ip)
diff --git a/tempest/api/compute/admin/test_servers.py b/tempest/api/compute/admin/test_servers.py
new file mode 100644
index 0000000..cb47066
--- /dev/null
+++ b/tempest/api/compute/admin/test_servers.py
@@ -0,0 +1,64 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 IBM Corp.
+#
+#    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.compute import base
+from tempest.common.utils.data_utils import rand_name
+from tempest.test import attr
+
+
+class ServersAdminTestJSON(base.BaseComputeAdminTest):
+
+    """
+    Tests Servers API using admin privileges
+    """
+
+    _interface = 'json'
+
+    @classmethod
+    def setUpClass(cls):
+        super(ServersAdminTestJSON, cls).setUpClass()
+        cls.client = cls.os_adm.servers_client
+
+        cls.s1_name = rand_name('server')
+        resp, server = cls.create_server(name=cls.s1_name,
+                                         wait_until='ACTIVE')
+        cls.s2_name = rand_name('server')
+        resp, server = cls.create_server(name=cls.s2_name,
+                                         wait_until='ACTIVE')
+
+    @attr(type='gate')
+    def test_list_servers_by_admin(self):
+        # Listing servers by admin user returns empty list by default
+        resp, body = self.client.list_servers_with_detail()
+        servers = body['servers']
+        self.assertEqual('200', resp['status'])
+        self.assertEqual([], servers)
+
+    @attr(type='gate')
+    def test_list_servers_by_admin_with_all_tenants(self):
+        # Listing servers by admin user with all tenants parameter
+        # Here should be listed all servers
+        params = {'all_tenants': ''}
+        resp, body = self.client.list_servers_with_detail(params)
+        servers = body['servers']
+        servers_name = map(lambda x: x['name'], servers)
+
+        self.assertIn(self.s1_name, servers_name)
+        self.assertIn(self.s2_name, servers_name)
+
+
+class ServersAdminTestXML(ServersAdminTestJSON):
+    _interface = 'xml'
diff --git a/tempest/api/compute/security_groups/test_security_groups.py b/tempest/api/compute/security_groups/test_security_groups.py
index f960ca4..12c646d 100644
--- a/tempest/api/compute/security_groups/test_security_groups.py
+++ b/tempest/api/compute/security_groups/test_security_groups.py
@@ -15,8 +15,11 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import testtools
+
 from tempest.api.compute import base
 from tempest.common.utils.data_utils import rand_name
+from tempest import config
 from tempest import exceptions
 from tempest.test import attr
 
@@ -155,6 +158,8 @@
                           self.client.create_security_group, s_name,
                           s_description)
 
+    @testtools.skipIf(config.TempestConfig().network.quantum_available,
+                      "Quantum allows duplicate names for security groups")
     @attr(type=['negative', 'gate'])
     def test_security_group_create_with_duplicate_name(self):
         # Negative test:Security Group with duplicate name should not
diff --git a/tempest/api/compute/servers/test_servers_negative.py b/tempest/api/compute/servers/test_servers_negative.py
index bbe489c..5f53080 100644
--- a/tempest/api/compute/servers/test_servers_negative.py
+++ b/tempest/api/compute/servers/test_servers_negative.py
@@ -236,11 +236,7 @@
         # Create a server with a nonexistent security group
 
         security_groups = [{'name': 'does_not_exist'}]
-        if self.config.network.quantum_available:
-            expected_exception = exceptions.NotFound
-        else:
-            expected_exception = exceptions.BadRequest
-        self.assertRaises(expected_exception,
+        self.assertRaises(exceptions.BadRequest,
                           self.create_server,
                           security_groups=security_groups)
 
diff --git a/tempest/api/compute/servers/test_virtual_interfaces.py b/tempest/api/compute/servers/test_virtual_interfaces.py
index 3119643..9073aeb 100644
--- a/tempest/api/compute/servers/test_virtual_interfaces.py
+++ b/tempest/api/compute/servers/test_virtual_interfaces.py
@@ -16,9 +16,11 @@
 #    under the License.
 
 import netaddr
+import testtools
 
 from tempest.api.compute import base
 from tempest.common.utils.data_utils import rand_name
+from tempest import config
 from tempest import exceptions
 from tempest.test import attr
 
@@ -26,6 +28,8 @@
 class VirtualInterfacesTestJSON(base.BaseComputeTest):
     _interface = 'json'
 
+    CONF = config.TempestConfig()
+
     @classmethod
     def setUpClass(cls):
         super(VirtualInterfacesTestJSON, cls).setUpClass()
@@ -33,6 +37,8 @@
         resp, server = cls.create_server(wait_until='ACTIVE')
         cls.server_id = server['id']
 
+    @testtools.skipIf(CONF.network.quantum_available, "This feature is not " +
+                      "implemented by Quantum. See bug: #1183436")
     @attr(type='gate')
     def test_list_virtual_interfaces(self):
         # Positive test:Should be able to GET the virtual interfaces list
diff --git a/tempest/api/orchestration/base.py b/tempest/api/orchestration/base.py
index 544558e..fa8190a 100644
--- a/tempest/api/orchestration/base.py
+++ b/tempest/api/orchestration/base.py
@@ -36,8 +36,10 @@
 
         cls.os = os
         cls.orchestration_client = os.orchestration_client
+        cls.servers_client = os.servers_client
         cls.keypairs_client = os.keypairs_client
         cls.stacks = []
+        cls.keypairs = []
 
     @classmethod
     def _get_identity_admin_client(cls):
@@ -88,11 +90,21 @@
         kp_name = rand_name(namestart)
         resp, body = self.keypairs_client.create_keypair(kp_name)
         self.assertEqual(body['name'], kp_name)
+        self.keypairs.append(kp_name)
         return body
 
     @classmethod
+    def clear_keypairs(cls):
+        for kp_name in cls.keypairs:
+            try:
+                cls.keypairs_client.delete_keypair(kp_name)
+            except Exception:
+                pass
+
+    @classmethod
     def tearDownClass(cls):
         cls.clear_stacks()
+        cls.clear_keypairs()
 
     def wait_for(self, condition):
         """Repeatedly calls condition() until a timeout."""
@@ -108,3 +120,9 @@
                 condition()
                 return
             time.sleep(self.build_interval)
+
+    @staticmethod
+    def stack_output(stack, output_key):
+        """Return a stack output value for a give key."""
+        return next((o['output_value'] for o in stack['outputs']
+                    if o['output_key'] == output_key), None)
diff --git a/tempest/api/orchestration/stacks/test_instance_cfn_init.py b/tempest/api/orchestration/stacks/test_instance_cfn_init.py
index 2349830..e3b8162 100644
--- a/tempest/api/orchestration/stacks/test_instance_cfn_init.py
+++ b/tempest/api/orchestration/stacks/test_instance_cfn_init.py
@@ -14,9 +14,12 @@
 
 import json
 import logging
+import testtools
 
 from tempest.api.orchestration import base
 from tempest.common.utils.data_utils import rand_name
+from tempest.common.utils.linux.remote_client import RemoteClient
+import tempest.config
 from tempest.test import attr
 
 
@@ -25,6 +28,8 @@
 
 class InstanceCfnInitTestJSON(base.BaseOrchestrationTest):
     _interface = 'json'
+    existing_keypair = (tempest.config.TempestConfig().
+                        orchestration.keypair_name is not None)
 
     template = """
 HeatTemplateFormatVersion: '2012-12-12'
@@ -101,6 +106,10 @@
     Description: Contents of /tmp/smoke-status on SmokeServer
     Value:
       Fn::GetAtt: [WaitCondition, Data]
+  SmokeServerIp:
+    Description: IP address of server
+    Value:
+      Fn::GetAtt: [SmokeServer, PublicIp]
 """
 
     @classmethod
@@ -113,8 +122,11 @@
     def setUp(self):
         super(InstanceCfnInitTestJSON, self).setUp()
         stack_name = rand_name('heat')
-        keypair_name = (self.orchestration_cfg.keypair_name or
-                        self._create_keypair()['name'])
+        if self.orchestration_cfg.keypair_name:
+            keypair_name = self.orchestration_cfg.keypair_name
+        else:
+            self.keypair = self._create_keypair()
+            keypair_name = self.keypair['name']
 
         # create the stack
         self.stack_identifier = self.create_stack(
@@ -127,6 +139,29 @@
             })
 
     @attr(type='gate')
+    @testtools.skipIf(existing_keypair, 'Server ssh tests are disabled.')
+    def test_can_log_into_created_server(self):
+
+        sid = self.stack_identifier
+        rid = 'SmokeServer'
+
+        # wait for server resource create to complete.
+        self.client.wait_for_resource_status(sid, rid, 'CREATE_COMPLETE')
+
+        resp, body = self.client.get_resource(sid, rid)
+        self.assertEqual('CREATE_COMPLETE', body['resource_status'])
+
+        # fetch the ip address from servers client, since we can't get it
+        # from the stack until stack create is complete
+        resp, server = self.servers_client.get_server(
+            body['physical_resource_id'])
+
+        # Check that the user can authenticate with the generated password
+        linux_client = RemoteClient(
+            server, 'ec2-user', pkey=self.keypair['private_key'])
+        self.assertTrue(linux_client.can_authenticate())
+
+    @attr(type='gate')
     def test_stack_wait_condition_data(self):
 
         sid = self.stack_identifier
@@ -148,5 +183,6 @@
         # - a user was created and credentials written to the instance
         # - a cfn-signal was built which was signed with provided credentials
         # - the wait condition was fulfilled and the stack has changed state
-        wait_status = json.loads(body['outputs'][0]['output_value'])
+        wait_status = json.loads(
+            self.stack_output(body, 'WaitConditionStatus'))
         self.assertEqual('smoke test complete', wait_status['00000'])
diff --git a/tempest/api/volume/admin/test_volume_types.py b/tempest/api/volume/admin/test_volume_types.py
index 4131d3e..3c4b5d8 100644
--- a/tempest/api/volume/admin/test_volume_types.py
+++ b/tempest/api/volume/admin/test_volume_types.py
@@ -55,8 +55,10 @@
             volume = {}
             vol_name = rand_name("volume-")
             vol_type_name = rand_name("volume-type-")
-            extra_specs = {"storage_protocol": "iSCSI",
-                           "vendor_name": "Open Source"}
+            proto = self.config.volume.storage_protocol
+            vendor = self.config.volume.vendor_name
+            extra_specs = {"storage_protocol": proto,
+                           "vendor_name": vendor}
             body = {}
             resp, body = self.client.create_volume_type(
                 vol_type_name,
diff --git a/tempest/cli/README.rst b/tempest/cli/README.rst
index 76b05a3..3eae492 100644
--- a/tempest/cli/README.rst
+++ b/tempest/cli/README.rst
@@ -36,7 +36,7 @@
 If a test is validating the cli for bad data, it should do it with
 assertRaises.
 
-A reasonable example of an existing test is as follows:
+A reasonable example of an existing test is as follows::
 
     def test_admin_list(self):
         self.nova('list')
diff --git a/tempest/cli/__init__.py b/tempest/cli/__init__.py
index 413990d..5bbedfd 100644
--- a/tempest/cli/__init__.py
+++ b/tempest/cli/__init__.py
@@ -16,6 +16,7 @@
 #    under the License.
 
 import logging
+import os
 import shlex
 import subprocess
 
@@ -99,7 +100,7 @@
     def cmd(self, cmd, action, flags='', params='', fail_ok=False,
             merge_stderr=False):
         """Executes specified command for the given action."""
-        cmd = ' '.join([CONF.cli.cli_dir + cmd,
+        cmd = ' '.join([os.path.join(CONF.cli.cli_dir, cmd),
                         flags, action, params])
         LOG.info("running: '%s'" % cmd)
         cmd = shlex.split(cmd)
@@ -109,7 +110,7 @@
             else:
                 with open('/dev/null', 'w') as devnull:
                     result = self.check_output(cmd, stderr=devnull)
-        except subprocess.CalledProcessError, e:
+        except subprocess.CalledProcessError as e:
             LOG.error("command output:\n%s" % e.output)
             raise
         return result
diff --git a/tempest/common/glance_http.py b/tempest/common/glance_http.py
index d19d216..cd33a22 100644
--- a/tempest/common/glance_http.py
+++ b/tempest/common/glance_http.py
@@ -304,14 +304,14 @@
         if self.cert_file:
             try:
                 self.context.use_certificate_file(self.cert_file)
-            except Exception, e:
+            except Exception as e:
                 msg = 'Unable to load cert from "%s" %s' % (self.cert_file, e)
                 raise exc.SSLConfigurationError(msg)
             if self.key_file is None:
                 # We support having key and cert in same file
                 try:
                     self.context.use_privatekey_file(self.cert_file)
-                except Exception, e:
+                except Exception as e:
                     msg = ('No key file specified and unable to load key '
                            'from "%s" %s' % (self.cert_file, e))
                     raise exc.SSLConfigurationError(msg)
@@ -319,14 +319,14 @@
         if self.key_file:
             try:
                 self.context.use_privatekey_file(self.key_file)
-            except Exception, e:
+            except Exception as e:
                 msg = 'Unable to load key from "%s" %s' % (self.key_file, e)
                 raise exc.SSLConfigurationError(msg)
 
         if self.cacert:
             try:
                 self.context.load_verify_locations(self.cacert)
-            except Exception, e:
+            except Exception as e:
                 msg = 'Unable to load CA from "%s"' % (self.cacert, e)
                 raise exc.SSLConfigurationError(msg)
         else:
diff --git a/tempest/common/log.py b/tempest/common/log.py
index 9b35723..2159bfe 100644
--- a/tempest/common/log.py
+++ b/tempest/common/log.py
@@ -55,7 +55,7 @@
     log_config = os.path.join(conf_dir, conf_file)
     try:
         logging.config.fileConfig(log_config)
-    except ConfigParser.Error, exc:
+    except ConfigParser.Error as exc:
         raise cfg.ConfigFileParseError(log_config, str(exc))
     return True
 
diff --git a/tempest/common/rest_client.py b/tempest/common/rest_client.py
index 531dfc8..e94455d 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -144,8 +144,8 @@
             try:
                 auth_data = json.loads(resp_body)['access']
                 token = auth_data['token']['id']
-            except Exception, e:
-                print "Failed to obtain token for user: %s" % e
+            except Exception as e:
+                print("Failed to obtain token for user: %s" % e)
                 raise
 
             mgmt_url = None
diff --git a/tempest/config.py b/tempest/config.py
index 7196078..8795b33 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -330,6 +330,12 @@
     cfg.StrOpt('backend2_name',
                default='BACKEND_2',
                help="Name of the backend2 (must be declared in cinder.conf)"),
+    cfg.StrOpt('storage_protocol',
+               default='iSCSI',
+               help='Backend protocol to target when creating volume types'),
+    cfg.StrOpt('vendor_name',
+               default='Open Source',
+               help='Backend vendor to target when creating volume types'),
 ]
 
 
diff --git a/tempest/scenario/test_snapshot_pattern.py b/tempest/scenario/test_snapshot_pattern.py
new file mode 100644
index 0000000..7725421
--- /dev/null
+++ b/tempest/scenario/test_snapshot_pattern.py
@@ -0,0 +1,134 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 NEC Corporation
+# 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.
+
+import logging
+
+from tempest.common.utils.data_utils import rand_name
+from tempest.common.utils.linux.remote_client import RemoteClient
+from tempest.scenario import manager
+
+
+LOG = logging.getLogger(__name__)
+
+
+class TestSnapshotPattern(manager.OfficialClientTest):
+    """
+    This test is for snapshotting an instance and booting with it.
+    The following is the scenario outline:
+     * boot a instance and create a timestamp file in it
+     * snapshot the instance
+     * boot a second instance from the snapshot
+     * check the existence of the timestamp file in the second instance
+
+    """
+
+    def _wait_for_server_status(self, server, status):
+        self.status_timeout(self.compute_client.servers,
+                            server.id,
+                            status)
+
+    def _wait_for_image_status(self, image_id, status):
+        self.status_timeout(self.image_client.images, image_id, status)
+
+    def _boot_image(self, image_id):
+        name = rand_name('scenario-server-')
+        client = self.compute_client
+        flavor_id = self.config.compute.flavor_ref
+        LOG.debug("name:%s, image:%s" % (name, image_id))
+        server = client.servers.create(name=name,
+                                       image=image_id,
+                                       flavor=flavor_id,
+                                       key_name=self.keypair.name)
+        self.addCleanup(self.compute_client.servers.delete, server)
+        self.assertEqual(name, server.name)
+        self._wait_for_server_status(server, 'ACTIVE')
+        server = client.servers.get(server)  # getting network information
+        LOG.debug("server:%s" % server)
+        return server
+
+    def _add_keypair(self):
+        name = rand_name('scenario-keypair-')
+        self.keypair = self.compute_client.keypairs.create(name=name)
+        self.addCleanup(self.compute_client.keypairs.delete, self.keypair)
+        self.assertEqual(name, self.keypair.name)
+
+    def _create_security_group_rule(self):
+        sgs = self.compute_client.security_groups.list()
+        for sg in sgs:
+            if sg.name == 'default':
+                secgroup = sg
+
+        ruleset = {
+            # ssh
+            'ip_protocol': 'tcp',
+            'from_port': 22,
+            'to_port': 22,
+            'cidr': '0.0.0.0/0',
+            'group_id': None
+        }
+        sg_rule = self.compute_client.security_group_rules.create(secgroup.id,
+                                                                  **ruleset)
+        self.addCleanup(self.compute_client.security_group_rules.delete,
+                        sg_rule.id)
+
+    def _ssh_to_server(self, server):
+        username = self.config.scenario.ssh_user
+        ip = server.networks[self.config.compute.network_for_ssh][0]
+        linux_client = RemoteClient(ip,
+                                    username,
+                                    pkey=self.keypair.private_key)
+
+        return linux_client.ssh_client
+
+    def _write_timestamp(self, server):
+        ssh_client = self._ssh_to_server(server)
+        ssh_client.exec_command('date > /tmp/timestamp; sync')
+        self.timestamp = ssh_client.exec_command('cat /tmp/timestamp')
+
+    def _create_image(self, server):
+        snapshot_name = rand_name('scenario-snapshot-')
+        create_image_client = self.compute_client.servers.create_image
+        image_id = create_image_client(server, snapshot_name)
+        self.addCleanup(self.image_client.images.delete, image_id)
+        self._wait_for_server_status(server, 'ACTIVE')
+        self._wait_for_image_status(image_id, 'active')
+        snapshot_image = self.image_client.images.get(image_id)
+        self.assertEquals(snapshot_name, snapshot_image.name)
+        return image_id
+
+    def _check_timestamp(self, server):
+        ssh_client = self._ssh_to_server(server)
+        got_timestamp = ssh_client.exec_command('cat /tmp/timestamp')
+        self.assertEqual(self.timestamp, got_timestamp)
+
+    def test_snapshot_pattern(self):
+        # prepare for booting a instance
+        self._add_keypair()
+        self._create_security_group_rule()
+
+        # boot a instance and create a timestamp file in it
+        server = self._boot_image(self.config.compute.image_ref)
+        self._write_timestamp(server)
+
+        # snapshot the instance
+        snapshot_image_id = self._create_image(server)
+
+        # boot a second instance from the snapshot
+        server_from_snapshot = self._boot_image(snapshot_image_id)
+
+        # check the existence of the timestamp file in the second instance
+        self._check_timestamp(server_from_snapshot)
diff --git a/tempest/services/compute/json/servers_client.py b/tempest/services/compute/json/servers_client.py
index d4822da..6906610 100644
--- a/tempest/services/compute/json/servers_client.py
+++ b/tempest/services/compute/json/servers_client.py
@@ -332,15 +332,6 @@
                                req_body, self.headers)
         return resp, body
 
-    def list_servers_for_all_tenants(self):
-
-        url = self.base_url + '/servers?all_tenants=1'
-        resp = self.requests.get(url)
-        resp, body = self.get('servers', self.headers)
-
-        body = json.loads(body)
-        return resp, body['servers']
-
     def migrate_server(self, server_id, **kwargs):
         """Migrates a server to a new host."""
         return self.action(server_id, 'migrate', None, **kwargs)
diff --git a/tempest/services/image/v1/json/image_client.py b/tempest/services/image/v1/json/image_client.py
index f0b1c28..dac77a2 100644
--- a/tempest/services/image/v1/json/image_client.py
+++ b/tempest/services/image/v1/json/image_client.py
@@ -88,7 +88,7 @@
                 obj_size = obj.tell()
                 obj.seek(0)
                 return obj_size
-            except IOError, e:
+            except IOError as e:
                 if e.errno == errno.ESPIPE:
                     # Illegal seek. This means the user is trying
                     # to pipe image data to the client, e.g.
diff --git a/tempest/services/orchestration/json/orchestration_client.py b/tempest/services/orchestration/json/orchestration_client.py
index 81162df..6b0e7e3 100644
--- a/tempest/services/orchestration/json/orchestration_client.py
+++ b/tempest/services/orchestration/json/orchestration_client.py
@@ -16,6 +16,7 @@
 #    under the License.
 
 import json
+import re
 import time
 import urllib
 
@@ -68,24 +69,69 @@
         body = json.loads(body)
         return resp, body['stack']
 
+    def list_resources(self, stack_identifier):
+        """Returns the details of a single resource."""
+        url = "stacks/%s/resources" % stack_identifier
+        resp, body = self.get(url)
+        body = json.loads(body)
+        return resp, body['resources']
+
+    def get_resource(self, stack_identifier, resource_name):
+        """Returns the details of a single resource."""
+        url = "stacks/%s/resources/%s" % (stack_identifier, resource_name)
+        resp, body = self.get(url)
+        body = json.loads(body)
+        return resp, body['resource']
+
     def delete_stack(self, stack_identifier):
         """Deletes the specified Stack."""
         return self.delete("stacks/%s" % str(stack_identifier))
 
-    def wait_for_stack_status(self, stack_identifier, status, failure_status=(
-            'CREATE_FAILED',
-            'DELETE_FAILED',
-            'UPDATE_FAILED',
-            'ROLLBACK_FAILED')):
-        """Waits for a Volume to reach a given status."""
-        stack_status = None
+    def wait_for_resource_status(self, stack_identifier, resource_name,
+                                 status, failure_pattern='^.*_FAILED$'):
+        """Waits for a Resource to reach a given status."""
         start = int(time.time())
+        fail_regexp = re.compile(failure_pattern)
 
-        while stack_status != status:
+        while True:
+            try:
+                resp, body = self.get_resource(
+                    stack_identifier, resource_name)
+            except exceptions.NotFound:
+                # ignore this, as the resource may not have
+                # been created yet
+                pass
+            else:
+                resource_name = body['logical_resource_id']
+                resource_status = body['resource_status']
+                if resource_status == status:
+                    return
+                if fail_regexp.search(resource_status):
+                    raise exceptions.StackBuildErrorException(
+                        stack_identifier=stack_identifier,
+                        resource_status=resource_status,
+                        resource_status_reason=body['resource_status_reason'])
+
+            if int(time.time()) - start >= self.build_timeout:
+                message = ('Resource %s failed to reach %s status within '
+                           'the required time (%s s).' %
+                           (resource_name, status, self.build_timeout))
+                raise exceptions.TimeoutException(message)
+            time.sleep(self.build_interval)
+
+    def wait_for_stack_status(self, stack_identifier, status,
+                              failure_pattern='^.*_FAILED$'):
+        """Waits for a Stack to reach a given status."""
+        start = int(time.time())
+        fail_regexp = re.compile(failure_pattern)
+
+        while True:
             resp, body = self.get_stack(stack_identifier)
             stack_name = body['stack_name']
             stack_status = body['stack_status']
-            if stack_status in failure_status:
+            if stack_status == status:
+                return
+            if fail_regexp.search(stack_status):
                 raise exceptions.StackBuildErrorException(
                     stack_identifier=stack_identifier,
                     stack_status=stack_status,
diff --git a/tempest/whitebox/manager.py b/tempest/whitebox/manager.py
index aa58ab6..3bd057c 100644
--- a/tempest/whitebox/manager.py
+++ b/tempest/whitebox/manager.py
@@ -128,7 +128,7 @@
             meta = MetaData()
             meta.reflect(bind=engine)
 
-        except Exception, e:
+        except Exception as e:
             raise exceptions.SQLException(message=e)
 
         return connection, meta
diff --git a/tools/find_stack_traces.py b/tools/find_stack_traces.py
index 3129484..0ce1500 100755
--- a/tools/find_stack_traces.py
+++ b/tools/find_stack_traces.py
@@ -110,7 +110,7 @@
 
 
 def usage():
-    print """
+    print("""
 Usage: find_stack_traces.py <logurl>
 
 Hunts for stack traces in a devstack run. Must provide it a base log url
@@ -118,20 +118,20 @@
 
 Returns a report listing stack traces out of the various files where
 they are found.
-"""
+""")
     sys.exit(0)
 
 
 def print_stats(items, fname, verbose=False):
     errors = len(filter(lambda x: x.level == "ERROR", items))
     traces = len(filter(lambda x: x.level == "TRACE", items))
-    print "%d ERRORS found in %s" % (errors, fname)
-    print "%d TRACES found in %s" % (traces, fname)
+    print("%d ERRORS found in %s" % (errors, fname))
+    print("%d TRACES found in %s" % (traces, fname))
 
     if verbose:
         for item in items:
-            print item
-        print "\n\n"
+            print(item)
+        print("\n\n")
 
 
 def main():
diff --git a/tools/skip_tracker.py b/tools/skip_tracker.py
index c7b0033..1ed6961 100755
--- a/tools/skip_tracker.py
+++ b/tools/skip_tracker.py
@@ -118,8 +118,8 @@
 
     unskips = sorted(set(unskips))
     if unskips:
-        print "The following bugs have been fixed and the corresponding skips"
-        print "should be removed from the test cases:"
-        print
+        print("The following bugs have been fixed and the corresponding skips")
+        print("should be removed from the test cases:")
+        print()
         for bug in unskips:
-            print "  %7s" % bug
+            print("  %7s" % bug)
diff --git a/tools/tempest_coverage.py b/tools/tempest_coverage.py
index 5b926f9..ef2eacd 100755
--- a/tools/tempest_coverage.py
+++ b/tools/tempest_coverage.py
@@ -12,7 +12,7 @@
 #    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
+#    under the License.
 
 import json
 import os
@@ -151,14 +151,14 @@
     elif CLI.command == 'stop':
         resp, body = coverage_client.stop_coverage()
         if not resp['status'] == '200':
-            print 'coverage stop failed with: %s:' % (resp['status'] + ': '
-                                                      + body)
+            print('coverage stop failed with: %s:' % (resp['status'] + ': '
+                                                      + body))
             exit(int(resp['status']))
         path = body['path']
         if CLI.output:
             shutil.copytree(path, CLI.output)
         else:
-            print "Data files located at: %s" % path
+            print("Data files located at: %s" % path)
 
     elif CLI.command == 'report':
         if CLI.xml:
@@ -169,8 +169,8 @@
         else:
             resp, body = coverage_client.report_coverage(file=CLI.filename)
         if not resp['status'] == '200':
-            print 'coverage report failed with: %s:' % (resp['status'] + ': '
-                                                        + body)
+            print('coverage report failed with: %s:' % (resp['status'] + ': '
+                                                        + body))
             exit(int(resp['status']))
         path = body['path']
         if CLI.output:
@@ -182,10 +182,10 @@
         else:
             if not CLI.html:
                 path = os.path.dirname(path)
-            print 'Report files located at: %s' % path
+            print('Report files located at: %s' % path)
 
     else:
-        print 'Invalid command'
+        print('Invalid command')
         exit(1)