Initial import of tests from the Zodiac project. On suggestion from Westmaas, imported tests under the nova directory

(final naming TBD) to more quickly get them imported. To run these tests, execute 'nosetests nova/tests'.
I've also only submitted the most stable of the tests. More to come.

Change-Id: I2abd961992c02b27c4deaa9f11a49ba91c5b765d

Fixed config defaults

Change-Id: I90d5ea20167caddbec6b4cf51a0df9bb333514cb
This commit is contained in:
Daryl Walleck 2011-11-02 02:22:15 -05:00
parent 883cb26642
commit 1465d61ac5
18 changed files with 637 additions and 0 deletions

0
storm/__init__.py Normal file
View File

0
storm/common/__init__.py Normal file
View File

View File

@ -0,0 +1,96 @@
import httplib2
import json
import storm.config
class RestClient(object):
def __init__(self, user, key, auth_url, tenant_name=None):
self.config = storm.config.StormConfig()
if self.config.env.authentication == 'keystone_v2':
self.token, self.base_url = self.keystone_v2_auth(user,
key,
auth_url,
tenant_name)
else:
self.token, self.base_url = self.basic_auth(user,
key,
auth_url)
def basic_auth(self, user, api_key, auth_url):
"""
Provides authentication for the target API
"""
params = {}
params['headers'] = {'User-Agent': 'Test-Client', 'X-Auth-User': user,
'X-Auth-Key': api_key}
self.http_obj = httplib2.Http()
resp, body = self.http_obj.request(auth_url, 'GET', **params)
try:
return resp['x-auth-token'], resp['x-server-management-url']
except:
raise
def keystone_v2_auth(self, user, api_key, auth_url, tenant_name):
"""
Provides authentication via Keystone 2.0
"""
creds = {'auth': {
'passwordCredentials': {
'username': user,
'password': api_key,
},
'tenantName': tenant_name
}
}
self.http_obj = httplib2.Http()
headers = {'Content-Type': 'application/json'}
body = json.dumps(creds)
resp, body = self.http_obj.request(auth_url, 'POST',
headers=headers, body=body)
try:
auth_data = json.loads(body)['access']
token = auth_data['token']['id']
endpoints = auth_data['serviceCatalog'][0]['endpoints']
mgmt_url = endpoints[0]['publicURL']
#TODO (dwalleck): This is a horrible stopgap.
#Need to join strings more cleanly
temp = mgmt_url.rsplit('/')
service_url = temp[0] + '//' + temp[2] + '/' + temp[3] + '/'
management_url = service_url + tenant_name
return token, management_url
except KeyError:
print "Failed to authenticate user"
raise
def post(self, url, body, headers):
return self.request('POST', url, headers, body)
def get(self, url):
return self.request('GET', url)
def delete(self, url):
return self.request('DELETE', url)
def put(self, url, body, headers):
return self.request('PUT', url, headers, body)
def request(self, method, url, headers=None, body=None):
""" A simple HTTP request interface."""
self.http_obj = httplib2.Http()
if headers == None:
headers = {}
headers['X-Auth-Token'] = self.token
req_url = "%s/%s" % (self.base_url, url)
resp, body = self.http_obj.request(req_url, method,
headers=headers, body=body)
return resp, body

79
storm/common/ssh.py Normal file
View File

@ -0,0 +1,79 @@
import time
import socket
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore")
import paramiko
class Client(object):
def __init__(self, host, username, password, timeout=300):
self.host = host
self.username = username
self.password = password
self.timeout = int(timeout)
def _get_ssh_connection(self):
"""Returns an ssh connection to the specified host"""
_timeout = True
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(
paramiko.AutoAddPolicy())
_start_time = time.time()
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)
_timeout = False
break
except socket.error:
continue
except paramiko.AuthenticationException:
time.sleep(15)
continue
if _timeout:
raise socket.error("SSH connect timed out")
return ssh
def _is_timed_out(self, timeout, start_time):
return (time.time() - timeout) > start_time
def connect_until_closed(self):
"""Connect to the server and wait until connection is lost"""
try:
ssh = self._get_ssh_connection()
_transport = ssh.get_transport()
_start_time = time.time()
_timed_out = self._is_timed_out(self.timeout, _start_time)
while _transport.is_active() and not _timed_out:
time.sleep(5)
_timed_out = self._is_timed_out(self.timeout, _start_time)
ssh.close()
except (EOFError, paramiko.AuthenticationException, socket.error):
return
def exec_command(self, cmd):
"""Execute the specified command on the server.
:returns: data read from standard output of the command
"""
ssh = self._get_ssh_connection()
stdin, stdout, stderr = ssh.exec_command(cmd)
output = stdout.read()
ssh.close()
return output
def test_connection_auth(self):
""" Returns true if ssh can connect to server"""
try:
connection = self._get_ssh_connection()
connection.close()
except paramiko.AuthenticationException:
return False
return True

