Merge "Correct mispell words in comments"
diff --git a/tempest/api/compute/admin/test_servers_on_multinodes.py b/tempest/api/compute/admin/test_servers_on_multinodes.py
index 814a876..1bbde98 100644
--- a/tempest/api/compute/admin/test_servers_on_multinodes.py
+++ b/tempest/api/compute/admin/test_servers_on_multinodes.py
@@ -12,6 +12,8 @@
 #    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 test
@@ -34,6 +36,9 @@
             server_id)['server']['OS-EXT-SRV-ATTR:host']
 
     @test.idempotent_id('26a9d5df-6890-45f2-abc4-a659290cb130')
+    @testtools.skipUnless(
+        test.is_scheduler_filter_enabled("SameHostFilter"),
+        'SameHostFilter is not available.')
     def test_create_servers_on_same_host(self):
         server01 = self.create_test_server(wait_until='ACTIVE')['id']
 
@@ -45,6 +50,9 @@
         self.assertEqual(host01, host02)
 
     @test.idempotent_id('cc7ca884-6e3e-42a3-a92f-c522fcf25e8e')
+    @testtools.skipUnless(
+        test.is_scheduler_filter_enabled("DifferentHostFilter"),
+        'DifferentHostFilter is not available.')
     def test_create_servers_on_different_hosts(self):
         server01 = self.create_test_server(wait_until='ACTIVE')['id']
 
@@ -56,6 +64,9 @@
         self.assertNotEqual(host01, host02)
 
     @test.idempotent_id('7869cc84-d661-4e14-9f00-c18cdc89cf57')
+    @testtools.skipUnless(
+        test.is_scheduler_filter_enabled("DifferentHostFilter"),
+        'DifferentHostFilter is not available.')
     def test_create_servers_on_different_hosts_with_list_of_servers(self):
         server01 = self.create_test_server(wait_until='ACTIVE')['id']
 
diff --git a/tempest/api/compute/servers/test_create_server.py b/tempest/api/compute/servers/test_create_server.py
index 87f3c86..c05045e 100644
--- a/tempest/api/compute/servers/test_create_server.py
+++ b/tempest/api/compute/servers/test_create_server.py
@@ -119,7 +119,9 @@
             self.get_server_ip(self.server),
             self.ssh_user,
             self.password,
-            self.validation_resources['keypair']['private_key'])
+            self.validation_resources['keypair']['private_key'],
+            server=self.server,
+            servers_client=self.client)
         self.assertEqual(flavor['vcpus'], linux_client.get_number_of_vcpus())
 
     @test.idempotent_id('ac1ad47f-984b-4441-9274-c9079b7a0666')
@@ -131,7 +133,9 @@
             self.get_server_ip(self.server),
             self.ssh_user,
             self.password,
-            self.validation_resources['keypair']['private_key'])
+            self.validation_resources['keypair']['private_key'],
+            server=self.server,
+            servers_client=self.client)
         self.assertTrue(linux_client.hostname_equals_servername(self.name))
 
     @test.idempotent_id('ed20d3fb-9d1f-4329-b160-543fbd5d9811')
@@ -322,7 +326,9 @@
             self.get_server_ip(server_no_eph_disk),
             self.ssh_user,
             admin_pass,
-            self.validation_resources['keypair']['private_key'])
+            self.validation_resources['keypair']['private_key'],
+            server=server_no_eph_disk,
+            servers_client=self.client)
         partition_num = len(linux_client.get_partitions().split('\n'))
 
         # Explicit server deletion necessary for Juno compatibility
@@ -340,7 +346,9 @@
             self.get_server_ip(server_with_eph_disk),
             self.ssh_user,
             admin_pass,
-            self.validation_resources['keypair']['private_key'])
+            self.validation_resources['keypair']['private_key'],
+            server=server_with_eph_disk,
+            servers_client=self.client)
         partition_num_emph = len(linux_client.get_partitions().split('\n'))
         self.assertEqual(partition_num + 1, partition_num_emph)
 
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index f3aa16a..f01657b 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -91,7 +91,9 @@
             linux_client = remote_client.RemoteClient(
                 self.get_server_ip(server),
                 self.ssh_user,
-                new_password)
+                new_password,
+                server=server,
+                servers_client=self.client)
             linux_client.validate_authentication()
 
     def _test_reboot_server(self, reboot_type):
