Merge "Adds instance_utils library and initial SSH tests"
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index aa101d3..c7c403c 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -57,6 +57,23 @@
 # to build or reach an expected status
 build_timeout = 600
 
+# Run additional tests that use SSH for instance validation?
+# This requires the instances be routable from the host
+#  executing the tests
+run_ssh = false
+
+# Name of a user used to authenticated to an instance
+ssh_user = {$SSH_USER}
+
+# Network id used for SSH (public, private, etc)
+network_for_ssh = {$SSH_NETWORK}
+
+# IP version of the address used for SSH
+ip_version_for_ssh = {$SSH_IP_VERSION}
+
+# Number of seconds to wait to authenticate to an instance
+ssh_timeout = 300
+
 # The type of endpoint for a Compute API service. Unless you have a
 # custom Keystone service catalog implementation, you probably want to leave
 # this value as "compute"
diff --git a/tempest/common/ssh.py b/tempest/common/ssh.py
index 2f1d96b..f43ebd9 100644
--- a/tempest/common/ssh.py
+++ b/tempest/common/ssh.py
@@ -1,6 +1,7 @@
 import time
 import socket
 import warnings
+from tempest import exceptions
 
 with warnings.catch_warnings():
     warnings.simplefilter("ignore")
@@ -9,7 +10,7 @@
 
 class Client(object):
 
-    def __init__(self, host, username, password, timeout=300):
+    def __init__(self, host, username, password, timeout=60):
         self.host = host
         self.username = username
         self.password = password
@@ -26,8 +27,7 @@
         while not self._is_timed_out(self.timeout, _start_time):
             try:
                 ssh.connect(self.host, username=self.username,
-                    password=self.password, look_for_keys=False,
-                    timeout=20)
+                    password=self.password, timeout=20)
                 _timeout = False
                 break
             except socket.error:
@@ -36,7 +36,9 @@
                 time.sleep(15)
                 continue
         if _timeout:
-            raise socket.error("SSH connect timed out")
+            raise exceptions.SSHTimeout(host=self.host,
+                                        user=self.username,
+                                        password=self.password)
         return ssh
 
     def _is_timed_out(self, timeout, start_time):
diff --git a/tempest/common/utils/__init__.py b/tempest/common/utils/__init__.py
index e69de29..b9829a3 100644
--- a/tempest/common/utils/__init__.py
+++ b/tempest/common/utils/__init__.py
@@ -0,0 +1,4 @@
+LAST_REBOOT_TIME_FORMAT = '%Y-%m-%d %H:%M'
+PING_IPV4_COMMAND = 'ping -c 3 '
+PING_IPV6_COMMAND = 'ping6 -c 3 '
+PING_PACKET_LOSS_REGEX = '(\d{1,3})\.?\d*\% packet loss'
diff --git a/tempest/common/utils/linux/__init__.py b/tempest/common/utils/linux/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/common/utils/linux/__init__.py
diff --git a/tempest/common/utils/linux/remote_client.py b/tempest/common/utils/linux/remote_client.py
new file mode 100644
index 0000000..27f8fd3
--- /dev/null
+++ b/tempest/common/utils/linux/remote_client.py
@@ -0,0 +1,48 @@
+from tempest.common.ssh import Client
+from tempest.config import TempestConfig
+from tempest.exceptions import SSHTimeout, ServerUnreachable
+
+
+class RemoteClient():
+
+    def __init__(self, server, username, password):
+        ssh_timeout = TempestConfig().compute.ssh_timeout
+        network = TempestConfig().compute.network_for_ssh
+        ip_version = TempestConfig().compute.ip_version_for_ssh
+        addresses = server['addresses'][network]
+
+        for address in addresses:
+            if address['version'] == ip_version:
+                ip_address = address['addr']
+                break
+
+        if ip_address is None:
+            raise ServerUnreachable()
+
+        self.ssh_client = Client(ip_address, username, password, ssh_timeout)
+        if not self.ssh_client.test_connection_auth():
+            raise SSHTimeout()
+
+    def can_authenticate(self):
+        # Re-authenticate
+        return self.ssh_client.test_connection_auth()
+
+    def hostname_equals_servername(self, expected_hostname):
+        # Get hostname using command "hostname"
+        actual_hostname = self.ssh_client.exec_command("hostname").rstrip()
+        return expected_hostname == actual_hostname
+
+    def get_files(self, path):
+        # Return a list of comma seperated files
+        command = "ls -m " + path
+        return self.ssh_client.exec_command(command).rstrip('\n').split(', ')
+
+    def get_ram_size_in_mb(self):
+        output = self.ssh_client.exec_command('free -m | grep Mem')
+        if output:
+            return output.split()[1]
+
+    def get_number_of_vcpus(self):
+        command = 'cat /proc/cpuinfo | grep processor | wc -l'
+        output = self.ssh_client.exec_command(command)
+        return int(output)
diff --git a/tempest/config.py b/tempest/config.py
index d4a0c03..0a76ce5 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -165,6 +165,31 @@
         return float(self.get("build_timeout", 300))
 
     @property
