From e5b83d49dea81e5acccbd4277fc8832fa84e6cc5 Mon Sep 17 00:00:00 2001 From: Daryl Walleck Date: Thu, 10 Nov 2011 14:39:02 -0600 Subject: [PATCH] Added server details tests. Also re-added several files that somehow missed the initial commit Change-Id: I23eb08d2589b5c513b38de2476e44d53f21a79a1 --- etc/storm.conf.sample | 17 ++ storm/common/rest_client.py | 2 +- storm/config.py | 2 +- storm/exceptions.py | 4 +- storm/services/nova/json/images_client.py | 85 +++++++ storm/services/nova/json/servers_client.py | 259 +++++++++++++++++++++ storm/tests/test_flavors.py | 6 +- storm/tests/test_server_actions.py | 6 +- storm/tests/test_server_details.py | 99 ++++++++ storm/tests/test_servers.py | 2 +- 10 files changed, 471 insertions(+), 11 deletions(-) create mode 100644 etc/storm.conf.sample create mode 100644 storm/services/nova/json/images_client.py create mode 100644 storm/services/nova/json/servers_client.py create mode 100644 storm/tests/test_server_details.py diff --git a/etc/storm.conf.sample b/etc/storm.conf.sample new file mode 100644 index 0000000000..fc0658005b --- /dev/null +++ b/etc/storm.conf.sample @@ -0,0 +1,17 @@ +[nova] +auth_url=http://127.0.0.1:5000/v2.0/tokens +user=admin +api_key=admin-key +tenant_name=admin-project +ssh_timeout=300 +build_interval=10 +build_timeout=600 + +[environment] +image_ref=3 +image_ref_alt=4 +flavor_ref=1 +flavor_ref_alt=2 +create_image_enabled=true +resize_available=true +authentication=keystone_v2 \ No newline at end of file diff --git a/storm/common/rest_client.py b/storm/common/rest_client.py index 44658fab93..e4ab450b88 100644 --- a/storm/common/rest_client.py +++ b/storm/common/rest_client.py @@ -83,7 +83,7 @@ class RestClient(object): return self.request('PUT', url, headers, body) def request(self, method, url, headers=None, body=None): - """ A simple HTTP request interface.""" + """A simple HTTP request interface.""" self.http_obj = httplib2.Http() if headers == None: diff --git a/storm/config.py b/storm/config.py index 8feb85b3ee..90dfed227f 100644 --- a/storm/config.py +++ b/storm/config.py @@ -5,7 +5,7 @@ class NovaConfig(object): """Provides configuration information for connecting to Nova.""" def __init__(self, conf): - """Initialize a Nova-specific configuration object.""" + """Initialize a Nova-specific configuration object""" self.conf = conf def get(self, item_name, default_value): diff --git a/storm/exceptions.py b/storm/exceptions.py index 8415ccb2fc..c75544c7a4 100644 --- a/storm/exceptions.py +++ b/storm/exceptions.py @@ -1,10 +1,10 @@ class TimeoutException(Exception): - """ Exception on timeout """ + """Exception on timeout""" def __repr__(self): return "Request timed out" class BuildErrorException(Exception): - """ Exception on server build """ + """Exception on server build""" def __repr__(self): return "Server failed into error status" diff --git a/storm/services/nova/json/images_client.py b/storm/services/nova/json/images_client.py new file mode 100644 index 0000000000..87b205abc6 --- /dev/null +++ b/storm/services/nova/json/images_client.py @@ -0,0 +1,85 @@ +from storm.common import rest_client +import json +import time + + +class ImagesClient(object): + + def __init__(self, username, key, auth_url, tenant_name=None): + self.client = rest_client.RestClient(username, key, + auth_url, tenant_name) + self.headers = {'Content-Type': 'application/json', + 'Accept': 'application/json'} + + def create_image(self, server_id, name, meta=None): + """Creates an image of the original server""" + + post_body = { + 'createImage': { + 'name': name, + } + } + + if meta != None: + post_body['metadata'] = meta + + post_body = json.dumps(post_body) + resp, body = self.client.post('servers/%s/action' % + str(server_id), post_body, self.headers) + body = json.loads(body) + return resp, body + + def list_images(self, params=None): + """Returns a list of all images filtered by any parameters""" + url = 'images' + if params != None: + param_list = [] + for param, value in params.iteritems(): + param_list.append("%s=%s&" % (param, value)) + + url = "images?" + "".join(param_list) + + resp, body = self.client.get(url) + body = json.loads(body) + return resp, body + + def list_images_with_detail(self, params=None): + """Returns a detailed list of images filtered by any parameters""" + url = 'images/detail' + if params != None: + param_list = [] + for param, value in params.iteritems(): + param_list.append("%s=%s&" % (param, value)) + + url = "images/detail?" + "".join(param_list) + + resp, body = self.client.get(url) + body = json.loads(body) + return resp, body + + def get_image(self, image_id): + """Returns the details of a single image""" + resp, body = self.client.get("images/%s" % str(image_id)) + body = json.loads(body) + return resp, body['image'] + + def delete_image(self, image_id): + """Deletes the provided image""" + return self.client.delete("images/%s" % str(image_id)) + + def wait_for_image_status(self, image_id, status): + """Waits for an image to reach a given status""" + resp, body = self.get_image(image_id) + image_status = body['image']['status'] + start = int(time.time()) + + while image_status != status: + time.sleep(self.build_interval) + resp, body = self.get_image(image_id) + image_status = body['image']['status'] + + if image_status == 'ERROR': + raise exceptions.TimeoutException + + if int(time.time()) - start >= self.build_timeout: + raise exceptions.BuildErrorException diff --git a/storm/services/nova/json/servers_client.py b/storm/services/nova/json/servers_client.py new file mode 100644 index 0000000000..21cabfa8d7 --- /dev/null +++ b/storm/services/nova/json/servers_client.py @@ -0,0 +1,259 @@ +from storm import exceptions +from storm.common import rest_client +import json +import storm.config +import time + + +class ServersClient(object): + + def __init__(self, username, key, auth_url, tenant_name=None): + self.client = rest_client.RestClient(username, key, + auth_url, tenant_name) + self.config = storm.config.StormConfig() + self.build_interval = self.config.nova.build_interval + self.build_timeout = self.config.nova.build_timeout + self.headers = {'Content-Type': 'application/json', + 'Accept': 'application/json'} + + def create_server(self, name, image_ref, flavor_ref, meta=None, + personality=None, accessIPv4=None, accessIPv6=None, + adminPass=None): + """ + Creates an instance of a server. + name: The name of the server. + image_ref: The reference to the image used to build the server. + flavor_ref: The flavor used to build the server. + adminPass: Sets the initial root password. + meta: A dictionary of values to be used as metadata. + personality: A list of dictionaries for files to be injected into + the server. + accessIPv4: The IPv4 access address for the server. + accessIPv6: The IPv6 access address for the server. + """ + + post_body = { + 'name': name, + 'imageRef': image_ref, + 'flavorRef': flavor_ref, + } + + if meta != None: + post_body['metadata'] = meta + + if personality != None: + post_body['personality'] = personality + + if adminPass != None: + post_body['adminPass'] = adminPass + + if accessIPv4 != None: + post_body['accessIPv4'] = accessIPv4 + + if accessIPv6 != None: + post_body['accessIPv6'] = accessIPv6 + + post_body = json.dumps({'server': post_body}) + resp, body = self.client.post('servers', post_body, self.headers) + body = json.loads(body) + return resp, body['server'] + + def update_server(self, server_id, name=None, meta=None, accessIPv4=None, + accessIPv6=None): + """ + Updates the properties of an existing server. + server_id: The id of an existing server. + name: The name of the server. + personality: A list of files to be injected into the server. + accessIPv4: The IPv4 access address for the server. + accessIPv6: The IPv6 access address for the server. + """ + + post_body = {} + + if meta != None: + post_body['metadata'] = meta + + if name != None: + post_body['name'] = name + + if accessIPv4 != None: + post_body['accessIPv4'] = accessIPv4 + + if accessIPv6 != None: + post_body['accessIPv6'] = accessIPv6 + + post_body = json.dumps({'server': post_body}) + resp, body = self.client.put("servers/%s" % str(server_id), + post_body, self.headers) + body = json.loads(body) + return resp, body['server'] + + def get_server(self, server_id): + """Returns the details of an existing server""" + resp, body = self.client.get("servers/%s" % str(server_id)) + body = json.loads(body) + return resp, body['server'] + + def delete_server(self, server_id): + """Deletes the given server""" + return self.client.delete("servers/%s" % str(server_id)) + + def list_servers(self, params=None): + """Lists all servers for a user""" + + url = 'servers' + if params != None: + param_list = [] + for param, value in params.iteritems(): + param_list.append("%s=%s&" % (param, value)) + + url = "servers?" + "".join(param_list) + + resp, body = self.client.get(url) + body = json.loads(body) + return resp, body + + def list_servers_with_detail(self, params=None): + """Lists all servers in detail for a user""" + + url = 'servers/detail' + if params != None: + param_list = [] + for param, value in params.iteritems(): + param_list.append("%s=%s&" % (param, value)) + + url = "servers/detail?" + "".join(param_list) + + resp, body = self.client.get(url) + body = json.loads(body) + return resp, body + + def wait_for_server_status(self, server_id, status): + """Waits for a server to reach a given status""" + resp, body = self.get_server(server_id) + server_status = body['status'] + start = int(time.time()) + + while(server_status != status): + time.sleep(self.build_interval) + resp, body = self.get_server(server_id) + server_status = body['status'] + + if(server_status == 'ERROR'): + raise exceptions.BuildErrorException + + if (int(time.time()) - start >= self.build_timeout): + raise exceptions.TimeoutException + + def list_addresses(self, server_id): + """Lists all addresses for a server""" + resp, body = self.client.get("servers/%s/ips" % str(server_id)) + body = json.loads(body) + return resp, body['addresses'] + + def list_addresses_by_network(self, server_id, network_id): + """Lists all addresses of a specific network type for a server""" + resp, body = self.client.get("servers/%s/ips/%s" % + (str(server_id), network_id)) + body = json.loads(body) + return resp, body + + def change_password(self, server_id, password): + """Changes the root password for the server""" + post_body = { + 'changePassword': { + 'adminPass': password, + } + } + + post_body = json.dumps(post_body) + return self.client.post('servers/%s/action' % str(server_id), + post_body, self.headers) + + def reboot(self, server_id, reboot_type): + """Reboots a server""" + post_body = { + 'reboot': { + 'type': reboot_type, + } + } + + post_body = json.dumps(post_body) + return self.client.post('servers/%s/action' % str(server_id), + post_body, self.headers) + + def rebuild(self, server_id, image_ref, name=None, meta=None, + personality=None, adminPass=None): + """Rebuilds a server with a new image""" + post_body = { + 'imageRef': image_ref, + } + + if name != None: + post_body['name'] = name + + if adminPass != None: + post_body['adminPass'] = adminPass + + if meta != None: + post_body['metadata'] = meta + + if personality != None: + post_body['personality'] = personality + + post_body = json.dumps({'rebuild': post_body}) + resp, body = self.client.post('servers/%s/action' % + str(server_id), post_body, + self.headers) + body = json.loads(body) + return resp, body + + def resize(self, server_id, flavor_ref): + """Changes the flavor of a server.""" + post_body = { + 'resize': { + 'flavorRef': flavor_ref, + } + } + + post_body = json.dumps(post_body) + resp, body = self.client.post('servers/%s/action' % + str(server_id), post_body, self.headers) + return resp, body + + def confirm_resize(self, server_id): + """Confirms the flavor change for a server""" + post_body = { + 'confirmResize': null + } + + post_body = json.dumps(post_body) + resp, body = self.client.post('servers/%s/action' % + str(server_id), post_body, self.headers) + return resp, body + + def revert_resize(self, server_id): + """Reverts a server back to its original flavor""" + post_body = { + 'revertResize': null + } + + post_body = json.dumps(post_body) + resp, body = self.client.post('servers/%s/action' % + str(server_id), post_body, self.headers) + return resp, body + + def create_image(self, server_id, image_name): + """Creates an image of the given server""" + post_body = { + 'createImage': { + 'name': image_name, + } + } + + post_body = json.dumps(post_body) + resp, body = self.client.post('servers/%s/action' % + str(server_id), post_body, self.headers) + body = json.loads(body) + return resp, body diff --git a/storm/tests/test_flavors.py b/storm/tests/test_flavors.py index cdac903e67..a4fa2f3ecc 100644 --- a/storm/tests/test_flavors.py +++ b/storm/tests/test_flavors.py @@ -15,7 +15,7 @@ class FlavorsTest(unittest.TestCase): @attr(type='smoke') def test_list_flavors(self): - """ List of all flavors should contain the expected flavor """ + """List of all flavors should contain the expected flavor""" resp, body = self.client.list_flavors() flavors = body['flavors'] @@ -26,7 +26,7 @@ class FlavorsTest(unittest.TestCase): @attr(type='smoke') def test_list_flavors_with_detail(self): - """ Detailed list of all flavors should contain the expected flavor """ + """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) @@ -34,6 +34,6 @@ class FlavorsTest(unittest.TestCase): @attr(type='smoke') def test_get_flavor(self): - """ The expected flavor details should be returned """ + """The expected flavor details should be returned""" resp, flavor = self.client.get_flavor_details(self.flavor_id) self.assertEqual(self.flavor_id, flavor['id']) diff --git a/storm/tests/test_server_actions.py b/storm/tests/test_server_actions.py index e93ed2b420..d6ff11c4bb 100644 --- a/storm/tests/test_server_actions.py +++ b/storm/tests/test_server_actions.py @@ -30,7 +30,7 @@ class ServerActionsTest(unittest.TestCase): @attr(type='smoke') def test_change_server_password(self): - """ The server's password should be set to the provided password """ + """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 @@ -45,7 +45,7 @@ class ServerActionsTest(unittest.TestCase): @attr(type='smoke') def test_reboot_server_soft(self): - """ The server should be signaled to reboot gracefully """ + """The server should be signaled to reboot gracefully""" #TODO: Add validation the server has been rebooted resp, body = self.client.reboot(self.id, 'SOFT') @@ -53,7 +53,7 @@ class ServerActionsTest(unittest.TestCase): @attr(type='smoke') def test_rebuild_server(self): - """ The server should be rebuilt using the provided image """ + """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') diff --git a/storm/tests/test_server_details.py b/storm/tests/test_server_details.py new file mode 100644 index 0000000000..a27d838c54 --- /dev/null +++ b/storm/tests/test_server_details.py @@ -0,0 +1,99 @@ +from nose.plugins.attrib import attr +from storm import openstack +from storm.common.utils.data_utils import rand_name +import unittest2 as unittest +import storm.config + + +class ServerDetailsTest(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.image_ref_alt = cls.config.env.image_ref_alt + cls.flavor_ref_alt = cls.config.env.flavor_ref_alt + + cls.s1_name = rand_name('server') + resp, server = cls.client.create_server(cls.s1_name, cls.image_ref, + cls.flavor_ref) + cls.client.wait_for_server_status(server['id'], 'ACTIVE') + resp, cls.s1 = cls.client.get_server(server['id']) + + cls.s2_name = rand_name('server') + resp, server = cls.client.create_server(cls.s2_name, cls.image_ref_alt, + cls.flavor_ref) + cls.client.wait_for_server_status(server['id'], 'ACTIVE') + resp, cls.s2 = cls.client.get_server(server['id']) + + cls.s3_name = rand_name('server') + resp, server = cls.client.create_server(cls.s3_name, cls.image_ref, + cls.flavor_ref_alt) + cls.client.wait_for_server_status(server['id'], 'ACTIVE') + resp, cls.s3 = cls.client.get_server(server['id']) + + @classmethod + def tearDownClass(cls): + cls.client.delete_server(cls.s1['id']) + cls.client.delete_server(cls.s2['id']) + cls.client.delete_server(cls.s3['id']) + + def test_list_servers_with_detail(self): + """ Return a detailed list of all servers """ + resp, body = self.client.list_servers_with_detail() + servers = body['servers'] + + self.assertTrue(self.s1 in servers) + self.assertTrue(self.s2 in servers) + self.assertTrue(self.s3 in servers) + + def test_list_servers_detailed_filter_by_image(self): + """Filter the detailed list of servers by image""" + params = {'image': self.image_ref} + resp, body = self.client.list_servers_with_detail(params) + servers = body['servers'] + + self.assertTrue(self.s1 in servers) + self.assertTrue(self.s2 not in servers) + self.assertTrue(self.s3 in servers) + + def test_list_servers_detailed_filter_by_flavor(self): + """Filter the detailed list of servers by flavor""" + params = {'flavor': self.flavor_ref_alt} + resp, body = self.client.list_servers_with_detail(params) + servers = body['servers'] + + self.assertTrue(self.s1 not in servers) + self.assertTrue(self.s2 not in servers) + self.assertTrue(self.s3 in servers) + + def test_list_servers_detailed_filter_by_server_name(self): + """Filter the detailed list of servers by server name""" + params = {'name': self.s1_name} + resp, body = self.client.list_servers_with_detail(params) + servers = body['servers'] + + self.assertTrue(self.s1 in servers) + self.assertTrue(self.s2 not in servers) + self.assertTrue(self.s3 not in servers) + + def test_list_servers_detailed_filter_by_server_status(self): + """Filter the detailed list of servers by server status""" + params = {'status': 'active'} + resp, body = self.client.list_servers_with_detail(params) + servers = body['servers'] + + self.assertTrue(self.s1 in servers) + self.assertTrue(self.s2 in servers) + self.assertTrue(self.s3 in servers) + + def test_get_server_details(self): + """Return the full details of a single server""" + resp, server = self.client.get_server(self.s1['id']) + + self.assertEqual(self.s1_name, server['name']) + self.assertEqual(self.image_ref, server['image']['id']) + self.assertEqual(str(self.flavor_ref), server['flavor']['id']) diff --git a/storm/tests/test_servers.py b/storm/tests/test_servers.py index 958da69ca0..8f4bae735a 100644 --- a/storm/tests/test_servers.py +++ b/storm/tests/test_servers.py @@ -77,7 +77,7 @@ class ServersTest(unittest.TestCase): @attr(type='smoke') def test_update_server_name(self): - """ The server name should be changed to the the provided value """ + """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)