View File

View File

@ -0,0 +1,5 @@
import random
def rand_name(self, name='test'):
return name + str(random.randint(1, 99999999999))

115
storm/config.py Normal file
View File

@ -0,0 +1,115 @@
import ConfigParser
class NovaConfig(object):
"""Provides configuration information for connecting to Nova."""
def __init__(self, conf):
"""Initialize a Nova-specific configuration object."""
self.conf = conf
def get(self, item_name, default_value):
try:
return self.conf.get("nova", item_name)
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
return default_value
@property
def auth_url(self):
"""URL used to authenticate. Defaults to 127.0.0.1."""
return self.get("auth_url", "127.0.0.1")
@property
def username(self):
"""Username to use for Nova API requests. Defaults to 'admin'."""
return self.get("user", "admin")
@property
def tenant_name(self):
"""Tenant name to use for Nova API requests. Defaults to 'admin'."""
return self.get("tenant_name", "admin")
@property
def api_key(self):
"""API key to use when authenticating. Defaults to 'admin_key'."""
return self.get("api_key", "admin_key")
@property
def build_interval(self):
"""Time in seconds between build status checks."""
return float(self.get("build_interval", 10))
@property
def ssh_timeout(self):
"""Timeout in seconds to use when connecting via ssh."""
return float(self.get("ssh_timeout", 300))
@property
def build_timeout(self):
"""Timeout in seconds to wait for an entity to build."""
return float(self.get("build_timeout", 300))
class EnvironmentConfig(object):
def __init__(self, conf):
"""Initialize a Environment-specific configuration object."""
self.conf = conf
def get(self, item_name, default_value):
try:
return self.conf.get("environment", item_name)
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
return default_value
@property
def image_ref(self):
"""Valid imageRef to use """
return self.get("image_ref", 3)
@property
def image_ref_alt(self):
"""Valid imageRef to rebuild images with"""
return self.get("image_ref_alt", 3)
@property
def flavor_ref(self):
"""Valid flavorRef to use"""
return int(self.get("flavor_ref", 1))
@property
def flavor_ref_alt(self):
"""Valid flavorRef to resize images with"""
return self.get("flavor_ref_alt", 2)
@property
def resize_available(self):
""" Does the test environment support resizing """
return self.get("resize_available", 'false') != 'false'
@property
def create_image_enabled(self):
""" Does the test environment support resizing """
return self.get("create_image_enabled", 'false') != 'false'
@property
def authentication(self):
""" What auth method does the environment use (basic|keystone) """
return self.get("authentication", 'keystone')
class StormConfig(object):
"""Provides OpenStack configuration information."""
_path = "etc/storm.conf"
def __init__(self, path=None):
"""Initialize a configuration from a path."""
self._conf = self.load_config(self._path)
self.nova = NovaConfig(self._conf)
self.env = EnvironmentConfig(self._conf)
def load_config(self, path=None):
"""Read configuration from given path and return a config object."""
config = ConfigParser.SafeConfigParser()
config.read(path)
return config

10
storm/exceptions.py Normal file
View File

@ -0,0 +1,10 @@
class TimeoutException(Exception):
""" Exception on timeout """
def __repr__(self):
return "Request timed out"
class BuildErrorException(Exception):
""" Exception on server build """
def __repr__(self):
return "Server failed into error status"

38
storm/openstack.py Normal file
View File

@ -0,0 +1,38 @@
from storm.services.nova.json.images_client import ImagesClient
from storm.services.nova.json.flavors_client import FlavorsClient
from storm.services.nova.json.servers_client import ServersClient
import storm.config
class Manager(object):
def __init__(self):
"""
Top level manager for all Openstack APIs
"""
self.config = storm.config.StormConfig()
if self.config.env.authentication == 'keystone_v2':
self.servers_client = ServersClient(self.config.nova.username,
self.config.nova.api_key,
self.config.nova.auth_url,
self.config.nova.tenant_name)
self.flavors_client = FlavorsClient(self.config.nova.username,
self.config.nova.api_key,
self.config.nova.auth_url,
self.config.nova.tenant_name)
self.images_client = ImagesClient(self.config.nova.username,
self.config.nova.api_key,
self.config.nova.auth_url,
self.config.nova.tenant_name)
else:
#Assuming basic/native authentication
self.servers_client = ServersClient(self.config.nova.username,
self.config.nova.api_key,
self.config.nova.auth_url)
self.flavors_client = FlavorsClient(self.config.nova.username,
self.config.nova.api_key,
self.config.nova.auth_url)
self.images_client = ImagesClient(self.config.nova.username,
self.config.nova.api_key,
self.config.nova.auth_url)