+    def run_ssh(self):
+        """Does the test environment support snapshots?"""
+        return self.get("run_ssh", 'false').lower() != 'false'
+
+    @property
+    def ssh_user(self):
+        """User name used to authenticate to an instance."""
+        return self.get("ssh_user", "root")
+
+    @property
+    def ssh_timeout(self):
+        """Timeout in seconds to wait for authentcation to succeed."""
+        return float(self.get("ssh_timeout", 300))
+
+    @property
+    def network_for_ssh(self):
+        """Network used for SSH connections."""
+        return self.get("network_for_ssh", "public")
+
+    @property
+    def ip_version_for_ssh(self):
+        """IP version used for SSH connections."""
+        return int(self.get("ip_version_for_ssh", 4))
+
+    @property
     def catalog_type(self):
         """Catalog type of the Compute service."""
         return self.get("catalog_type", 'compute')
diff --git a/tempest/exceptions.py b/tempest/exceptions.py
index 2741ff1..29ddeeb 100644
--- a/tempest/exceptions.py
+++ b/tempest/exceptions.py
@@ -79,3 +79,12 @@
 
 class Duplicate(TempestException):
     message = "An object with that identifier already exists"
+
+
+class SSHTimeout(TempestException):
+    message = ("Connection to the %(host)s via SSH timed out.\n"
+                "User: %(user)s, Password: %(password)s")
+
+
+class ServerUnreachable(TempestException):
+    message = "The server is not reachable via the configured network"
diff --git a/tempest/tests/base_compute_test.py b/tempest/tests/base_compute_test.py
index 216f506..2436bb0 100644
--- a/tempest/tests/base_compute_test.py
+++ b/tempest/tests/base_compute_test.py
@@ -20,6 +20,7 @@
     config = os.config
     build_interval = config.compute.build_interval
     build_timeout = config.compute.build_timeout
+    ssh_user = config.compute.ssh_user
 
     # Validate reference data exists
     # If not, attempt to auto-configure
