Merge lp:~rackspace-titan/nova/servers-next into lp:~hudson-openstack/nova/trunk

Proposed by William Wolf
Status: Merged
Approved by: Brian Lamar
Approved revision: 1589
Merged at revision: 1615
Proposed branch: lp:~rackspace-titan/nova/servers-next
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 362 lines (+216/-9)
5 files modified
nova/api/openstack/common.py (+10/-0)
nova/api/openstack/schemas/v1.1/servers_index.rng (+3/-0)
nova/api/openstack/servers.py (+35/-7)
nova/api/openstack/views/servers.py (+36/-1)
nova/tests/api/openstack/test_servers.py (+132/-1)
To merge this branch: bzr merge lp:~rackspace-titan/nova/servers-next
Reviewer Review Type Date Requested Status
Brian Lamar (community) Approve
Brian Waldon (community) Approve
Review via email: mp+75558@code.launchpad.net

Description of the change

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.

To post a comment you must log in.
Revision history for this message
Brian Waldon (bcwaldon) wrote :

When making a request with a changes-since query param such as this:

curl -i -H "X-Auth-Token: admin:admin" 'http://localhost:8774/v1.1/admin/servers?limit=2&changes-since=2011-09-01T00:00:00Z'

The values of changes-since and limit are respected, but the servers_links value returned is unusable:

http://localhost:8774/v1.1?marker=6&limit=2&changes-since=2011-09-01T00%3A00%3A00Z

I would expect the changes-since value not to be url-encoded at all.

review: Needs Fixing
1582. By William Wolf

merge with trunk

1583. By William Wolf

make our own function instead of using urllib.urlencode since we apparently don't suppor urlencoded strings yet

1584. By William Wolf

merge with trunk

Revision history for this message
William Wolf (throughnothing) wrote :

I've replaced urllib with a common function that doesn't urlencode the strings from the dict. I think ultimately we should support urlencoded URL's though, and give those in our own links just to ensure everything is correctly set/interpreted.

Revision history for this message
Brian Waldon (bcwaldon) wrote :

I wouldn't call it elegant, but it definitely works. However, now I'm getting the following url back. Notice the missing 'admin/servers' after 'v1.1':

http://localhost:8774/v1.1?marker=8&limit=1&changes-since=2011-09-01T00:00:00Z

Revision history for this message
William Wolf (throughnothing) wrote :

Wow, good catch waldon. Seems I wasn't even checking that in the tests either. Updated the tests and the code, that should be fixed now.

1585. By William Wolf

remove urllib import

1586. By William Wolf

oops, add project_id and 'servers' to next links

1587. By William Wolf

merged with trunk

1588. By William Wolf

merge from trunk

1589. By William Wolf

pep8 fixes

Revision history for this message
Brian Waldon (bcwaldon) wrote :

Great stuff.

