From 54be28647ac3ad401006bca3069b1dfc1a65d093 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Tue, 12 Jul 2011 14:35:09 -0400 Subject: server create deserialization functional and tested --- nova/api/openstack/create_instance_helper.py | 81 ++++-- nova/api/openstack/servers.py | 63 +++- nova/api/openstack/wsgi.py | 21 ++ nova/tests/api/openstack/test_servers.py | 414 ++++++++++++++++++++++++--- 4 files changed, 508 insertions(+), 71 deletions(-) diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 2654e3c40..eea973a56 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -180,7 +180,7 @@ class CreateInstanceHelper(object): Overrides normal behavior in the case of xml content """ if request.content_type == "application/xml": - deserializer = ServerCreateRequestXMLDeserializer() + deserializer = ServerXMLDeserializer() return deserializer.deserialize(request.body) else: return self._deserialize(request.body, request.get_content_type()) @@ -295,9 +295,15 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer): """Marshal the server attribute of a parsed request""" server = {} server_node = self._find_first_child_named(node, 'server') - for attr in ["name", "imageId", "flavorId", "imageRef", "flavorRef"]: + for attr in ["name", "imageId", "flavorId"]: if server_node.getAttribute(attr): server[attr] = server_node.getAttribute(attr) + image = self._extract_image(server_node) + if image is not None: + server["image"] = image + flavor = self._extract_flavor(server_node) + if flavor is not None: + server["flavor"] = flavor metadata = self._extract_metadata(server_node) if metadata is not None: server["metadata"] = metadata @@ -306,6 +312,56 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer): server["personality"] = personality return server + def _extract_image(self, server_node): + """Retrieve an image entity from the server node""" + image_node = self._find_first_child_named(server_node, "image") + if image_node is None: + return None + + image = {} + image_id = image_node.getAttribute('id') + if image_id: + image['id'] = image_id + + image_links = self._extract_links_from_node(image_node) + if len(image_links) > 0: + image['links'] = image_links + + return image + + def _extract_flavor(self, server_node): + """Retrieve a flavor entity from the server node""" + flavor_node = self._find_first_child_named(server_node, "flavor") + if flavor_node is None: + return None + + flavor = {} + flavor_id = flavor_node.getAttribute('id') + if flavor_id: + flavor['id'] = flavor_id + + flavor_links = self._extract_links_from_node(flavor_node) + if len(flavor_links) > 0: + flavor['links'] = flavor_links + + return flavor + + def _extract_links_from_node(self, parent_node): + """Retrieve link entities from a links container provided node""" + links = [] + + for link_node in self._find_children_named(parent_node, 'atom:link'): + link = {} + link_rel = link_node.getAttribute('rel') + if link_rel is not None: + link['rel'] = link_rel + link_href = link_node.getAttribute('href') + if link_href is not None: + link['href'] = link_href + links.append(link) + + return links + def _extract_metadata(self, server_node): """Marshal the metadata attribute of a parsed request""" metadata_node = self._find_first_child_named(server_node, "metadata") @@ -331,24 +387,3 @@ class ServerXMLDeserializer(wsgi.XMLDeserializer): item["contents"] = self._extract_text(file_node) personality.append(item) return personality - - def _find_first_child_named(self, parent, name): - """Search a nodes children for the first child with a given name""" - for node in parent.childNodes: - if node.nodeName == name: - return node - return None - - def _find_children_named(self, parent, name): - """Return all of a nodes children who have the given name""" - for node in parent.childNodes: - if node.nodeName == name: - yield node - - def _extract_text(self, node): - """Get the text field contained by the given node""" - if len(node.childNodes) == 1: - child = node.childNodes[0] - if child.nodeType == child.TEXT_NODE: - return child.nodeValue - return "" diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 12af44a8d..f239044ff 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -492,11 +492,68 @@ class ControllerV11(Controller): return faults.Fault(exc.HTTPNotFound()) def _image_ref_from_req_data(self, data): - return data['server']['imageRef'] + try: + image = data['server']['image'] + except (AttributeError, KeyError): + msg = _("Missing image entity") + raise exc.HTTPBadRequest(explanation=msg) + + try: + links = image.get('links', []) + except AttributeError: + msg = _("Malformed image entity") + raise exc.HTTPBadRequest(explanation=msg) + + image_ref = None + for link in links: + try: + if link.get('rel') == 'bookmark': + image_ref = link.get('href') + break + except AttributeError: + msg = _("Malformed image link") + raise exc.HTTPBadRequest(explanation=msg) + + if image_ref is None: + try: + image_ref = image['id'] + except KeyError: + msg = _("Missing id attribute on image entity") + raise exc.HTTPBadRequest(explanation=msg) + + return image_ref def _flavor_id_from_req_data(self, data): - href = data['server']['flavorRef'] - return common.get_id_from_href(href) + try: + flavor = data['server']['flavor'] + except (AttributeError, KeyError): + msg = _("Missing flavor entity") + raise exc.HTTPBadRequest(explanation=msg) + + try: + links = flavor.get('links', []) + except AttributeError: + msg = _("Malformed flavor entity") + raise exc.HTTPBadRequest(explanation=msg) + + flavor_ref = None + for link in links: + try: + if link.get('rel') == 'bookmark': + flavor_ref = link.get('href') + break + except AttributeError: + msg = _("Malformed flavor link") + raise exc.HTTPBadRequest(explanation=msg) + + if flavor_ref is None: + try: + return flavor['id'] + except (KeyError, AttributeError): + msg = _("Missing id attribute in flavor entity") + raise exc.HTTPBadRequest(explanation=msg) + else: + return common.get_id_from_href(flavor_ref) def _get_view_builder(self, req): base_url = req.application_url diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py index 8eff9e441..0ece2cff0 100644 --- a/nova/api/openstack/wsgi.py +++ b/nova/api/openstack/wsgi.py @@ -134,6 +134,27 @@ class XMLDeserializer(TextDeserializer): listnames) return result + def _find_first_child_named(self, parent, name): + """Search a nodes children for the first child with a given name""" + for node in parent.childNodes: + if node.nodeName == name: + return node + return None + + def _find_children_named(self, parent, name): + """Return all of a nodes children who have the given name""" + for node in parent.childNodes: + if node.nodeName == name: + yield node + + def _extract_text(self, node): + """Get the text field contained by the given node""" + if len(node.childNodes) == 1: + child = node.childNodes[0] + if child.nodeType == child.TEXT_NODE: + return child.nodeValue + return "" + def default(self, datastring): return {'body': self._from_xml(datastring)} diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 775f66ad0..cb7e03934 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -612,7 +612,7 @@ class ServersTest(test.TestCase): "_get_kernel_ramdisk_from_image", kernel_ramdisk_mapping) self.stubs.Set(nova.compute.api.API, "_find_host", find_host) - def _test_create_instance_helper(self): + def test_create_instance(self): self._setup_for_create_instance() body = dict(server=dict( @@ -626,6 +626,7 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) server = json.loads(res.body)['server'] self.assertEqual(16, len(server['adminPass'])) self.assertEqual('server_test', server['name']) @@ -633,10 +634,6 @@ class ServersTest(test.TestCase): self.assertEqual(2, server['flavorId']) self.assertEqual(3, server['imageId']) self.assertEqual(FAKE_UUID, server['uuid']) - self.assertEqual(res.status_int, 200) - - def test_create_instance(self): - self._test_create_instance_helper() def test_create_instance_has_uuid(self): """Tests at the db-layer instead of API layer since that's where the @@ -692,7 +689,27 @@ class ServersTest(test.TestCase): def test_create_instance_no_key_pair(self): fakes.stub_out_key_pair_funcs(self.stubs, have_key_pair=False) - self._test_create_instance_helper() + self._setup_for_create_instance() + + body = dict(server=dict( + name='server_test', imageId=3, flavorId=2, + metadata={'hello': 'world', 'open': 'stack'}, + personality={})) + req = webob.Request.blank('/v1.0/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app()) + + server = json.loads(res.body)['server'] + self.assertEqual(16, len(server['adminPass'])) + self.assertEqual('server_test', server['name']) + self.assertEqual(1, server['id']) + self.assertEqual(2, server['flavorId']) + self.assertEqual(3, server['imageId']) + self.assertEqual(FAKE_UUID, server['uuid']) + self.assertEqual(res.status_int, 200) def test_create_instance_no_name(self): self._setup_for_create_instance() @@ -765,18 +782,34 @@ class ServersTest(test.TestCase): def test_create_instance_v1_1(self): self._setup_for_create_instance() - image_href = 'http://localhost/v1.1/images/2' - flavor_ref = 'http://localhost/v1.1/flavors/3' + image_href = 'http://localhost/v1.1/images/3' + flavor_href = 'http://localhost/v1.1/flavors/2' + body = { 'server': { 'name': 'server_test', - 'imageRef': image_href, - 'flavorRef': flavor_ref, + 'image': { + 'id': 3, + 'links': [ + {'rel': 'bookmark', 'href': image_href}, + ], + }, + 'flavor': { + 'id': 2, + 'links': [ + {'rel': 'bookmark', 'href': flavor_href}, + ], + }, 'metadata': { 'hello': 'world', 'open': 'stack', }, - 'personality': {}, + 'personality': [ + { + "path" : "/etc/banner.txt", + "contents" : "MQ==", + }, + ], }, } @@ -787,40 +820,102 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) server = json.loads(res.body)['server'] self.assertEqual(16, len(server['adminPass'])) self.assertEqual('server_test', server['name']) self.assertEqual(1, server['id']) - self.assertEqual(flavor_ref, server['flavorRef']) + self.assertEqual(flavor_href, server['flavorRef']) self.assertEqual(image_href, server['imageRef']) - self.assertEqual(res.status_int, 200) + self.assertFalse('personality' in server) - def test_create_instance_v1_1_bad_href(self): + def test_create_instance_v1_1_image_id(self): self._setup_for_create_instance() - image_href = 'http://localhost/v1.1/images/asdf' + image_id = 2 flavor_ref = 'http://localhost/v1.1/flavors/3' - body = dict(server=dict( - name='server_test', imageRef=image_href, flavorRef=flavor_ref, - metadata={'hello': 'world', 'open': 'stack'}, - personality={})) + body = { + 'server': { + 'name': 'server_test', + 'image': {'id': image_id}, + 'flavor': {'id': 3}, + }, + } + + req = webob.Request.blank('/v1.1/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(res.status_int, 200) + server = json.loads(res.body)['server'] + self.assertEqual(flavor_ref, server['flavorRef']) + + def test_create_instance_v1_1_image_link(self): + self._setup_for_create_instance() + + image_ref = 'http://localhost/v1.1/image/3' + body = { + 'server': { + 'name': 'server_test', + 'image': { + 'links':[ + {'rel': 'self', 'href': 'http://google.com'}, + {'rel': 'bookmark', 'href': image_ref}, + ], + }, + 'flavor': {'id': 3}, + }, + } + req = webob.Request.blank('/v1.1/servers') req.method = 'POST' req.body = json.dumps(body) req.headers["content-type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(res.status_int, 200) + server = json.loads(res.body)['server'] + self.assertEqual(image_ref, server['imageRef']) + + def test_create_instance_v1_1_no_valid_image(self): + self._setup_for_create_instance() + + body = { + 'server': { + 'name': 'server_test', + 'image': {}, + 'flavor': {'id': 3}, + }, + } + + req = webob.Request.blank('/v1.1/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) - def test_create_instance_v1_1_local_href(self): + def test_create_instance_v1_1_flavor_link(self): self._setup_for_create_instance() - image_id = 2 flavor_ref = 'http://localhost/v1.1/flavors/3' + body = { 'server': { 'name': 'server_test', - 'imageRef': image_id, - 'flavorRef': flavor_ref, + 'image': {'id': 3}, + 'flavor': { + 'id': 2, + 'links': [ + {'rel': 'bookmark', 'href': flavor_ref}, + ], + }, }, } @@ -831,11 +926,55 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) server = json.loads(res.body)['server'] - self.assertEqual(1, server['id']) self.assertEqual(flavor_ref, server['flavorRef']) - self.assertEqual(image_id, server['imageRef']) + + def test_create_instance_v1_1_flavor_id(self): + self._setup_for_create_instance() + + flavor_ref = 'http://localhost/v1.1/flavors/2' + + body = { + 'server': { + 'name': 'server_test', + 'image': {'id': 3}, + 'flavor': {'id': 2}, + }, + } + + req = webob.Request.blank('/v1.1/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + server = json.loads(res.body)['server'] + self.assertEqual(flavor_ref, server['flavorRef']) + + def test_create_instance_v1_1_no_valid_flavor(self): + self._setup_for_create_instance() + + body = { + 'server': { + 'name': 'server_test', + 'image': {'id': 3}, + 'flavor': {}, + }, + } + + req = webob.Request.blank('/v1.1/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(res.status_int, 400) + + def test_create_instance_with_admin_pass_v1_0(self): self._setup_for_create_instance() @@ -858,7 +997,7 @@ class ServersTest(test.TestCase): self.assertNotEqual(res['server']['adminPass'], body['server']['adminPass']) - def test_create_instance_with_admin_pass_v1_1(self): + def test_create_instance_v1_1_admin_pass(self): self._setup_for_create_instance() image_href = 'http://localhost/v1.1/images/2' @@ -866,8 +1005,8 @@ class ServersTest(test.TestCase): body = { 'server': { 'name': 'server_test', - 'imageRef': image_href, - 'flavorRef': flavor_ref, + 'image': {'id': 3}, + 'flavor': {'id': 3}, 'adminPass': 'testpass', }, } @@ -876,20 +1015,22 @@ class ServersTest(test.TestCase): req.method = 'POST' req.body = json.dumps(body) req.headers['content-type'] = "application/json" + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(res.status_int, 200) + server = json.loads(res.body)['server'] self.assertEqual(server['adminPass'], body['server']['adminPass']) - def test_create_instance_with_empty_admin_pass_v1_1(self): + def test_create_instance_v1_1_admin_pass_empty(self): self._setup_for_create_instance() - image_href = 'http://localhost/v1.1/images/2' - flavor_ref = 'http://localhost/v1.1/flavors/3' body = { 'server': { 'name': 'server_test', - 'imageRef': image_href, - 'flavorRef': flavor_ref, + 'image': {'id': 3}, + 'flavor': {'id': 3}, 'adminPass': '', }, } @@ -1644,7 +1785,7 @@ class ServersTest(test.TestCase): self.assertEqual(res_dict['server']['status'], 'SHUTOFF') -class TestServerCreateRequestXMLDeserializer(unittest.TestCase): +class TestServerCreateRequestXMLDeserializerV10(unittest.TestCase): def setUp(self): self.deserializer = create_instance_helper.ServerXMLDeserializer() @@ -1652,7 +1793,7 @@ class TestServerCreateRequestXMLDeserializer(unittest.TestCase): def test_minimal_request(self): serial_request = """ """ + name="new-server-test" imageId="1" flavorId="1" />""" request = self.deserializer.deserialize(serial_request, 'create') expected = {"server": { "name": "new-server-test", @@ -1924,19 +2065,202 @@ b25zLiINCg0KLVJpY2hhcmQgQmFjaA==""", request = self.deserializer.deserialize(serial_request, 'create') self.assertEqual(request['body'], expected) - def test_request_xmlser_with_flavor_image_href(self): + +class TestServerCreateRequestXMLDeserializerV11(unittest.TestCase): + + def setUp(self): + self.deserializer = create_instance_helper.ServerXMLDeserializer() + + def test_minimal_request(self): serial_request = """ - - """ + + + +""" request = self.deserializer.deserialize(serial_request, 'create') - self.assertEquals(request['body']["server"]["flavorRef"], - "http://localhost:8774/v1.1/flavors/1") - self.assertEquals(request['body']["server"]["imageRef"], - "http://localhost:8774/v1.1/images/1") + expected = { + "server": { + "name": "new-server-test", + "image": {"id": "1"}, + "flavor": {"id": "2"}, + }, + } + self.assertEquals(request['body'], expected) + def test_image_link(self): + serial_request = """ + + + + + +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = { + "server": { + "name": "new-server-test", + "image": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": "http://localhost:8774/v1.1/images/2", + }, + ], + }, + "flavor": {"id": "3"}, + }, + } + self.assertEquals(request['body'], expected) + + def test_flavor_link(self): + serial_request = """ + + + + + +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = { + "server": { + "name": "new-server-test", + "image": {"id": "1"}, + "flavor": { + "id": "2", + "links": [ + { + "rel": "bookmark", + "href": "http://localhost:8774/v1.1/flavors/3", + }, + ], + }, + }, + } + self.assertEquals(request['body'], expected) + + def test_empty_metadata_personality(self): + serial_request = """ + + + + + +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = { + "server": { + "name": "new-server-test", + "image": {"id": "1"}, + "flavor": {"id": "2"}, + "metadata": {}, + "personality": [], + }, + } + self.assertEquals(request['body'], expected) + + def test_multiple_metadata_items(self): + serial_request = """ + + + + + two + snack + +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = { + "server": { + "name": "new-server-test", + "image": {"id": "1"}, + "flavor": {"id": "2"}, + "metadata": {"one": "two", "open": "snack"}, + }, + } + self.assertEquals(request['body'], expected) + + def test_multiple_personality_files(self): + serial_request = """ + + + + + MQ== + Mg== + +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = { + "server": { + "name": "new-server-test", + "image": {"id": "1"}, + "flavor": {"id": "2"}, + "personality": [ + {"path": "/etc/banner.txt", "contents": "MQ=="}, + {"path": "/etc/hosts", "contents": "Mg=="}, + ], + }, + } + self.assertEquals(request['body'], expected) + + def test_spec_request(self): + serial_request = """ + + + + + + + + Apache1 + + + Mg== + +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = { + "server": { + "name": "new-server-test", + "image": { + "id": "52415800-8b69-11e0-9b19-734f6f006e54", + "links": [ + { + "rel": "self", + "href": "http://servers.api.openstack.org/" + \ + "v1.1/1234/images/52415800-8b69-11" + \ + "e0-9b19-734f6f006e54", + }, + { + "rel": "bookmark", + "href": "http://servers.api.openstack.org/" + \ + "1234/images/52415800-8b69-11e0-9b" + \ + "19-734f6f006e54", + }, + ], + }, + "flavor": {"id": "52415800-8b69-11e0-9b19-734f1195ff37"}, + "metadata": {"My Server Name": "Apache1"}, + "personality": [ + { + "path": "/etc/banner.txt", + "contents": "Mg==", + }, + ], + }, + } + self.assertEquals(request['body'], expected) class TestServerInstanceCreation(test.TestCase): -- cgit