View File

View File

View File

View File

@ -0,0 +1,41 @@
from storm.common import rest_client
import json
import time
class FlavorsClient(object):
def __init__(self, username, key, auth_url, tenant_name=None):
self.client = rest_client.RestClient(username, key,
auth_url, tenant_name)
def list_flavors(self, params=None):
url = 'flavors'
if params != None:
param_list = []
for param, value in params.iteritems():
param_list.append("%s=%s&" % (param, value))
url = "flavors?" + "".join(param_list)
resp, body = self.client.get(url)
body = json.loads(body)
return resp, body
def list_flavors_with_detail(self, params=None):
url = 'flavors/detail'
if params != None:
param_list = []
for param, value in params.iteritems():
param_list.append("%s=%s&" % (param, value))
url = "flavors/detail?" + "".join(param_list)
resp, body = self.client.get(url)
body = json.loads(body)
return resp, body
def get_flavor_details(self, flavor_id):
resp, body = self.client.get("flavors/%s" % str(flavor_id))
body = json.loads(body)
return resp, body['flavor']

View File

0
storm/tests/__init__.py Normal file
View File

View File

@ -0,0 +1,39 @@
from nose.plugins.attrib import attr
from storm import openstack
import storm.config
import unittest2 as unittest
class FlavorsTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.os = openstack.Manager()
cls.client = cls.os.flavors_client
cls.config = storm.config.StormConfig()
cls.flavor_id = cls.config.env.flavor_ref
@attr(type='smoke')
def test_list_flavors(self):
""" List of all flavors should contain the expected flavor """
resp, body = self.client.list_flavors()
flavors = body['flavors']
resp, flavor = self.client.get_flavor_details(self.flavor_id)
flavor_min_detail = {'id': flavor['id'], 'links': flavor['links'],
'name': flavor['name']}
self.assertTrue(flavor_min_detail in flavors)
@attr(type='smoke')
def test_list_flavors_with_detail(self):
""" Detailed list of all flavors should contain the expected flavor """
resp, body = self.client.list_flavors_with_detail()
flavors = body['flavors']
resp, flavor = self.client.get_flavor_details(self.flavor_id)
self.assertTrue(flavor in flavors)
@attr(type='smoke')
def test_get_flavor(self):
""" The expected flavor details should be returned """
resp, flavor = self.client.get_flavor_details(self.flavor_id)
self.assertEqual(self.flavor_id, flavor['id'])

View File

