Merge lp:~rackspace-titan/nova/servers-next into lp:~hudson-openstack/nova/trunk
- servers-next
- Merge into trunk
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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Brian Lamar (community) | Approve | ||
Brian Waldon (community) | Approve | ||
Review via email: mp+75558@code.launchpad.net |
Commit message
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.
- 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
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.
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://
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
Brian Lamar (blamar) : | # |
Preview Diff
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 |
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.