@@ -102,7 +104,9 @@
                 self.get_server_ip(server),
                 self.ssh_user,
                 self.password,
-                self.validation_resources['keypair']['private_key'])
+                self.validation_resources['keypair']['private_key'],
+                server=server,
+                servers_client=self.client)
             boot_time = linux_client.get_boot_time()
 
         self.client.reboot_server(self.server_id, type=reboot_type)
@@ -114,7 +118,9 @@
                 self.get_server_ip(server),
                 self.ssh_user,
                 self.password,
-                self.validation_resources['keypair']['private_key'])
+                self.validation_resources['keypair']['private_key'],
+                server=server,
+                servers_client=self.client)
             new_boot_time = linux_client.get_boot_time()
             self.assertTrue(new_boot_time > boot_time,
                             '%s > %s' % (new_boot_time, boot_time))
@@ -183,7 +189,9 @@
                 self.get_server_ip(rebuilt_server),
                 self.ssh_user,
                 password,
-                self.validation_resources['keypair']['private_key'])
+                self.validation_resources['keypair']['private_key'],
+                server=rebuilt_server,
+                servers_client=self.client)
             linux_client.validate_authentication()
 
     @test.idempotent_id('30449a88-5aff-4f9b-9866-6ee9b17f906d')
diff --git a/tempest/api/compute/servers/test_server_personality.py b/tempest/api/compute/servers/test_server_personality.py
index 74d34a2..baa4f9a 100644
--- a/tempest/api/compute/servers/test_server_personality.py
+++ b/tempest/api/compute/servers/test_server_personality.py
@@ -66,7 +66,9 @@
             linux_client = remote_client.RemoteClient(
                 self.get_server_ip(server),
                 self.ssh_user, password,
-                self.validation_resources['keypair']['private_key'])
+                self.validation_resources['keypair']['private_key'],
+                server=server,
+                servers_client=self.client)
             self.assertEqual(file_contents,
                              linux_client.exec_command(
                                  'sudo cat %s' % file_path))
@@ -130,7 +132,9 @@
             linux_client = remote_client.RemoteClient(
                 self.get_server_ip(server),
                 self.ssh_user, password,
-                self.validation_resources['keypair']['private_key'])
+                self.validation_resources['keypair']['private_key'],
+                server=server,
+                servers_client=self.client)
             for i in person:
                 self.assertEqual(base64.b64decode(i['contents']),
                                  linux_client.exec_command(
diff --git a/tempest/api/compute/volumes/test_attach_volume.py b/tempest/api/compute/volumes/test_attach_volume.py
index 37423a3..fa3fdfe 100644
--- a/tempest/api/compute/volumes/test_attach_volume.py
+++ b/tempest/api/compute/volumes/test_attach_volume.py
@@ -84,6 +84,19 @@
                                        self.volume['id'], 'available')
 
         if shelve_server:
+            # NOTE(andreaf) If we are going to shelve a server, we should
+            # check first whether the server is ssh-able. Otherwise we won't
+            # be able to distinguish failures introduced by shelve from
+            # pre-existing ones. Also it's good to wait for cloud-init to be
+            # done and sshd server to be running before shelving to avoid
+            # breaking the VM
+            linux_client = remote_client.RemoteClient(
+                self.get_server_ip(self.server),
+                self.image_ssh_user,
+                self.admin_pass,
+                self.validation_resources['keypair']['private_key'])
+            linux_client.validate_authentication()
+            # If validation went ok, shelve the server
             compute.shelve_server(self.servers_client, self.server['id'])
 
         # Attach the volume to the server
@@ -116,7 +129,9 @@
             self.get_server_ip(self.server),
             self.image_ssh_user,
             self.admin_pass,
-            self.validation_resources['keypair']['private_key'])
+            self.validation_resources['keypair']['private_key'],
+            server=self.server,
+            servers_client=self.servers_client)
 
         partitions = linux_client.get_partitions()
         self.assertIn(self.device, partitions)
