diff options
| author | William Wolf <throughnothing@gmail.com> | 2011-09-22 15:41:34 +0000 |
|---|---|---|
| committer | Tarmac <> | 2011-09-22 15:41:34 +0000 |
| commit | f81b8e1efe0fdce003078b1ae328c7bee18e875d (patch) | |
| tree | fc6f3ba6e516d345a5fed6149500012c70b6405e | |
| parent | 4e85d7555c0b7844c22ed1bc6c8a24d9abe61dc4 (diff) | |
| parent | 2fdc37c21ee9c6533cf7452e4347a9fa9212c31d (diff) | |
Add next links for server lists in OSAPI 1.1. This adds servers_links to the json responses, and an extra atom:link element to the servers node in the xml response.
| -rw-r--r-- | nova/api/openstack/common.py | 10 | ||||
| -rw-r--r-- | nova/api/openstack/schemas/v1.1/servers_index.rng | 3 | ||||
| -rw-r--r-- | nova/api/openstack/servers.py | 42 | ||||
| -rw-r--r-- | nova/api/openstack/views/servers.py | 37 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_servers.py | 133 |
5 files changed, 216 insertions, 9 deletions
diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 3ef9bdee5..726ec4612 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -262,6 +262,16 @@ def check_img_metadata_quota_limit(context, metadata): headers={'Retry-After': 0}) +def dict_to_query_str(params): + # TODO: we should just use urllib.urlencode instead of this + # But currently we don't work with urlencoded url's + param_str = "" + for key, val in params.iteritems(): + param_str = param_str + '='.join([str(key), str(val)]) + '&' + + return param_str.rstrip('&') + + class MetadataXMLDeserializer(wsgi.XMLDeserializer): def extract_metadata(self, metadata_node): diff --git a/nova/api/openstack/schemas/v1.1/servers_index.rng b/nova/api/openstack/schemas/v1.1/servers_index.rng index 768f0912d..023e4b66a 100644 --- a/nova/api/openstack/schemas/v1.1/servers_index.rng +++ b/nova/api/openstack/schemas/v1.1/servers_index.rng @@ -9,4 +9,7 @@ </zeroOrMore> </element> </zeroOrMore> + <zeroOrMore> + <externalRef href="../atom-link.rng"/> + </zeroOrMore> </element> diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 1a4703069..6f55aef9e 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -97,6 +97,9 @@ class Controller(object): def _build_view(self, req, instance, is_detail=False): raise NotImplementedError() + def _build_list(self, req, instances, is_detail=False): + raise NotImplementedError() + def _limit_items(self, items, req): raise NotImplementedError() @@ -152,10 +155,7 @@ class Controller(object): search_opts=search_opts) limited_list = self._limit_items(instance_list, req) - servers = [self._build_view(req, inst, is_detail)['server'] - for inst in limited_list] - - return dict(servers=servers) + return self._build_list(req, limited_list, is_detail=is_detail) @novaclient_exception_converter @scheduler_api.redirect_handler @@ -641,6 +641,11 @@ class ControllerV10(Controller): builder = nova.api.openstack.views.servers.ViewBuilderV10(addresses) return builder.build(instance, is_detail=is_detail) + def _build_list(self, req, instances, is_detail=False): + addresses = nova.api.openstack.views.addresses.ViewBuilderV10() + builder = nova.api.openstack.views.servers.ViewBuilderV10(addresses) + return builder.build_list(instances, is_detail=is_detail) + def _limit_items(self, items, req): return common.limited(items, req) @@ -739,6 +744,25 @@ class ControllerV11(Controller): return builder.build(instance, is_detail=is_detail) + def _build_list(self, req, instances, is_detail=False): + params = req.GET.copy() + pagination_params = common.get_pagination_params(req) + # Update params with int() values from pagination params + for key, val in pagination_params.iteritems(): + params[key] = val + + project_id = getattr(req.environ['nova.context'], 'project_id', '') + base_url = req.application_url + flavor_builder = nova.api.openstack.views.flavors.ViewBuilderV11( + base_url, project_id) + image_builder = nova.api.openstack.views.images.ViewBuilderV11( + base_url, project_id) + addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV11() + builder = nova.api.openstack.views.servers.ViewBuilderV11( + addresses_builder, flavor_builder, image_builder, + base_url, project_id) + return builder.build_list(instances, is_detail=is_detail, **params) + def _action_change_password(self, input_dict, req, id): context = req.environ['nova.context'] if (not 'changePassword' in input_dict @@ -986,18 +1010,22 @@ class ServerXMLSerializer(wsgi.XMLDictSerializer): 'security_group') group_elem.set('name', group['name']) - for link in server_dict.get('links', []): - elem = etree.SubElement(server_elem, + self._populate_links(server_elem, server_dict.get('links', [])) + + def _populate_links(self, parent, links): + for link in links: + elem = etree.SubElement(parent, '{%s}link' % xmlutil.XMLNS_ATOM) elem.set('rel', link['rel']) elem.set('href', link['href']) - return server_elem def index(self, servers_dict): servers = etree.Element('servers', nsmap=self.NSMAP) for server_dict in servers_dict['servers']: server = etree.SubElement(servers, 'server') self._populate_server(server, server_dict, False) + + self._populate_links(servers, servers_dict.get('servers_links', [])) return self._to_xml(servers) def detail(self, servers_dict): diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 925668e56..f3666eb6b 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -40,7 +40,7 @@ class ViewBuilder(object): def __init__(self, addresses_builder): self.addresses_builder = addresses_builder - def build(self, inst, is_detail): + def build(self, inst, is_detail=False): """Return a dict that represenst a server.""" if inst.get('_is_precooked', False): server = dict(server=inst) @@ -54,6 +54,16 @@ class ViewBuilder(object): return server + def build_list(self, server_objs, is_detail=False, **kwargs): + limit = kwargs.get('limit', None) + servers = [] + servers_links = [] + + for server_obj in server_objs: + servers.append(self.build(server_obj, is_detail)['server']) + + return dict(servers=servers) + def _build_simple(self, inst): """Return a simple model of a server.""" return dict(server=dict(id=inst['id'], name=inst['display_name'])) @@ -205,6 +215,31 @@ class ViewBuilderV11(ViewBuilder): response["links"] = links + def build_list(self, server_objs, is_detail=False, **kwargs): + limit = kwargs.get('limit', None) + servers = [] + servers_links = [] + + for server_obj in server_objs: + servers.append(self.build(server_obj, is_detail)['server']) + + if (len(servers) and limit) and (limit == len(servers)): + next_link = self.generate_next_link(servers[-1]['id'], + kwargs, is_detail) + servers_links = [dict(rel='next', href=next_link)] + + reval = dict(servers=servers) + if len(servers_links) > 0: + reval['servers_links'] = servers_links + return reval + + def generate_next_link(self, server_id, params, is_detail=False): + """ Return an href string with proper limit and marker params""" + params['marker'] = server_id + return "%s?%s" % ( + os.path.join(self.base_url, self.project_id, "servers"), + common.dict_to_query_str(params)) + def generate_href(self, server_id): """Create an url that refers to a specific server id.""" return os.path.join(self.base_url, self.project_id, diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index b83aad49f..107e332d1 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -20,6 +20,7 @@ import base64 import datetime import json import unittest +import urlparse from lxml import etree from xml.dom import minidom @@ -1154,6 +1155,67 @@ class ServersTest(test.TestCase): self.assertEqual(res.status_int, 400) self.assertTrue('limit' in res.body) + def test_get_servers_with_limit_v1_1(self): + req = webob.Request.blank('/v1.1/fake/servers?limit=3') + res = req.get_response(fakes.wsgi_app()) + servers = json.loads(res.body)['servers'] + servers_links = json.loads(res.body)['servers_links'] + self.assertEqual([s['id'] for s in servers], [0, 1, 2]) + self.assertEqual(servers_links[0]['rel'], 'next') + + href_parts = urlparse.urlparse(servers_links[0]['href']) + self.assertEqual('/v1.1/fake/servers', href_parts.path) + params = urlparse.parse_qs(href_parts.query) + self.assertDictMatch({'limit': ['3'], 'marker': ['2']}, params) + + req = webob.Request.blank('/v1.1/fake/servers?limit=aaa') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + self.assertTrue('limit' in res.body) + + def test_get_server_details_with_limit_v1_1(self): + req = webob.Request.blank('/v1.1/fake/servers/detail?limit=3') + res = req.get_response(fakes.wsgi_app()) + servers = json.loads(res.body)['servers'] + servers_links = json.loads(res.body)['servers_links'] + self.assertEqual([s['id'] for s in servers], [0, 1, 2]) + self.assertEqual(servers_links[0]['rel'], 'next') + + href_parts = urlparse.urlparse(servers_links[0]['href']) + self.assertEqual('/v1.1/fake/servers', href_parts.path) + params = urlparse.parse_qs(href_parts.query) + self.assertDictMatch({'limit': ['3'], 'marker': ['2']}, params) + + req = webob.Request.blank('/v1.1/fake/servers/detail?limit=aaa') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + self.assertTrue('limit' in res.body) + + def test_get_server_details_with_limit_and_other_params_v1_1(self): + req = webob.Request.blank('/v1.1/fake/servers/detail?limit=3&blah=2:t') + res = req.get_response(fakes.wsgi_app()) + servers = json.loads(res.body)['servers'] + servers_links = json.loads(res.body)['servers_links'] + self.assertEqual([s['id'] for s in servers], [0, 1, 2]) + self.assertEqual(servers_links[0]['rel'], 'next') + + href_parts = urlparse.urlparse(servers_links[0]['href']) + self.assertEqual('/v1.1/fake/servers', href_parts.path) + params = urlparse.parse_qs(href_parts.query) + self.assertDictMatch({'limit': ['3'], 'blah': ['2:t'], + 'marker': ['2']}, params) + + req = webob.Request.blank('/v1.1/fake/servers/detail?limit=aaa') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + self.assertTrue('limit' in res.body) + + def test_get_servers_with_too_big_limit_v1_1(self): + req = webob.Request.blank('/v1.1/fake/servers?limit=30') + res = req.get_response(fakes.wsgi_app()) + res_dict = json.loads(res.body) + self.assertTrue('servers_links' not in res_dict) + def test_get_servers_with_offset(self): req = webob.Request.blank('/v1.0/servers?offset=2') res = req.get_response(fakes.wsgi_app()) @@ -1955,7 +2017,6 @@ class ServersTest(test.TestCase): req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) - print res self.assertEqual(res.status_int, 202) server = json.loads(res.body)['server'] self.assertEqual(1, server['id']) @@ -2483,6 +2544,7 @@ class ServersTest(test.TestCase): } req = webob.Request.blank('/v1.1/fake/servers/detail') res = req.get_response(fakes.wsgi_app()) + print res.body res_dict = json.loads(res.body) for i, s in enumerate(res_dict['servers']): @@ -4186,6 +4248,7 @@ class ServerXMLSerializationTest(test.TestCase): TIMESTAMP = "2010-10-11T10:30:22Z" SERVER_HREF = 'http://localhost/v1.1/servers/123' + SERVER_NEXT = 'http://localhost/v1.1/servers?limit=%s&marker=%s' SERVER_BOOKMARK = 'http://localhost/servers/123' IMAGE_BOOKMARK = 'http://localhost/images/5' FLAVOR_BOOKMARK = 'http://localhost/flavors/1' @@ -4604,6 +4667,74 @@ class ServerXMLSerializationTest(test.TestCase): for key, value in link.items(): self.assertEqual(link_nodes[i].get(key), value) + def test_index_with_servers_links(self): + serializer = servers.ServerXMLSerializer() + + expected_server_href = 'http://localhost/v1.1/servers/1' + expected_server_next = self.SERVER_NEXT % (2, 2) + expected_server_bookmark = 'http://localhost/servers/1' + expected_server_href_2 = 'http://localhost/v1.1/servers/2' + expected_server_bookmark_2 = 'http://localhost/servers/2' + fixture = {"servers": [ + { + "id": 1, + "name": "test_server", + 'links': [ + { + 'href': expected_server_href, + 'rel': 'self', + }, + { + 'href': expected_server_bookmark, + 'rel': 'bookmark', + }, + ], + }, + { + "id": 2, + "name": "test_server_2", + 'links': [ + { + 'href': expected_server_href_2, + 'rel': 'self', + }, + { + 'href': expected_server_bookmark_2, + 'rel': 'bookmark', + }, + ], + }, + ], + "servers_links": [ + { + 'rel': 'next', + 'href': expected_server_next, + }, + ]} + + output = serializer.serialize(fixture, 'index') + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'servers_index') + server_elems = root.findall('{0}server'.format(NS)) + self.assertEqual(len(server_elems), 2) + for i, server_elem in enumerate(server_elems): + server_dict = fixture['servers'][i] + for key in ['name', 'id']: + self.assertEqual(server_elem.get(key), str(server_dict[key])) + + link_nodes = server_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(server_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + # Check servers_links + servers_links = root.findall('{0}link'.format(ATOMNS)) + for i, link in enumerate(fixture['servers_links']): + for key, value in link.items(): + self.assertEqual(servers_links[i].get(key), value) + def test_detail(self): serializer = servers.ServerXMLSerializer() |