diff --git a/tempest/tests/compute/test_create_server.py b/tempest/tests/compute/test_create_server.py
new file mode 100644
index 0000000..c51afce
--- /dev/null
+++ b/tempest/tests/compute/test_create_server.py
@@ -0,0 +1,81 @@
+import base64
+import unittest2 as unittest
+from nose.plugins.attrib import attr
+from tempest import openstack
+import tempest.config
+from tempest.common.utils.data_utils import rand_name
+from tempest.common.utils.linux.remote_client import RemoteClient
+from tempest.tests.base_compute_test import BaseComputeTest
+
+
+class ServersTest(BaseComputeTest):
+
+    run_ssh = tempest.config.TempestConfig().compute.run_ssh
+
+    @classmethod
+    def setUpClass(cls):
+        cls.meta = {'hello': 'world'}
+        cls.accessIPv4 = '1.1.1.1'
+        cls.accessIPv6 = '::babe:220.12.22.2'
+        cls.name = rand_name('server')
+        file_contents = 'This is a test file.'
+        personality = [{'path': '/etc/test.txt',
+                       'contents': base64.b64encode(file_contents)}]
+        cls.client = cls.servers_client
+        cls.resp, cls.server_initial = cls.client.create_server(cls.name,
+                                                 cls.image_ref,
+                                                 cls.flavor_ref,
+                                                 meta=cls.meta,
+                                                 accessIPv4=cls.accessIPv4,
+                                                 accessIPv6=cls.accessIPv6,
+                                                 personality=personality)
+        cls.password = cls.server_initial['adminPass']
+        cls.client.wait_for_server_status(cls.server_initial['id'], 'ACTIVE')
+        resp, cls.server = cls.client.get_server(cls.server_initial['id'])
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.client.delete_server(cls.server_initial['id'])
+
+    @attr(type='smoke')
+    def test_create_server_response(self):
+        """Check that the required fields are returned with values"""
+        self.assertEqual(202, self.resp.status)
+        self.assertTrue(self.server_initial['id'] is not None)
+        self.assertTrue(self.server_initial['adminPass'] is not None)
+
+    @attr(type='smoke')
+    def test_created_server_fields(self):
+        """Verify the specified server attributes are set correctly"""
+
+        self.assertEqual(self.accessIPv4, self.server['accessIPv4'])
+        self.assertEqual(self.accessIPv6, self.server['accessIPv6'])
+        self.assertEqual(self.name, self.server['name'])
+        self.assertEqual(self.image_ref, self.server['image']['id'])
+        self.assertEqual(str(self.flavor_ref), self.server['flavor']['id'])
+        self.assertEqual(self.meta, self.server['metadata'])
+
+    @attr(type='positive')
+    @unittest.skipIf(not run_ssh, 'Instance validation tests are disabled.')
+    def test_can_log_into_created_server(self):
+        """Check that the user can authenticate with the generated password"""
+        linux_client = RemoteClient(self.server, self.ssh_user, self.password)
+        self.assertTrue(linux_client.can_authenticate())
+
+    @attr(type='positive')
+    @unittest.skipIf(not run_ssh, 'Instance validation tests are disabled.')
+    def test_verify_created_server_vcpus(self):
+        """
+        Verify that the number of vcpus reported by the instance matches
+        the amount stated by the flavor
+        """
+        resp, flavor = self.flavors_client.get_flavor_details(self.flavor_ref)
+        linux_client = RemoteClient(self.server, self.ssh_user, self.password)
+        self.assertEqual(flavor['vcpus'], linux_client.get_number_of_vcpus())
+
+    @attr(type='positive')
+    @unittest.skipIf(not run_ssh, 'Instance validation tests are disabled.')
+    def test_host_name_is_same_as_server_name(self):
+        """Verify the instance host name is the same as the server name"""
+        linux_client = RemoteClient(self.server, self.ssh_user, self.password)
+        self.assertTrue(linux_client.hostname_equals_servername(self.name))
diff --git a/tempest/tests/test_servers.py b/tempest/tests/test_servers.py
index b152b4c..aab7a96 100644
--- a/tempest/tests/test_servers.py
+++ b/tempest/tests/test_servers.py
@@ -12,42 +12,6 @@
         cls.client = cls.servers_client
 
     @attr(type='smoke')
-    def test_create_delete_server(self):
-        meta = {'hello': 'world'}
-        accessIPv4 = '1.1.1.1'
-        accessIPv6 = '::babe:220.12.22.2'
-        name = rand_name('server')
-        file_contents = 'This is a test file.'
-        personality = [{'path': '/etc/test.txt',
-                       'contents': base64.b64encode(file_contents)}]
-        resp, server = self.client.create_server(name,
-                                                 self.image_ref,
-                                                 self.flavor_ref,
-                                                 meta=meta,
-                                                 accessIPv4=accessIPv4,
-                                                 accessIPv6=accessIPv6,
-                                                 personality=personality)
-        #Check the initial response
-        self.assertEqual(202, resp.status)
-        self.assertTrue(server['id'] is not None)
-        self.assertTrue(server['adminPass'] is not None)
-
-        #Wait for the server to become active
-        self.client.wait_for_server_status(server['id'], 'ACTIVE')
-
-        #Verify the specified attributes are set correctly
-        resp, server = self.client.get_server(server['id'])
-        self.assertEqual('1.1.1.1', server['accessIPv4'])
-        self.assertEqual('::babe:220.12.22.2', server['accessIPv6'])
-        self.assertEqual(name, server['name'])
-        self.assertEqual(self.image_ref, server['image']['id'])
-        self.assertEqual(str(self.flavor_ref), server['flavor']['id'])
-
-        #Delete the server
-        resp, body = self.client.delete_server(server['id'])
-        self.assertEqual(204, resp.status)
-
-    @attr(type='smoke')
     def test_create_server_with_admin_password(self):
         """
         If an admin password is provided on server creation, the server's root