@@ -135,7 +150,9 @@
             self.get_server_ip(self.server),
             self.image_ssh_user,
             self.admin_pass,
-            self.validation_resources['keypair']['private_key'])
+            self.validation_resources['keypair']['private_key'],
+            server=self.server,
+            servers_client=self.servers_client)
 
         partitions = linux_client.get_partitions()
         self.assertNotIn(self.device, partitions)
@@ -179,7 +196,9 @@
             self.get_server_ip(self.server['id']),
             self.image_ssh_user,
             self.admin_pass,
-            self.validation_resources['keypair']['private_key'])
+            self.validation_resources['keypair']['private_key'],
+            server=self.server,
+            servers_client=self.servers_client)
 
         command = 'grep vd /proc/partitions | wc -l'
         nb_partitions = linux_client.exec_command(command).strip()
diff --git a/tempest/cmd/init.py b/tempest/cmd/init.py
index 9a3a4ed..633b9e9 100644
--- a/tempest/cmd/init.py
+++ b/tempest/cmd/init.py
@@ -85,6 +85,10 @@
         parser = super(TempestInit, self).get_parser(prog_name)
         parser.add_argument('dir', nargs='?', default=os.getcwd())
         parser.add_argument('--config-dir', '-c', default=None)
+        parser.add_argument('--show-global-config-dir', '-s',
+                            action='store_true', dest='show_global_dir',
+                            help="Print the global config dir location, "
+                                 "then exit")
         return parser
 
     def generate_testr_conf(self, local_path):
@@ -156,4 +160,7 @@
 
     def take_action(self, parsed_args):
         config_dir = parsed_args.config_dir or get_tempest_default_config_dir()
+        if parsed_args.show_global_dir:
+            print("Global config dir is located at: %s" % config_dir)
+            sys.exit(0)
         self.create_working_dir(parsed_args.dir, config_dir)
diff --git a/tempest/common/utils/linux/remote_client.py b/tempest/common/utils/linux/remote_client.py
index 3a215a0..3f573b7 100644
--- a/tempest/common/utils/linux/remote_client.py
+++ b/tempest/common/utils/linux/remote_client.py
@@ -12,6 +12,8 @@
 
 import netaddr
 import re
+import six
+import sys
 import time
 
 from oslo_log import log as logging
@@ -19,6 +21,7 @@
 from tempest import config
 from tempest import exceptions
 from tempest.lib.common import ssh
+from tempest.lib.common.utils import misc as misc_utils
 import tempest.lib.exceptions
 
 CONF = config.CONF
@@ -26,16 +29,61 @@
 LOG = logging.getLogger(__name__)
 
 
+def debug_ssh(function):
+    """Decorator to generate extra debug info in case off SSH failure"""
+    def wrapper(self, *args, **kwargs):
+        try:
+            return function(self, *args, **kwargs)
+        except tempest.lib.exceptions.SSHTimeout:
+            try:
+                original_exception = sys.exc_info()
+                caller = misc_utils.find_test_caller() or "not found"
+                if self.server:
+                    msg = 'Caller: %s. Timeout trying to ssh to server %s'
+                    LOG.debug(msg, caller, self.server)
+                    if self.log_console and self.servers_client:
+                        try:
+                            msg = 'Console log for server %s: %s'
+                            console_log = (
+                                self.servers_client.get_console_output(
+                                    self.server['id'])['output'])
+                            LOG.debug(msg, self.server['id'], console_log)
+                        except Exception:
+                            msg = 'Could not get console_log for server %s'
+                            LOG.debug(msg, self.server['id'])
+                # re-raise the original ssh timeout exception
+                six.reraise(*original_exception)
+            finally:
+                # Delete the traceback to avoid circular references
+                _, _, trace = original_exception
+                del trace
+    return wrapper
+
+
 class RemoteClient(object):
 
