summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWilliam Wolf <throughnothing@gmail.com>2011-09-22 15:41:34 +0000
committerTarmac <>2011-09-22 15:41:34 +0000
commitf81b8e1efe0fdce003078b1ae328c7bee18e875d (patch)
treefc6f3ba6e516d345a5fed6149500012c70b6405e
parent4e85d7555c0b7844c22ed1bc6c8a24d9abe61dc4 (diff)
parent2fdc37c21ee9c6533cf7452e4347a9fa9212c31d (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.py10
-rw-r--r--nova/api/openstack/schemas/v1.1/servers_index.rng3
-rw-r--r--nova/api/openstack/servers.py42
-rw-r--r--nova/api/openstack/views/servers.py37
-rw-r--r--nova/tests/api/openstack/test_servers.py133
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()