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