Adds instance_utils library and initial SSH tests
* Provides an instance util class for common server queries * Refactored the create server smoke test as an example * Added ssh tag to tests requiring SSH to allow them to be skipped if needed Change-Id: Ia34d7c75ad05f7658d1abb7bebeb1bbd271fd089
This commit is contained in:
@@ -57,6 +57,23 @@ build_interval = 10
|
||||
# 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"
|
||||
|
||||
@@ -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 @@ with warnings.catch_warnings():
|
||||
|
||||
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 @@ class Client(object):
|
||||
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 @@ class Client(object):
|
||||
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):
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
@@ -164,6 +164,31 @@ class ComputeConfig(BaseConfig):
|
||||
"""Timeout in seconds to wait for an entity to build."""
|
||||
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."""
|
||||
|
||||
@@ -75,3 +75,12 @@ class ComputeFault(TempestException):
|
||||
|
||||
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"
|
||||
|
||||
@@ -20,6 +20,7 @@ class BaseComputeTest(unittest.TestCase):
|
||||
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
|
||||
|
||||
@@ -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))
|
||||
@@ -11,42 +11,6 @@ class ServersTest(BaseComputeTest):
|
||||
def setUpClass(cls):
|
||||
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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user