-    def __init__(self, ip_address, username, password=None, pkey=None):
+    def __init__(self, ip_address, username, password=None, pkey=None,
+                 server=None, servers_client=None):
+        """Executes commands in a VM over ssh
+
+        :param ip_address: IP address to ssh to
+        :param username: ssh username
+        :param password: ssh password (optional)
+        :param pkey: ssh public key (optional)
+        :param server: server dict, used for debugging purposes
+        :param servers_client: servers client, used for debugging purposes
+        """
+        self.server = server
+        self.servers_client = servers_client
         ssh_timeout = CONF.validation.ssh_timeout
         connect_timeout = CONF.validation.connect_timeout
+        self.log_console = CONF.compute_feature_enabled.console_output
 
         self.ssh_client = ssh.Client(ip_address, username, password,
                                      ssh_timeout, pkey=pkey,
                                      channel_timeout=connect_timeout)
 
+    @debug_ssh
     def exec_command(self, cmd):
         # Shell options below add more clearness on failures,
         # path is extended for some non-cirros guest oses (centos7)
@@ -43,6 +91,7 @@
         LOG.debug("Remote command: %s" % cmd)
         return self.ssh_client.exec_command(cmd)
 
+    @debug_ssh
     def validate_authentication(self):
         """Validate ssh connection and authentication
 
diff --git a/tempest/config.py b/tempest/config.py
index a09080d..db94e6c 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -15,6 +15,7 @@
 
 from __future__ import print_function
 
+import functools
 import logging as std_logging
 import os
 import tempfile
@@ -22,6 +23,7 @@
 from oslo_concurrency import lockutils
 from oslo_config import cfg
 from oslo_log import log as logging
+import testtools
 
 from tempest.test_discover import plugins
 
@@ -1385,3 +1387,72 @@
 
 
 CONF = TempestConfigProxy()
+
+
+def skip_unless_config(*args):
+    """Decorator to raise a skip if a config opt doesn't exist and is False
+
+    :param str group: The first arg, the option group to check
+    :param str name: The second arg, the option name to check
+    :param str msg: Optional third arg, the skip msg to use if a skip is raised
+    :raises testtools.TestCaseskipException: If the specified config option
+        doesn't exist or it exists and evaluates to False
+    """
+    def decorator(f):
+        group = args[0]
+        name = args[1]
+
+        @functools.wraps(f)
+        def wrapper(self, *func_args, **func_kwargs):
+            if not hasattr(CONF, group):
+                msg = "Config group %s doesn't exist" % group
+                raise testtools.TestCase.skipException(msg)
+            else:
+                conf_group = getattr(CONF, group)
+                if not hasattr(conf_group, name):
+                    msg = "Config option %s.%s doesn't exist" % (group,
+                                                                 name)
+                    raise testtools.TestCase.skipException(msg)
+                else:
+                    value = getattr(conf_group, name)
+                    if not value:
+                        if len(args) == 3:
+                            msg = args[2]
+                        else:
+                            msg = "Config option %s.%s is false" % (group,
+                                                                    name)
+                        raise testtools.TestCase.skipException(msg)
+            return f(self, *func_args, **func_kwargs)
+        return wrapper
+    return decorator
+
+
+def skip_if_config(*args):
+    """Raise a skipException if a config exists and is True
+
+    :param str group: The first arg, the option group to check
+    :param str name: The second arg, the option name to check
+    :param str msg: Optional third arg, the skip msg to use if a skip is raised
+    :raises testtools.TestCase.skipException: If the specified config option
+        exists and evaluates to True
+    """
+    def decorator(f):
+        group = args[0]
+        name = args[1]
+
+        @functools.wraps(f)
+        def wrapper(self, *func_args, **func_kwargs):
+            if hasattr(CONF, group):
+                conf_group = getattr(CONF, group)
+                if hasattr(conf_group, name):
+                    value = getattr(conf_group, name)
+                    if value:
+                        if len(args) == 3:
+                            msg = args[2]
+                        else:
+                            msg = "Config option %s.%s is false" % (group,
+                                                                    name)
+                        raise testtools.TestCase.skipException(msg)
+            return f(self, *func_args, **func_kwargs)
+        return wrapper
+    return decorator
diff --git a/tempest/tests/common/utils/linux/test_remote_client.py b/tempest/tests/common/utils/linux/test_remote_client.py
index e9146bc..7d625cf 100644
--- a/tempest/tests/common/utils/linux/test_remote_client.py
+++ b/tempest/tests/common/utils/linux/test_remote_client.py
@@ -12,6 +12,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import fixtures
 import time
 
 from oslo_config import cfg
@@ -19,10 +20,39 @@
 
 from tempest.common.utils.linux import remote_client
 from tempest import config
+from tempest.lib import exceptions as lib_exc
 from tempest.tests import base
 from tempest.tests import fake_config
 
 
+SERVER = {
+    'id': 'server_uuid',
+    'name': 'fake_server',
+    'status': 'ACTIVE'
+}
+
+BROKEN_SERVER = {
+    'id': 'broken_server_uuid',
+    'name': 'broken_server',
+    'status': 'ERROR'
+}
+
+
+class FakeServersClient(object):
+
+    CONSOLE_OUTPUT = "Console output for %s"
+
+    def get_console_output(self, server_id):
+        status = 'ERROR'
+        for s in SERVER, BROKEN_SERVER:
+            if s['id'] == server_id:
+                status = s['status']
+        if status == 'ERROR':
+            raise lib_exc.BadRequest('Server in ERROR state')
+        else:
+            return dict(output=self.CONSOLE_OUTPUT % server_id)
+
+
 class TestRemoteClient(base.TestCase):
     def setUp(self):
         super(TestRemoteClient, self).setUp()
@@ -155,3 +185,78 @@
         self.conn.set_nic_state(nic, "down")
         self._assert_exec_called_with(
             'sudo ip link set %s down' % nic)
+
+
+class TestRemoteClientWithServer(base.TestCase):
+
+    server = SERVER
+
+    def setUp(self):
+        super(TestRemoteClientWithServer, self).setUp()
+        self.useFixture(fake_config.ConfigFixture())
+        self.patchobject(config, 'TempestConfigPrivate',
+                         fake_config.FakePrivate)
+        cfg.CONF.set_default('ip_version_for_ssh', 4, group='validation')
+        cfg.CONF.set_default('network_for_ssh', 'public',
+                             group='validation')
+        cfg.CONF.set_default('connect_timeout', 1, group='validation')
+        cfg.CONF.set_default('console_output', True,
+                             group='compute-feature-enabled')
+
+        self.conn = remote_client.RemoteClient(
+            '127.0.0.1', 'user', 'pass',
+            server=self.server, servers_client=FakeServersClient())
+        self.useFixture(fixtures.MockPatch(
+            'tempest.lib.common.ssh.Client._get_ssh_connection',
+            side_effect=lib_exc.SSHTimeout(host='127.0.0.1',
+                                           user='user',
+                                           password='pass')))
+        self.log = self.useFixture(fixtures.FakeLogger(
+            name='tempest.common.utils.linux.remote_client',
+            level='DEBUG'))
+
+    def test_validate_debug_ssh_console(self):
+        self.assertRaises(lib_exc.SSHTimeout,
+                          self.conn.validate_authentication)
+        msg = 'Caller: %s. Timeout trying to ssh to server %s' % (
+            'TestRemoteClientWithServer:test_validate_debug_ssh_console',
+            self.server)
+        self.assertIn(msg, self.log.output)
+        self.assertIn('Console output for', self.log.output)
+
+    def test_exec_command_debug_ssh_console(self):
+        self.assertRaises(lib_exc.SSHTimeout,
+                          self.conn.exec_command, 'fake command')
+        self.assertIn('fake command', self.log.output)
+        msg = 'Caller: %s. Timeout trying to ssh to server %s' % (
+            'TestRemoteClientWithServer:test_exec_command_debug_ssh_console',
+            self.server)
+        self.assertIn(msg, self.log.output)
+        self.assertIn('Console output for', self.log.output)
+
+
+class TestRemoteClientWithBrokenServer(TestRemoteClientWithServer):
+
+    server = BROKEN_SERVER
+
+    def test_validate_debug_ssh_console(self):
+        self.assertRaises(lib_exc.SSHTimeout,
+                          self.conn.validate_authentication)
+        msg = 'Caller: %s. Timeout trying to ssh to server %s' % (
+            'TestRemoteClientWithBrokenServer:test_validate_debug_ssh_console',
+            self.server)
+        self.assertIn(msg, self.log.output)
+        msg = 'Could not get console_log for server %s' % self.server['id']
+        self.assertIn(msg, self.log.output)
+
+    def test_exec_command_debug_ssh_console(self):
+        self.assertRaises(lib_exc.SSHTimeout,
+                          self.conn.exec_command, 'fake command')
+        self.assertIn('fake command', self.log.output)
+        caller = ":".join(['TestRemoteClientWithBrokenServer',
+                           'test_exec_command_debug_ssh_console'])
+        msg = 'Caller: %s. Timeout trying to ssh to server %s' % (
+            caller, self.server)
+        self.assertIn(msg, self.log.output)
+        msg = 'Could not get console_log for server %s' % self.server['id']
+        self.assertIn(msg, self.log.output)
diff --git a/tempest/tests/test_decorators.py b/tempest/tests/test_decorators.py
index 7c9579b..af5fc09 100644
--- a/tempest/tests/test_decorators.py
+++ b/tempest/tests/test_decorators.py
@@ -246,3 +246,75 @@
         self.assertIn("test_fake_negative", dir(obj))
         obj.test_fake_negative()
         mock.assert_called_once_with(self.FakeNegativeJSONTest._schema)
+
+
+class TestConfigDecorators(BaseDecoratorsTest):
+    def setUp(self):
+        super(TestConfigDecorators, self).setUp()
+        cfg.CONF.set_default('nova', True, 'service_available')
+        cfg.CONF.set_default('glance', False, 'service_available')
+
+    def _test_skip_unless_config(self, expected_to_skip=True, *decorator_args):
+
+        class TestFoo(test.BaseTestCase):
+            @config.skip_unless_config(*decorator_args)
+            def test_bar(self):
+                return 0
+
+        t = TestFoo('test_bar')
+        if expected_to_skip:
+            self.assertRaises(testtools.TestCase.skipException, t.test_bar)
+        else:
+            try:
+                self.assertEqual(t.test_bar(), 0)
+            except testtools.TestCase.skipException:
+                # We caught a skipException but we didn't expect to skip
+                # this test so raise a hard test failure instead.
+                raise testtools.TestCase.failureException(
+                    "Not supposed to skip")
+
+    def _test_skip_if_config(self, expected_to_skip=True,
+                             *decorator_args):
+
+        class TestFoo(test.BaseTestCase):
+            @config.skip_if_config(*decorator_args)
+            def test_bar(self):
+                return 0
+
+        t = TestFoo('test_bar')
+        if expected_to_skip:
+            self.assertRaises(testtools.TestCase.skipException, t.test_bar)
+        else:
+            try:
+                self.assertEqual(t.test_bar(), 0)
+            except testtools.TestCase.skipException:
+                # We caught a skipException but we didn't expect to skip
+                # this test so raise a hard test failure instead.
+                raise testtools.TestCase.failureException(
+                    "Not supposed to skip")
+
+    def test_skip_unless_no_group(self):
+        self._test_skip_unless_config(True, 'fake_group', 'an_option')
+
+    def test_skip_unless_no_option(self):
+        self._test_skip_unless_config(True, 'service_available',
+                                      'not_an_option')
+
+    def test_skip_unless_false_option(self):
+        self._test_skip_unless_config(True, 'service_available', 'glance')
+
+    def test_skip_unless_true_option(self):
+        self._test_skip_unless_config(False,
+                                      'service_available', 'nova')
+
+    def test_skip_if_no_group(self):
+        self._test_skip_if_config(False, 'fake_group', 'an_option')
+
+    def test_skip_if_no_option(self):
+        self._test_skip_if_config(False, 'service_available', 'not_an_option')
+
+    def test_skip_if_false_option(self):
+        self._test_skip_if_config(False, 'service_available', 'glance')
+
+    def test_skip_if_true_option(self):
+        self._test_skip_if_config(True, 'service_available', 'nova')