review: Approve
Revision history for this message
Brian Lamar (blamar) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'nova/api/openstack/common.py'
2--- nova/api/openstack/common.py 2011-09-14 15:23:11 +0000
3+++ nova/api/openstack/common.py 2011-09-20 18:47:17 +0000
4@@ -259,6 +259,16 @@
5 headers={'Retry-After': 0})
6
7
8+def dict_to_query_str(params):
9+ # TODO: we should just use urllib.urlencode instead of this
10+ # But currently we don't work with urlencoded url's
11+ param_str = ""
12+ for key, val in params.iteritems():
13+ param_str = param_str + '='.join([str(key), str(val)]) + '&'
14+
15+ return param_str.rstrip('&')
16+
17+
18 class MetadataXMLDeserializer(wsgi.XMLDeserializer):
19
20 def extract_metadata(self, metadata_node):
21
22=== modified file 'nova/api/openstack/schemas/v1.1/servers_index.rng'
23--- nova/api/openstack/schemas/v1.1/servers_index.rng 2011-08-19 19:55:56 +0000
24+++ nova/api/openstack/schemas/v1.1/servers_index.rng 2011-09-20 18:47:17 +0000
25@@ -9,4 +9,7 @@
26 </zeroOrMore>
27 </element>
28 </zeroOrMore>
29+ <zeroOrMore>
30+ <externalRef href="../atom-link.rng"/>
31+ </zeroOrMore>
32 </element>
33
34=== modified file 'nova/api/openstack/servers.py'
35--- nova/api/openstack/servers.py 2011-09-18 21:01:44 +0000
36+++ nova/api/openstack/servers.py 2011-09-20 18:47:17 +0000
37@@ -75,6 +75,9 @@
38 def _build_view(self, req, instance, is_detail=False):
39 raise NotImplementedError()
40
41+ def _build_list(self, req, instances, is_detail=False):
42+ raise NotImplementedError()
43+
44 def _limit_items(self, items, req):
45 raise NotImplementedError()
46
47@@ -130,10 +133,7 @@
48 search_opts=search_opts)
49
50 limited_list = self._limit_items(instance_list, req)
51- servers = [self._build_view(req, inst, is_detail)['server']
52- for inst in limited_list]
53-
54- return dict(servers=servers)
55+ return self._build_list(req, limited_list, is_detail=is_detail)
56
57 @scheduler_api.redirect_handler
58 def show(self, req, id):
59@@ -595,6 +595,11 @@
60 builder = nova.api.openstack.views.servers.ViewBuilderV10(addresses)
61 return builder.build(instance, is_detail=is_detail)
62
63+ def _build_list(self, req, instances, is_detail=False):
64+ addresses = nova.api.openstack.views.addresses.ViewBuilderV10()
65+ builder = nova.api.openstack.views.servers.ViewBuilderV10(addresses)
66+ return builder.build_list(instances, is_detail=is_detail)
67+
68 def _limit_items(self, items, req):
69 return common.limited(items, req)
70
71@@ -692,6 +697,25 @@
72
73 return builder.build(instance, is_detail=is_detail)
74
75+ def _build_list(self, req, instances, is_detail=False):
76+ params = req.GET.copy()
77+ pagination_params = common.get_pagination_params(req)
78+ # Update params with int() values from pagination params
79+ for key, val in pagination_params.iteritems():
80+ params[key] = val
81+
82+ project_id = getattr(req.environ['nova.context'], 'project_id', '')
83+ base_url = req.application_url
84+ flavor_builder = nova.api.openstack.views.flavors.ViewBuilderV11(
85+ base_url, project_id)
86+ image_builder = nova.api.openstack.views.images.ViewBuilderV11(
87+ base_url, project_id)
88+ addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV11()
89+ builder = nova.api.openstack.views.servers.ViewBuilderV11(
90+ addresses_builder, flavor_builder, image_builder,
91+ base_url, project_id)
92+ return builder.build_list(instances, is_detail=is_detail, **params)
93+
94 def _action_change_password(self, input_dict, req, id):
95 context = req.environ['nova.context']
96 if (not 'changePassword' in input_dict
97@@ -939,18 +963,22 @@
98 'security_group')
99 group_elem.set('name', group['name'])
100
101- for link in server_dict.get('links', []):
102- elem = etree.SubElement(server_elem,
103+ self._populate_links(server_elem, server_dict.get('links', []))
104+
105+ def _populate_links(self, parent, links):
106+ for link in links:
107+ elem = etree.SubElement(parent,
108 '{%s}link' % xmlutil.XMLNS_ATOM)
109 elem.set('rel', link['rel'])
110 elem.set('href', link['href'])
111- return server_elem
112
113 def index(self, servers_dict):
114 servers = etree.Element('servers', nsmap=self.NSMAP)
115 for server_dict in servers_dict['servers']:
116 server = etree.SubElement(servers, 'server')
117 self._populate_server(server, server_dict, False)
118+
119+ self._populate_links(servers, servers_dict.get('servers_links', []))
120 return self._to_xml(servers)
121
122 def detail(self, servers_dict):
123
124=== modified file 'nova/api/openstack/views/servers.py'
125--- nova/api/openstack/views/servers.py 2011-09-09 13:48:38 +0000
126+++ nova/api/openstack/views/servers.py 2011-09-20 18:47:17 +0000
127@@ -40,7 +40,7 @@
128 def __init__(self, addresses_builder):
129 self.addresses_builder = addresses_builder
130
131- def build(self, inst, is_detail):
132+ def build(self, inst, is_detail=False):
133 """Return a dict that represenst a server."""
134 if inst.get('_is_precooked', False):
135 server = dict(server=inst)
136@@ -54,6 +54,16 @@
137
138 return server
139
140+ def build_list(self, server_objs, is_detail=False, **kwargs):
141+ limit = kwargs.get('limit', None)
142+ servers = []
143+ servers_links = []
144+
145+ for server_obj in server_objs:
146+ servers.append(self.build(server_obj, is_detail)['server'])
147+
148+ return dict(servers=servers)
149+
150 def _build_simple(self, inst):
151 """Return a simple model of a server."""
152 return dict(server=dict(id=inst['id'], name=inst['display_name']))
153@@ -205,6 +215,31 @@
154
155 response["links"] = links
156
157+ def build_list(self, server_objs, is_detail=False, **kwargs):
158+ limit = kwargs.get('limit', None)
159+ servers = []
160+ servers_links = []
161+
162+ for server_obj in server_objs:
163+ servers.append(self.build(server_obj, is_detail)['server'])
164+
165+ if (len(servers) and limit) and (limit == len(servers)):
166+ next_link = self.generate_next_link(servers[-1]['id'],
167+ kwargs, is_detail)
168+ servers_links = [dict(rel='next', href=next_link)]
169+
170+ reval = dict(servers=servers)
171+ if len(servers_links) > 0:
172+ reval['servers_links'] = servers_links
173+ return reval
174+
175+ def generate_next_link(self, server_id, params, is_detail=False):
176+ """ Return an href string with proper limit and marker params"""
177+ params['marker'] = server_id
178+ return "%s?%s" % (
179+ os.path.join(self.base_url, self.project_id, "servers"),
180+ common.dict_to_query_str(params))
181+
182 def generate_href(self, server_id):
183 """Create an url that refers to a specific server id."""
184 return os.path.join(self.base_url, self.project_id,
185
186=== modified file 'nova/tests/api/openstack/test_servers.py'
187--- nova/tests/api/openstack/test_servers.py 2011-09-19 20:16:49 +0000
188+++ nova/tests/api/openstack/test_servers.py 2011-09-20 18:47:17 +0000
189@@ -20,6 +20,7 @@
190 import datetime
191 import json
192 import unittest
193+import urlparse
194 from lxml import etree
195 from xml.dom import minidom
196
197@@ -1152,6 +1153,67 @@
198 self.assertEqual(res.status_int, 400)
199 self.assertTrue('limit' in res.body)
200
201+ def test_get_servers_with_limit_v1_1(self):
202+ req = webob.Request.blank('/v1.1/fake/servers?limit=3')
203+ res = req.get_response(fakes.wsgi_app())
204+ servers = json.loads(res.body)['servers']
205+ servers_links = json.loads(res.body)['servers_links']
206+ self.assertEqual([s['id'] for s in servers], [0, 1, 2])
207+ self.assertEqual(servers_links[0]['rel'], 'next')
208+
209+ href_parts = urlparse.urlparse(servers_links[0]['href'])
210+ self.assertEqual('/v1.1/fake/servers', href_parts.path)
211+ params = urlparse.parse_qs(href_parts.query)
212+ self.assertDictMatch({'limit': ['3'], 'marker': ['2']}, params)
213+
214+ req = webob.Request.blank('/v1.1/fake/servers?limit=aaa')
215+ res = req.get_response(fakes.wsgi_app())
216+ self.assertEqual(res.status_int, 400)
217+ self.assertTrue('limit' in res.body)
218+
219+ def test_get_server_details_with_limit_v1_1(self):
220+ req = webob.Request.blank('/v1.1/fake/servers/detail?limit=3')
221+ res = req.get_response(fakes.wsgi_app())
222+ servers = json.loads(res.body)['servers']
223+ servers_links = json.loads(res.body)['servers_links']
224+ self.assertEqual([s['id'] for s in servers], [0, 1, 2])
225+ self.assertEqual(servers_links[0]['rel'], 'next')
226+
227+ href_parts = urlparse.urlparse(servers_links[0]['href'])
228+ self.assertEqual('/v1.1/fake/servers', href_parts.path)
229+ params = urlparse.parse_qs(href_parts.query)
230+ self.assertDictMatch({'limit': ['3'], 'marker': ['2']}, params)
231+
232+ req = webob.Request.blank('/v1.1/fake/servers/detail?limit=aaa')
233+ res = req.get_response(fakes.wsgi_app())
234+ self.assertEqual(res.status_int, 400)
235+ self.assertTrue('limit' in res.body)
236+
237+ def test_get_server_details_with_limit_and_other_params_v1_1(self):
238+ req = webob.Request.blank('/v1.1/fake/servers/detail?limit=3&blah=2:t')
239+ res = req.get_response(fakes.wsgi_app())
240+ servers = json.loads(res.body)['servers']
241+ servers_links = json.loads(res.body)['servers_links']
242+ self.assertEqual([s['id'] for s in servers], [0, 1, 2])
243+ self.assertEqual(servers_links[0]['rel'], 'next')
244+
245+ href_parts = urlparse.urlparse(servers_links[0]['href'])
246+ self.assertEqual('/v1.1/fake/servers', href_parts.path)
247+ params = urlparse.parse_qs(href_parts.query)
248+ self.assertDictMatch({'limit': ['3'], 'blah': ['2:t'],
249+ 'marker': ['2']}, params)
250+
251+ req = webob.Request.blank('/v1.1/fake/servers/detail?limit=aaa')
252+ res = req.get_response(fakes.wsgi_app())
253+ self.assertEqual(res.status_int, 400)
254+ self.assertTrue('limit' in res.body)
255+
256+ def test_get_servers_with_too_big_limit_v1_1(self):
257+ req = webob.Request.blank('/v1.1/fake/servers?limit=30')
258+ res = req.get_response(fakes.wsgi_app())
259+ res_dict = json.loads(res.body)
260+ self.assertTrue('servers_links' not in res_dict)
261+
262 def test_get_servers_with_offset(self):
263 req = webob.Request.blank('/v1.0/servers?offset=2')
264 res = req.get_response(fakes.wsgi_app())
265@@ -1952,7 +2014,6 @@
266 req.headers["content-type"] = "application/json"
267
268 res = req.get_response(fakes.wsgi_app())
269- print res
270 self.assertEqual(res.status_int, 202)
271 server = json.loads(res.body)['server']
272 self.assertEqual(1, server['id'])
273@@ -2480,6 +2541,7 @@
274 }
275 req = webob.Request.blank('/v1.1/fake/servers/detail')
276 res = req.get_response(fakes.wsgi_app())
277+ print res.body
278 res_dict = json.loads(res.body)
279
280 for i, s in enumerate(res_dict['servers']):
281@@ -4181,6 +4243,7 @@
282
283 TIMESTAMP = "2010-10-11T10:30:22Z"
284 SERVER_HREF = 'http://localhost/v1.1/servers/123'
285+ SERVER_NEXT = 'http://localhost/v1.1/servers?limit=%s&marker=%s'
286 SERVER_BOOKMARK = 'http://localhost/servers/123'
287 IMAGE_BOOKMARK = 'http://localhost/images/5'
288 FLAVOR_BOOKMARK = 'http://localhost/flavors/1'
289@@ -4599,6 +4662,74 @@
290 for key, value in link.items():
291 self.assertEqual(link_nodes[i].get(key), value)
292
293+ def test_index_with_servers_links(self):
294+ serializer = servers.ServerXMLSerializer()
295+
296+ expected_server_href = 'http://localhost/v1.1/servers/1'
297+ expected_server_next = self.SERVER_NEXT % (2, 2)
298+ expected_server_bookmark = 'http://localhost/servers/1'
299+ expected_server_href_2 = 'http://localhost/v1.1/servers/2'
300+ expected_server_bookmark_2 = 'http://localhost/servers/2'
301+ fixture = {"servers": [
302+ {
303+ "id": 1,
304+ "name": "test_server",
305+ 'links': [
306+ {
307+ 'href': expected_server_href,
308+ 'rel': 'self',
309+ },
310+ {
311+ 'href': expected_server_bookmark,
312+ 'rel': 'bookmark',
313+ },
314+ ],
315+ },
316+ {
317+ "id": 2,
318+ "name": "test_server_2",
319+ 'links': [
320+ {
321+ 'href': expected_server_href_2,
322+ 'rel': 'self',
323+ },
324+ {
325+ 'href': expected_server_bookmark_2,
326+ 'rel': 'bookmark',
327+ },
328+ ],
329+ },
330+ ],
331+ "servers_links": [
332+ {
333+ 'rel': 'next',
334+ 'href': expected_server_next,
335+ },
336+ ]}
337+
338+ output = serializer.serialize(fixture, 'index')
339+ print output
340+ root = etree.XML(output)
341+ xmlutil.validate_schema(root, 'servers_index')
342+ server_elems = root.findall('{0}server'.format(NS))
343+ self.assertEqual(len(server_elems), 2)
344+ for i, server_elem in enumerate(server_elems):
345+ server_dict = fixture['servers'][i]
346+ for key in ['name', 'id']:
347+ self.assertEqual(server_elem.get(key), str(server_dict[key]))
348+
349+ link_nodes = server_elem.findall('{0}link'.format(ATOMNS))
350+ self.assertEqual(len(link_nodes), 2)
351+ for i, link in enumerate(server_dict['links']):
352+ for key, value in link.items():
353+ self.assertEqual(link_nodes[i].get(key), value)
354+
355+ # Check servers_links
356+ servers_links = root.findall('{0}link'.format(ATOMNS))
357+ for i, link in enumerate(fixture['servers_links']):
358+ for key, value in link.items():
359+ self.assertEqual(servers_links[i].get(key), value)
360+
361 def test_detail(self):
362 serializer = servers.ServerXMLSerializer()
363