@ -0,0 +1,96 @@
from nose.plugins.attrib import attr
from storm import openstack
import unittest2 as unittest
import storm.config
from storm.common.utils.data_utils import rand_name
class ServerActionsTest(unittest.TestCase):
resize_available = storm.config.StormConfig().env.resize_available
@classmethod
def setUpClass(cls):
cls.os = openstack.Manager()
cls.client = cls.os.servers_client
cls.config = storm.config.StormConfig()
cls.image_ref = cls.config.env.image_ref
cls.image_ref_alt = cls.config.env.image_ref_alt
cls.flavor_ref = cls.config.env.flavor_ref
cls.flavor_ref_alt = cls.config.env.flavor_ref_alt
def setUp(self):
self.name = rand_name('server')
resp, server = self.client.create_server(self.name, self.image_ref,
self.flavor_ref)
self.id = server['id']
self.client.wait_for_server_status(self.id, 'ACTIVE')
def tearDown(self):
self.client.delete_server(self.id)
@attr(type='smoke')
def test_change_server_password(self):
""" The server's password should be set to the provided password """
resp, body = self.client.change_password(self.id, 'newpass')
self.client.wait_for_server_status(self.id, 'ACTIVE')
#TODO: SSH in to verify the new password works
@attr(type='smoke')
def test_reboot_server_hard(self):
""" The server should be power cycled """
#TODO: Add validation the server has been rebooted
resp, body = self.client.reboot(self.id, 'HARD')
self.client.wait_for_server_status(self.id, 'ACTIVE')
@attr(type='smoke')
def test_reboot_server_soft(self):
""" The server should be signaled to reboot gracefully """
#TODO: Add validation the server has been rebooted
resp, body = self.client.reboot(self.id, 'SOFT')
self.client.wait_for_server_status(self.id, 'ACTIVE')
@attr(type='smoke')
def test_rebuild_server(self):
""" The server should be rebuilt using the provided image """
self.client.rebuild(self.id, self.image_ref_alt, name='rebuiltserver')
self.client.wait_for_server_status(self.id, 'ACTIVE')
resp, server = self.client.get_server(self.id)
self.assertEqual(self.image_ref_alt, server['image']['id'])
self.assertEqual('rebuiltserver', server['name'])
@attr(type='smoke')
@unittest.skipIf(not resize_available, 'Resize not available.')
def test_resize_server_confirm(self):
"""
The server's RAM and disk space should be modified to that of
the provided flavor
"""
self.client.resize(self.id, self.flavor_ref_alt)
self.client.wait_for_server_status(self.id, 'VERIFY_RESIZE')
self.client.confirm_resize(self.id)
self.client.wait_for_server_status(self.id, 'ACTIVE')
resp, server = self.client.get_server(self.id)
self.assertEqual(self.flavor_ref_alt, server['flavor']['id'])
@attr(type='smoke')
@unittest.skipIf(not resize_available, 'Resize not available.')
def test_resize_server_revert(self):
"""
The server's RAM and disk space should return to its original
values after a resize is reverted
"""
self.client.resize(self.id, self.flavor_ref_alt)
self.client.wait_for_server_status(id, 'VERIFY_RESIZE')
self.client.revert_resize(self.id)
self.client.wait_for_server_status(id, 'ACTIVE')
resp, server = self.client.get_server(id)
self.assertEqual(self.flavor_ref, server['flavor']['id'])

118
storm/tests/test_servers.py Normal file
View File

@ -0,0 +1,118 @@
from storm.common import ssh
from nose.plugins.attrib import attr
from storm import openstack
from storm.common.utils.data_utils import rand_name
import base64
import storm.config
import unittest2 as unittest
class ServersTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.os = openstack.Manager()
cls.client = cls.os.servers_client
cls.config = storm.config.StormConfig()
cls.image_ref = cls.config.env.image_ref
cls.flavor_ref = cls.config.env.flavor_ref
cls.ssh_timeout = cls.config.nova.ssh_timeout
@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)
#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'])
#Teardown
self.client.delete_server(self.id)
@attr(type='smoke')
def test_create_server_with_admin_password(self):
"""
If an admin password is provided on server creation, the server's root
password should be set to that password.
"""
name = rand_name('server')
resp, server = self.client.create_server(name, self.image_ref,
self.flavor_ref,
adminPass='testpassword')
#Verify the password is set correctly in the response
self.assertEqual('testpassword', server['adminPass'])
#SSH into the server using the set password
self.client.wait_for_server_status(server['id'], 'ACTIVE')
resp, addresses = self.client.list_addresses(server['id'])
ip = addresses['public'][0]['addr']
client = ssh.Client(ip, 'root', 'testpassword', self.ssh_timeout)
self.assertTrue(client.test_connection_auth())
#Teardown
self.client.delete_server(server['id'])
@attr(type='smoke')
def test_update_server_name(self):
""" The server name should be changed to the the provided value """
name = rand_name('server')
resp, server = self.client.create_server(name, self.image_ref,
self.flavor_ref)
self.client.wait_for_server_status(server['id'], 'ACTIVE')
#Update the server with a new name
self.client.update_server(server['id'], name='newname')
self.client.wait_for_server_status(server['id'], 'ACTIVE')
#Verify the name of the server has changed
resp, server = self.client.get_server(server['id'])
self.assertEqual('newname', server['name'])
#Teardown
self.client.delete_server(server['id'])
@attr(type='smoke')
def test_update_access_server_address(self):
"""
The server's access addresses should reflect the provided values
"""
name = rand_name('server')
resp, server = self.client.create_server(name, self.image_ref,
self.flavor_ref)
self.client.wait_for_server_status(server['id'], 'ACTIVE')
#Update the IPv4 and IPv6 access addresses
self.client.update_server(server['id'], accessIPv4='1.1.1.1',
accessIPv6='::babe:2.2.2.2')
self.client.wait_for_server_status(server['id'], 'ACTIVE')
#Verify the access addresses have been updated
resp, server = self.client.get_server(server['id'])
self.assertEqual('1.1.1.1', server['accessIPv4'])
self.assertEqual('::babe:2.2.2.2', server['accessIPv6'])
#Teardown
self.client.delete_server(server['id'])