Merge lp:~allenap/maas/fix-resource-uri-model-mismatch into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 5598
Proposed branch: lp:~allenap/maas/fix-resource-uri-model-mismatch
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 899 lines (+203/-60)
38 files modified
src/maasserver/api/bcache.py (+7/-1)
src/maasserver/api/bcache_cacheset.py (+7/-1)
src/maasserver/api/blockdevices.py (+6/-1)
src/maasserver/api/boot_source_selections.py (+1/-0)
src/maasserver/api/dhcpsnippets.py (+1/-1)
src/maasserver/api/dnsresourcerecords.py (+1/-1)
src/maasserver/api/dnsresources.py (+1/-1)
src/maasserver/api/domains.py (+1/-1)
src/maasserver/api/fabrics.py (+1/-1)
src/maasserver/api/fannetworks.py (+1/-1)
src/maasserver/api/interfaces.py (+7/-1)
src/maasserver/api/ipranges.py (+1/-1)
src/maasserver/api/machines.py (+3/-2)
src/maasserver/api/nodes.py (+2/-0)
src/maasserver/api/packagerepositories.py (+1/-1)
src/maasserver/api/partitions.py (+12/-1)
src/maasserver/api/raid.py (+7/-1)
src/maasserver/api/spaces.py (+1/-1)
src/maasserver/api/ssh_keys.py (+1/-1)
src/maasserver/api/ssl_keys.py (+1/-1)
src/maasserver/api/staticroutes.py (+1/-1)
src/maasserver/api/subnets.py (+1/-1)
src/maasserver/api/support.py (+37/-0)
src/maasserver/api/tests/test_bcache.py (+1/-0)
src/maasserver/api/tests/test_bcache_cacheset.py (+1/-0)
src/maasserver/api/tests/test_blockdevice.py (+12/-0)
src/maasserver/api/tests/test_dhcpsnippets.py (+35/-16)
src/maasserver/api/tests/test_interfaces.py (+1/-0)
src/maasserver/api/tests/test_partitions.py (+7/-3)
src/maasserver/api/tests/test_raid.py (+1/-0)
src/maasserver/api/tests/test_user.py (+7/-1)
src/maasserver/api/tests/test_vlans.py (+1/-0)
src/maasserver/api/tests/test_volume_groups.py (+1/-0)
src/maasserver/api/users.py (+5/-0)
src/maasserver/api/vlans.py (+1/-0)
src/maasserver/api/volume_groups.py (+10/-1)
src/metadataserver/api.py (+17/-17)
src/metadataserver/tests/test_api.py (+1/-1)
To merge this branch: bzr merge lp:~allenap/maas/fix-resource-uri-model-mismatch
Reviewer Review Type Date Requested Status
Mike Pontillo (community) Approve
Review via email: mp+312632@code.launchpad.net

Commit message

Ensure that all Web API handlers render the fields required to reconstruct a self-referential URI.

In addition, make it an error to define a handler that omits the required fields.

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

You'll have to excuse my ignorance on this one.

How did you test this change and what was the requirement to drive this?

For the vast majority of these changes, I don't see any obviously-modified unit tests that would cover the changes.

Are there any potential side effects (such as API compatibility, and/or integration with other products) we need to watch out for if we rename 'fields' to 'subfields'?

review: Needs Information
Revision history for this message
Mike Pontillo (mpontillo) wrote :

I went back and read the bug, so I now better-understand the need for this change.

But I sill am not sure if current testing covers all these changes; thoughts?

Revision history for this message
Gavin Panella (allenap) wrote :

> Are there any potential side effects (such as API compatibility,
> and/or integration with other products) we need to watch out for if we
> rename 'fields' to 'subfields'?

No, there shouldn't be. That particular handler renders the response
itself so doesn't trigger Piston's emitters. The use of `fields` was an
unfortunate naming clash, but it was never noticed because only an
emitter would use it.

> For the vast majority of these changes, I don't see any
> obviously-modified unit tests that would cover the changes.
...
> I still am not sure if current testing covers all these changes;
> thoughts?

You're right. The has_fields/has_resource_uri checks ensure that we're
asking Piston to emit all of the fields we need, the lack of failures in
tests imply that these fields are being correctly rendered, but there
aren't any tests to check that those fields contain the right
information. I'll add those.

Thanks!

Revision history for this message
Mike Pontillo (mpontillo) wrote :

> No, there shouldn't be. That particular handler renders the response
> itself so doesn't trigger Piston's emitters. The use of `fields` was an
> unfortunate naming clash, but it was never noticed because only an
> emitter would use it.

Ah. Some comments around this would be nice, for future readers.

Revision history for this message
Gavin Panella (allenap) wrote :

Tests added, and a couple of DHCPSnippet tests fixed (they were using assertItemsEqual to compare dicts, so were in fact only comparing keys). Please take another look!

Revision history for this message
Mike Pontillo (mpontillo) wrote :

Thanks for the fixes!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/bcache.py'
2--- src/maasserver/api/bcache.py 2016-03-28 13:54:47 +0000
3+++ src/maasserver/api/bcache.py 2016-12-08 16:59:50 +0000
4@@ -25,6 +25,7 @@
5
6
7 DISPLAYED_BCACHE_FIELDS = (
8+ 'system_id',
9 'id',
10 'uuid',
11 'name',
12@@ -96,7 +97,7 @@
13 def resource_uri(cls, bcache=None):
14 # See the comment in NodeHandler.resource_uri.
15 system_id = "system_id"
16- bcache_id = "bcache_id"
17+ bcache_id = "id"
18 if bcache is not None:
19 bcache_id = bcache.id
20 node = bcache.get_node()
21@@ -105,6 +106,11 @@
22 return ('bcache_device_handler', (system_id, bcache_id))
23
24 @classmethod
25+ def system_id(cls, bcache):
26+ node = bcache.get_node()
27+ return None if node is None else node.system_id
28+
29+ @classmethod
30 def size(cls, bcache):
31 return bcache.get_size()
32
33
34=== modified file 'src/maasserver/api/bcache_cacheset.py'
35--- src/maasserver/api/bcache_cacheset.py 2016-03-28 13:54:47 +0000
36+++ src/maasserver/api/bcache_cacheset.py 2016-12-08 16:59:50 +0000
37@@ -25,6 +25,7 @@
38
39
40 DISPLAYED_CACHE_SET_FIELDS = (
41+ 'system_id',
42 'id',
43 'name',
44 'cache_device',
45@@ -85,7 +86,7 @@
46 def resource_uri(cls, cache_set=None):
47 # See the comment in NodeHandler.resource_uri.
48 system_id = "system_id"
49- cache_set_id = "cache_set_id"
50+ cache_set_id = "id"
51 if cache_set is not None:
52 cache_set_id = cache_set.id
53 node = cache_set.get_node()
54@@ -94,6 +95,11 @@
55 return ('bcache_cache_set_handler', (system_id, cache_set_id))
56
57 @classmethod
58+ def system_id(cls, cache_set):
59+ node = cache_set.get_node()
60+ return None if node is None else node.system_id
61+
62+ @classmethod
63 def cache_device(cls, cache_set):
64 """Return the cache device for this cache set."""
65 return cache_set.get_device()
66
67=== modified file 'src/maasserver/api/blockdevices.py'
68--- src/maasserver/api/blockdevices.py 2016-10-20 16:04:24 +0000
69+++ src/maasserver/api/blockdevices.py 2016-12-08 16:59:50 +0000
70@@ -38,6 +38,7 @@
71
72
73 DISPLAYED_BLOCKDEVICE_FIELDS = (
74+ 'system_id',
75 'id',
76 'name',
77 'uuid',
78@@ -125,13 +126,17 @@
79 # See the comment in NodeHandler.resource_uri.
80 if block_device is None:
81 system_id = "system_id"
82- device_id = "device_id"
83+ device_id = "id"
84 else:
85 device_id = block_device.id
86 system_id = block_device.node.system_id
87 return ('blockdevice_handler', (system_id, device_id))
88
89 @classmethod
90+ def system_id(cls, block_device):
91+ return block_device.node.system_id
92+
93+ @classmethod
94 def name(cls, block_device):
95 return block_device.actual_instance.get_name()
96
97
98=== modified file 'src/maasserver/api/boot_source_selections.py'
99--- src/maasserver/api/boot_source_selections.py 2016-04-12 17:36:35 +0000
100+++ src/maasserver/api/boot_source_selections.py 2016-12-08 16:59:50 +0000
101@@ -21,6 +21,7 @@
102
103
104 DISPLAYED_BOOTSOURCESELECTION_FIELDS = (
105+ 'boot_source_id',
106 'id',
107 'os',
108 'release',
109
110=== modified file 'src/maasserver/api/dhcpsnippets.py'
111--- src/maasserver/api/dhcpsnippets.py 2016-04-11 16:23:26 +0000
112+++ src/maasserver/api/dhcpsnippets.py 2016-12-08 16:59:50 +0000
113@@ -48,7 +48,7 @@
114 if dhcp_snippet is not None:
115 dhcp_snippet_id = dhcp_snippet.id
116 else:
117- dhcp_snippet_id = "dhcp_snippet_id"
118+ dhcp_snippet_id = "id"
119 return ('dhcp_snippet_handler', (dhcp_snippet_id,))
120
121 @classmethod
122
123=== modified file 'src/maasserver/api/dnsresourcerecords.py'
124--- src/maasserver/api/dnsresourcerecords.py 2016-03-28 13:54:47 +0000
125+++ src/maasserver/api/dnsresourcerecords.py 2016-12-08 16:59:50 +0000
126@@ -153,7 +153,7 @@
127 @classmethod
128 def resource_uri(cls, dnsresourcerecord=None):
129 # See the comment in NodeHandler.resource_uri.
130- dnsresourcerecord_id = "dnsresourcerecord_id"
131+ dnsresourcerecord_id = "id"
132 if dnsresourcerecord is not None:
133 dnsresourcerecord_id = dnsresourcerecord.id
134 return ('dnsresourcerecord_handler', (dnsresourcerecord_id,))
135
136=== modified file 'src/maasserver/api/dnsresources.py'
137--- src/maasserver/api/dnsresources.py 2016-03-28 13:54:47 +0000
138+++ src/maasserver/api/dnsresources.py 2016-12-08 16:59:50 +0000
139@@ -124,7 +124,7 @@
140 @classmethod
141 def resource_uri(cls, dnsresource=None):
142 # See the comment in NodeHandler.resource_uri.
143- dnsresource_id = "dnsresource_id"
144+ dnsresource_id = "id"
145 if dnsresource is not None:
146 dnsresource_id = dnsresource.id
147 return ('dnsresource_handler', (dnsresource_id,))
148
149=== modified file 'src/maasserver/api/domains.py'
150--- src/maasserver/api/domains.py 2016-06-14 18:31:07 +0000
151+++ src/maasserver/api/domains.py 2016-12-08 16:59:50 +0000
152@@ -96,7 +96,7 @@
153 @classmethod
154 def resource_uri(cls, domain=None):
155 # See the comment in NodeHandler.resource_uri.
156- domain_id = "domain_id"
157+ domain_id = "id"
158 if domain is not None:
159 domain_id = domain.id
160 return ('domain_handler', (domain_id,))
161
162=== modified file 'src/maasserver/api/fabrics.py'
163--- src/maasserver/api/fabrics.py 2016-04-27 20:18:20 +0000
164+++ src/maasserver/api/fabrics.py 2016-12-08 16:59:50 +0000
165@@ -62,7 +62,7 @@
166 @classmethod
167 def resource_uri(cls, fabric=None):
168 # See the comment in NodeHandler.resource_uri.
169- fabric_id = "fabric_id"
170+ fabric_id = "id"
171 if fabric is not None:
172 fabric_id = fabric.id
173 return ('fabric_handler', (fabric_id,))
174
175=== modified file 'src/maasserver/api/fannetworks.py'
176--- src/maasserver/api/fannetworks.py 2016-03-28 13:54:47 +0000
177+++ src/maasserver/api/fannetworks.py 2016-12-08 16:59:50 +0000
178@@ -70,7 +70,7 @@
179 @classmethod
180 def resource_uri(cls, fannetwork=None):
181 # See the comment in NodeHandler.resource_uri.
182- fannetwork_id = "fannetwork_id"
183+ fannetwork_id = "id"
184 if fannetwork is not None:
185 fannetwork_id = fannetwork.id
186 return ('fannetwork_handler', (fannetwork_id,))
187
188=== modified file 'src/maasserver/api/interfaces.py'
189--- src/maasserver/api/interfaces.py 2016-12-07 12:46:14 +0000
190+++ src/maasserver/api/interfaces.py 2016-12-08 16:59:50 +0000
191@@ -55,6 +55,7 @@
192 BLANK_FIELD = "This field cannot be blank."
193
194 DISPLAYED_INTERFACE_FIELDS = (
195+ 'system_id',
196 'id',
197 'name',
198 'type',
199@@ -340,7 +341,7 @@
200 def resource_uri(cls, interface=None):
201 # See the comment in NodeHandler.resource_uri.
202 system_id = "system_id"
203- interface_id = "interface_id"
204+ interface_id = "id"
205 if interface is not None:
206 interface_id = interface.id
207 node = interface.get_node()
208@@ -349,6 +350,11 @@
209 return ('interface_handler', (system_id, interface_id))
210
211 @classmethod
212+ def system_id(cls, interface):
213+ node = interface.get_node()
214+ return None if node is None else node.system_id
215+
216+ @classmethod
217 def mac_address(cls, interface):
218 if interface.mac_address is not None:
219 return "%s" % interface.mac_address
220
221=== modified file 'src/maasserver/api/ipranges.py'
222--- src/maasserver/api/ipranges.py 2016-10-17 20:45:44 +0000
223+++ src/maasserver/api/ipranges.py 2016-12-08 16:59:50 +0000
224@@ -81,7 +81,7 @@
225 @classmethod
226 def resource_uri(cls, iprange=None):
227 # See the comment in NodeHandler.resource_uri.
228- iprange_id = "iprange_id"
229+ iprange_id = "id"
230 if iprange is not None:
231 iprange_id = iprange.id
232 return ('iprange_handler', (iprange_id,))
233
234=== modified file 'src/maasserver/api/machines.py'
235--- src/maasserver/api/machines.py 2016-10-28 21:36:43 +0000
236+++ src/maasserver/api/machines.py 2016-12-08 16:59:50 +0000
237@@ -811,8 +811,9 @@
238 fields = DISPLAYED_ANON_MACHINE_FIELDS
239
240 @classmethod
241- def resource_uri(cls, machine):
242- return ('machine_handler', (machine.system_id, ))
243+ def resource_uri(cls, machine=None):
244+ system_id = "system_id" if machine is None else machine.system_id
245+ return ('machine_handler', (system_id, ))
246
247
248 class AnonMachinesHandler(AnonNodesHandler):
249
250=== modified file 'src/maasserver/api/nodes.py'
251--- src/maasserver/api/nodes.py 2016-12-07 12:46:14 +0000
252+++ src/maasserver/api/nodes.py 2016-12-08 16:59:50 +0000
253@@ -246,6 +246,8 @@
254 read = create = update = delete = None
255 model = Node
256
257+ resource_uri = NodeHandler.resource_uri
258+
259
260 class AnonNodesHandler(AnonymousOperationsHandler):
261 """Anonymous access to Nodes."""
262
263=== modified file 'src/maasserver/api/packagerepositories.py'
264--- src/maasserver/api/packagerepositories.py 2016-08-18 15:42:50 +0000
265+++ src/maasserver/api/packagerepositories.py 2016-12-08 16:59:50 +0000
266@@ -45,7 +45,7 @@
267 if package_repository is not None:
268 package_repository_id = package_repository.id
269 else:
270- package_repository_id = "package_repository_id"
271+ package_repository_id = "id"
272 return ('package_repository_handler', (package_repository_id,))
273
274 def read(self, request, package_repository_id):
275
276=== modified file 'src/maasserver/api/partitions.py'
277--- src/maasserver/api/partitions.py 2016-03-28 13:54:47 +0000
278+++ src/maasserver/api/partitions.py 2016-12-08 16:59:50 +0000
279@@ -33,6 +33,8 @@
280
281
282 DISPLAYED_PARTITION_FIELDS = (
283+ 'system_id',
284+ 'device_id',
285 'id',
286 'uuid',
287 'path',
288@@ -145,7 +147,7 @@
289 if partition is None:
290 system_id = "system_id"
291 device_id = "device_id"
292- partition_id = "partition_id"
293+ partition_id = "id"
294 else:
295 partition_id = partition.id
296 block_device = partition.partition_table.block_device
297@@ -154,6 +156,15 @@
298 return (
299 'partition_handler', (system_id, device_id, partition_id))
300
301+ @classmethod
302+ def system_id(cls, partition):
303+ block_device = partition.partition_table.block_device
304+ return block_device.node.system_id
305+
306+ @classmethod
307+ def device_id(cls, partition):
308+ return partition.partition_table.block_device.id
309+
310 def read(self, request, system_id, device_id, partition_id):
311 """Read partition.
312
313
314=== modified file 'src/maasserver/api/raid.py'
315--- src/maasserver/api/raid.py 2016-03-28 13:54:47 +0000
316+++ src/maasserver/api/raid.py 2016-12-08 16:59:50 +0000
317@@ -26,6 +26,7 @@
318
319
320 DISPLAYED_RAID_FIELDS = (
321+ 'system_id',
322 'id',
323 'uuid',
324 'name',
325@@ -95,7 +96,7 @@
326 def resource_uri(cls, raid=None):
327 # See the comment in NodeHandler.resource_uri.
328 system_id = "system_id"
329- raid_id = "raid_id"
330+ raid_id = "id"
331 if raid is not None:
332 raid_id = raid.id
333 node = raid.get_node()
334@@ -104,6 +105,11 @@
335 return ('raid_device_handler', (system_id, raid_id))
336
337 @classmethod
338+ def system_id(cls, raid):
339+ node = raid.get_node()
340+ return None if node is None else node.system_id
341+
342+ @classmethod
343 def level(cls, raid):
344 """Return the level of RAID for device."""
345 # group_type holds the raid level, we just expose it over the API
346
347=== modified file 'src/maasserver/api/spaces.py'
348--- src/maasserver/api/spaces.py 2016-12-06 13:53:47 +0000
349+++ src/maasserver/api/spaces.py 2016-12-08 16:59:50 +0000
350@@ -60,7 +60,7 @@
351 @classmethod
352 def resource_uri(cls, space=None):
353 # See the comment in NodeHandler.resource_uri.
354- space_id = "space_id"
355+ space_id = "id"
356 if space is not None:
357 space_id = space.id
358 return ('space_handler', (space_id,))
359
360=== modified file 'src/maasserver/api/ssh_keys.py'
361--- src/maasserver/api/ssh_keys.py 2016-09-23 19:21:31 +0000
362+++ src/maasserver/api/ssh_keys.py 2016-12-08 16:59:50 +0000
363@@ -141,7 +141,7 @@
364
365 @classmethod
366 def resource_uri(cls, sshkey=None):
367- keyid = "keyid"
368+ keyid = "id"
369 if sshkey is not None:
370 keyid = sshkey.id
371 return ('sshkey_handler', (keyid, ))
372
373=== modified file 'src/maasserver/api/ssl_keys.py'
374--- src/maasserver/api/ssl_keys.py 2016-04-11 16:23:26 +0000
375+++ src/maasserver/api/ssl_keys.py 2016-12-08 16:59:50 +0000
376@@ -102,7 +102,7 @@
377
378 @classmethod
379 def resource_uri(cls, sslkey=None):
380- keyid = "keyid"
381+ keyid = "id"
382 if sslkey is not None:
383 keyid = sslkey.id
384 return ('sslkey_handler', (keyid, ))
385
386=== modified file 'src/maasserver/api/staticroutes.py'
387--- src/maasserver/api/staticroutes.py 2016-08-17 15:29:26 +0000
388+++ src/maasserver/api/staticroutes.py 2016-12-08 16:59:50 +0000
389@@ -64,7 +64,7 @@
390 @classmethod
391 def resource_uri(cls, staticroute=None):
392 # See the comment in NodeHandler.resource_uri.
393- staticroute_id = "staticroute_id"
394+ staticroute_id = "id"
395 if staticroute is not None:
396 staticroute_id = staticroute.id
397 return ('staticroute_handler', (staticroute_id,))
398
399=== modified file 'src/maasserver/api/subnets.py'
400--- src/maasserver/api/subnets.py 2016-12-07 12:46:14 +0000
401+++ src/maasserver/api/subnets.py 2016-12-08 16:59:50 +0000
402@@ -138,7 +138,7 @@
403 @classmethod
404 def resource_uri(cls, subnet=None):
405 # See the comment in NodeHandler.resource_uri.
406- subnet_id = "subnet_id"
407+ subnet_id = "id"
408 if subnet is not None:
409 subnet_id = subnet.id
410 return ('subnet_handler', (subnet_id,))
411
412=== modified file 'src/maasserver/api/support.py'
413--- src/maasserver/api/support.py 2016-05-20 14:31:34 +0000
414+++ src/maasserver/api/support.py 2016-12-08 16:59:50 +0000
415@@ -28,6 +28,10 @@
416 HttpStatusCode,
417 rc,
418 )
419+from provisioningserver.logger import LegacyLogger
420+
421+
422+log = LegacyLogger()
423
424
425 class OperationsResource(Resource):
426@@ -225,9 +229,42 @@
427 cls.allowed_methods = frozenset(
428 http_method for http_method, name in exports)
429
430+ # Flags used later.
431+ has_fields = cls.fields is not BaseHandler.fields
432+ has_resource_uri = hasattr(cls, "resource_uri")
433+ is_internal_only = cls.__module__ in {__name__, "metadataserver.api"}
434+
435+ # Reject handlers which omit fields required for self-referential
436+ # URIs. See bug 1643552. We ignore handlers that don't define `fields`
437+ # because we assume they are doing custom object rendering and we have
438+ # no way to check here for compliance.
439+ if has_fields and has_resource_uri:
440+ _, uri_params, *_ = cls.resource_uri()
441+ missing = set(uri_params).difference(cls.fields)
442+ if len(missing) != 0:
443+ raise OperationsHandlerMisconfigured(
444+ "{handler.__module__}.{handler.__name__} does not render "
445+ "all fields required to construct a self-referential URI. "
446+ "Fields missing: {missing}.".format(
447+ handler=cls, missing=" ".join(sorted(missing))))
448+
449+ # Piston uses `resource_uri` even for handlers without models in order
450+ # to generate documentation. We ignore those modules we consider "for
451+ # internal use only" since we do not intend to generate documentation
452+ # for these.
453+ if not has_resource_uri and not is_internal_only:
454+ log.warn(
455+ "{handler.__module__}.{handler.__name__} does not have "
456+ "`resource_uri`. This means it may be omitted from generated "
457+ "documentation. Please investigate.", handler=cls)
458+
459 return cls
460
461
462+class OperationsHandlerMisconfigured(Exception):
463+ """Handler has been misconfigured; see the error message for details."""
464+
465+
466 class OperationsHandlerMixin:
467 """Handler mixin for operations dispatch.
468
469
470=== modified file 'src/maasserver/api/tests/test_bcache.py'
471--- src/maasserver/api/tests/test_bcache.py 2016-05-24 21:29:53 +0000
472+++ src/maasserver/api/tests/test_bcache.py 2016-12-08 16:59:50 +0000
473@@ -228,6 +228,7 @@
474 "backing_device": ContainsDict({
475 "id": Equals(backing_block_device.id),
476 }),
477+ "system_id": Equals(bcache.get_node().system_id),
478 }))
479
480 def test_read_404_when_not_bcache(self):
481
482=== modified file 'src/maasserver/api/tests/test_bcache_cacheset.py'
483--- src/maasserver/api/tests/test_bcache_cacheset.py 2016-05-24 22:05:45 +0000
484+++ src/maasserver/api/tests/test_bcache_cacheset.py 2016-12-08 16:59:50 +0000
485@@ -149,6 +149,7 @@
486 "cache_device": ContainsDict({
487 "id": Equals(cache_block_device.id),
488 }),
489+ "system_id": Equals(cache_set.get_node().system_id),
490 }))
491
492 def test_read_404_when_invalid_id(self):
493
494=== modified file 'src/maasserver/api/tests/test_blockdevice.py'
495--- src/maasserver/api/tests/test_blockdevice.py 2016-10-20 16:04:24 +0000
496+++ src/maasserver/api/tests/test_blockdevice.py 2016-12-08 16:59:50 +0000
497@@ -18,6 +18,7 @@
498 from maasserver.models.blockdevice import MIN_BLOCK_DEVICE_SIZE
499 from maasserver.testing.api import APITestCase
500 from maasserver.testing.factory import factory
501+from maasserver.testing.matchers import HasStatusCode
502 from maasserver.utils.converters import json_load_bytes
503 from maasserver.utils.orm import reload_object
504 from testtools.matchers import (
505@@ -383,6 +384,17 @@
506 "mount_options": filesystem2.mount_options,
507 }, parsed_device['partitions'][1]['filesystem'])
508
509+ def test_read_returns_system_id(self):
510+ node = factory.make_Node()
511+ block_device = factory.make_PhysicalBlockDevice(node=node)
512+ uri = get_blockdevice_uri(block_device)
513+ response = self.client.get(uri)
514+ self.assertThat(response, HasStatusCode(http.client.OK))
515+ parsed_device = json_load_bytes(response.content)
516+ self.assertThat(parsed_device, ContainsDict({
517+ "system_id": Equals(node.system_id),
518+ }))
519+
520 def test_delete_returns_403_when_not_admin(self):
521 block_device = factory.make_PhysicalBlockDevice()
522 uri = get_blockdevice_uri(block_device)
523
524=== modified file 'src/maasserver/api/tests/test_dhcpsnippets.py'
525--- src/maasserver/api/tests/test_dhcpsnippets.py 2016-05-24 21:29:53 +0000
526+++ src/maasserver/api/tests/test_dhcpsnippets.py 2016-12-08 16:59:50 +0000
527@@ -19,6 +19,7 @@
528 from maasserver.testing.api import APITestCase
529 from maasserver.testing.factory import factory
530 from maasserver.utils.orm import reload_object
531+from testtools.matchers import Equals
532
533
534 class TestDHCPSnippetAPI(APITestCase.ForUser):
535@@ -44,22 +45,31 @@
536 self.assertEqual(
537 http.client.OK, response.status_code, response.content)
538 parsed_dhcp_snippet = json.loads(response.content.decode())
539- self.assertItemsEqual({
540+ self.assertThat(parsed_dhcp_snippet, Equals({
541 'id': dhcp_snippet.id,
542 'name': dhcp_snippet.name,
543- 'value': dhcp_snippet.value,
544+ 'value': dhcp_snippet.value.data,
545 'description': dhcp_snippet.description,
546- 'history': [{
547- 'id': dhcp_snippet.value.previous_version.id,
548- 'value': dhcp_snippet.value.previous_version.data,
549- 'created': format_datetime(dhcp_snippet.value.created),
550- }],
551+ 'history': [
552+ {
553+ 'id': dhcp_snippet.value.id,
554+ 'value': dhcp_snippet.value.data,
555+ 'created': format_datetime(
556+ dhcp_snippet.value.created),
557+ },
558+ {
559+ 'id': dhcp_snippet.value.previous_version.id,
560+ 'value': dhcp_snippet.value.previous_version.data,
561+ 'created': format_datetime(
562+ dhcp_snippet.value.previous_version.created),
563+ },
564+ ],
565 'enabled': dhcp_snippet.enabled,
566 'node': None,
567 'subnet': None,
568 'global_snippet': True,
569 'resource_uri': self.get_dhcp_snippet_uri(dhcp_snippet),
570- }, parsed_dhcp_snippet)
571+ }))
572
573 def test_read_by_name(self):
574 dhcp_snippet = factory.make_DHCPSnippet()
575@@ -71,22 +81,31 @@
576 self.assertEqual(
577 http.client.OK, response.status_code, response.content)
578 parsed_dhcp_snippet = json.loads(response.content.decode())
579- self.assertItemsEqual({
580+ self.assertThat(parsed_dhcp_snippet, Equals({
581 'id': dhcp_snippet.id,
582 'name': dhcp_snippet.name,
583- 'value': dhcp_snippet.value,
584+ 'value': dhcp_snippet.value.data,
585 'description': dhcp_snippet.description,
586- 'history': [{
587- 'id': dhcp_snippet.value.previous_version.id,
588- 'value': dhcp_snippet.value.previous_version.data,
589- 'created': format_datetime(dhcp_snippet.value.created),
590- }],
591+ 'history': [
592+ {
593+ 'id': dhcp_snippet.value.id,
594+ 'value': dhcp_snippet.value.data,
595+ 'created': format_datetime(
596+ dhcp_snippet.value.created),
597+ },
598+ {
599+ 'id': dhcp_snippet.value.previous_version.id,
600+ 'value': dhcp_snippet.value.previous_version.data,
601+ 'created': format_datetime(
602+ dhcp_snippet.value.previous_version.created),
603+ },
604+ ],
605 'enabled': dhcp_snippet.enabled,
606 'node': None,
607 'subnet': None,
608 'global_snippet': True,
609 'resource_uri': self.get_dhcp_snippet_uri(dhcp_snippet),
610- }, parsed_dhcp_snippet)
611+ }))
612
613 def test_read_404_when_bad_id(self):
614 response = self.client.get(
615
616=== modified file 'src/maasserver/api/tests/test_interfaces.py'
617--- src/maasserver/api/tests/test_interfaces.py 2016-10-20 16:04:24 +0000
618+++ src/maasserver/api/tests/test_interfaces.py 2016-12-08 16:59:50 +0000
619@@ -831,6 +831,7 @@
620 "resource_uri": Equals(get_interface_uri(bond)),
621 "params": Equals(bond.params),
622 "effective_mtu": Equals(bond.get_effective_mtu()),
623+ "system_id": Equals(node.system_id),
624 }))
625 self.assertEqual(sorted(
626 nic.name
627
628=== modified file 'src/maasserver/api/tests/test_partitions.py'
629--- src/maasserver/api/tests/test_partitions.py 2016-05-24 22:05:45 +0000
630+++ src/maasserver/api/tests/test_partitions.py 2016-12-08 16:59:50 +0000
631@@ -165,9 +165,13 @@
632
633 parsed_partition = json.loads(
634 response.content.decode(settings.DEFAULT_CHARSET))
635- self.assertTrue(parsed_partition['bootable'])
636- self.assertEqual(partition.id, parsed_partition['id'])
637- self.assertEqual(partition.size, parsed_partition['size'])
638+ self.assertThat(parsed_partition, ContainsDict({
639+ "bootable": Is(True),
640+ "id": Equals(partition.id),
641+ "size": Equals(partition.size),
642+ "system_id": Equals(device.node.system_id),
643+ "device_id": Equals(device.id),
644+ }))
645
646 def test_read_partition_by_name(self):
647 device = factory.make_PhysicalBlockDevice(
648
649=== modified file 'src/maasserver/api/tests/test_raid.py'
650--- src/maasserver/api/tests/test_raid.py 2016-05-24 21:29:53 +0000
651+++ src/maasserver/api/tests/test_raid.py 2016-12-08 16:59:50 +0000
652@@ -818,6 +818,7 @@
653 "human_size": Equals(
654 human_readable_bytes(raid.get_size())),
655 "resource_uri": Equals(get_raid_device_uri(raid)),
656+ "system_id": Equals(node.system_id),
657 }))
658 self.assertItemsEqual(
659 block_device_ids + partitions_ids, parsed_device_ids)
660
661=== modified file 'src/maasserver/api/tests/test_user.py'
662--- src/maasserver/api/tests/test_user.py 2016-05-24 22:05:45 +0000
663+++ src/maasserver/api/tests/test_user.py 2016-12-08 16:59:50 +0000
664@@ -27,6 +27,11 @@
665 )
666
667
668+def get_user_uri(user):
669+ """Return a User URI."""
670+ return reverse('user_handler', args=[user.username])
671+
672+
673 class TestUsers(APITestCase.ForUser):
674
675 def test_handler_path(self):
676@@ -169,6 +174,7 @@
677 self.assertEqual(user.username, returned_user['username'])
678 self.assertEqual(user.email, returned_user['email'])
679 self.assertFalse(returned_user['is_superuser'])
680+ self.assertEqual(get_user_uri(user), returned_user['resource_uri'])
681
682 def test_GET_shows_expected_fields(self):
683 user = factory.make_User()
684@@ -181,7 +187,7 @@
685 returned_user = json.loads(
686 response.content.decode(settings.DEFAULT_CHARSET))
687 self.assertItemsEqual(
688- ['username', 'email', 'is_superuser'],
689+ ['username', 'email', 'resource_uri', 'is_superuser'],
690 returned_user.keys())
691
692 def test_GET_identifies_superuser_as_such(self):
693
694=== modified file 'src/maasserver/api/tests/test_vlans.py'
695--- src/maasserver/api/tests/test_vlans.py 2016-12-07 16:40:35 +0000
696+++ src/maasserver/api/tests/test_vlans.py 2016-12-08 16:59:50 +0000
697@@ -199,6 +199,7 @@
698 "name": Equals(vlan.get_name()),
699 "vid": Equals(vlan.vid),
700 "fabric": Equals(fabric.get_name()),
701+ "fabric_id": Equals(fabric.id),
702 "resource_uri": Equals(get_vlan_uri(vlan)),
703 }))
704
705
706=== modified file 'src/maasserver/api/tests/test_volume_groups.py'
707--- src/maasserver/api/tests/test_volume_groups.py 2016-05-24 21:29:53 +0000
708+++ src/maasserver/api/tests/test_volume_groups.py 2016-12-08 16:59:50 +0000
709@@ -249,6 +249,7 @@
710 "human_used_size": Equals(
711 human_readable_bytes(volume_group.get_lvm_allocated_size())),
712 "resource_uri": Equals(get_volume_group_uri(volume_group)),
713+ "system_id": Equals(node.system_id),
714 }))
715 self.assertItemsEqual(
716 block_device_ids + partitions_ids, parsed_device_ids)
717
718=== modified file 'src/maasserver/api/users.py'
719--- src/maasserver/api/users.py 2016-03-28 13:54:47 +0000
720+++ src/maasserver/api/users.py 2016-12-08 16:59:50 +0000
721@@ -112,3 +112,8 @@
722 user.delete()
723
724 return rc.DELETED
725+
726+ @classmethod
727+ def resource_uri(cls, user=None):
728+ username = "username" if user is None else user.username
729+ return ('user_handler', [username])
730
731=== modified file 'src/maasserver/api/vlans.py'
732--- src/maasserver/api/vlans.py 2016-12-07 16:40:35 +0000
733+++ src/maasserver/api/vlans.py 2016-12-08 16:59:50 +0000
734@@ -21,6 +21,7 @@
735 'name',
736 'vid',
737 'fabric',
738+ 'fabric_id',
739 'mtu',
740 'primary_rack',
741 'secondary_rack',
742
743=== modified file 'src/maasserver/api/volume_groups.py'
744--- src/maasserver/api/volume_groups.py 2016-03-28 13:54:47 +0000
745+++ src/maasserver/api/volume_groups.py 2016-12-08 16:59:50 +0000
746@@ -31,6 +31,7 @@
747
748
749 DISPLAYED_VOLUME_GROUP_FIELDS = (
750+ 'system_id',
751 'id',
752 'uuid',
753 'name',
754@@ -99,7 +100,7 @@
755 def resource_uri(cls, volume_group=None):
756 # See the comment in NodeHandler.resource_uri.
757 system_id = "system_id"
758- volume_group_id = "volume_group_id"
759+ volume_group_id = "id"
760 if volume_group is not None:
761 volume_group_id = volume_group.id
762 node = volume_group.get_node()
763@@ -108,6 +109,14 @@
764 return ('volume_group_handler', (system_id, volume_group_id))
765
766 @classmethod
767+ def system_id(cls, volume_group):
768+ if volume_group is None:
769+ return None
770+ else:
771+ node = volume_group.get_node()
772+ return None if node is None else node.system_id
773+
774+ @classmethod
775 def size(cls, filesystem_group):
776 return filesystem_group.get_size()
777
778
779=== modified file 'src/metadataserver/api.py'
780--- src/metadataserver/api.py 2016-09-13 20:58:15 +0000
781+++ src/metadataserver/api.py 2016-12-08 16:59:50 +0000
782@@ -196,13 +196,13 @@
783 create = update = delete = None
784
785 def read(self, request, mac=None):
786- return make_list_response(sorted(self.fields))
787+ return make_list_response(sorted(self.subfields))
788
789
790 class IndexHandler(MetadataViewHandler):
791 """Top-level metadata listing."""
792
793- fields = ('latest', '2012-03-01')
794+ subfields = ('latest', '2012-03-01')
795
796
797 class StatusHandler(MetadataViewHandler):
798@@ -394,7 +394,7 @@
799 class VersionIndexHandler(MetadataViewHandler):
800 """Listing for a given metadata version."""
801 create = update = delete = None
802- fields = ('maas-commissioning-scripts', 'meta-data', 'user-data')
803+ subfields = ('maas-commissioning-scripts', 'meta-data', 'user-data')
804
805 # States in which a node is allowed to signal
806 # commissioning/installing/entering-rescue-mode status.
807@@ -432,11 +432,11 @@
808 check_version(version)
809 node = get_queried_node(request, for_mac=mac)
810 if NodeUserData.objects.has_user_data(node):
811- shown_fields = self.fields
812+ shown_subfields = self.subfields
813 else:
814- shown_fields = list(self.fields)
815- shown_fields.remove('user-data')
816- return make_list_response(sorted(shown_fields))
817+ shown_subfields = list(self.subfields)
818+ shown_subfields.remove('user-data')
819+ return make_list_response(sorted(shown_subfields))
820
821 def _store_installation_results(self, node, request):
822 """Store installation result file for `node`."""
823@@ -608,7 +608,7 @@
824 class MetaDataHandler(VersionIndexHandler):
825 """Meta-data listing for a given version."""
826
827- fields = (
828+ subfields = (
829 'instance-id',
830 'local-hostname',
831 'public-keys',
832@@ -627,9 +627,9 @@
833 returns an HttpResponse.
834 :rtype: Callable
835 """
836- field = item.split('/')[0]
837- if field not in self.fields:
838- raise MAASAPINotFound("Unknown metadata attribute: %s" % field)
839+ subfield = item.split('/')[0]
840+ if subfield not in self.subfields:
841+ raise MAASAPINotFound("Unknown metadata attribute: %s" % subfield)
842
843 producers = {
844 'instance-id': self.instance_id,
845@@ -639,7 +639,7 @@
846 'x509': self.ssl_certs,
847 }
848
849- return producers[field]
850+ return producers[subfield]
851
852 def read(self, request, version, mac=None, item=None):
853 check_version(version)
854@@ -648,7 +648,7 @@
855 # Requesting the list of attributes, not any particular
856 # attribute.
857 if item is None or len(item) == 0:
858- fields = list(self.fields)
859+ subfields = list(self.subfields)
860 commissioning_without_ssh = (
861 node.status == NODE_STATUS.COMMISSIONING and
862 not node.enable_ssh)
863@@ -656,8 +656,8 @@
864 # node has registered SSH keys.
865 keys = SSHKey.objects.get_keys_for_user(user=node.owner)
866 if not keys or commissioning_without_ssh:
867- fields.remove('public-keys')
868- return make_list_response(sorted(fields))
869+ subfields.remove('public-keys')
870+ return make_list_response(sorted(subfields))
871
872 producer = self.get_attribute_producer(item)
873 return producer(node, version, item)
874@@ -787,10 +787,10 @@
875
876 class EnlistVersionIndexHandler(OperationsHandler):
877 create = update = delete = None
878- fields = ('meta-data', 'user-data')
879+ subfields = ('meta-data', 'user-data')
880
881 def read(self, request, version):
882- return make_list_response(sorted(self.fields))
883+ return make_list_response(sorted(self.subfields))
884
885
886 class AnonMetaDataHandler(VersionIndexHandler):
887
888=== modified file 'src/metadataserver/tests/test_api.py'
889--- src/metadataserver/tests/test_api.py 2016-09-22 02:53:33 +0000
890+++ src/metadataserver/tests/test_api.py 2016-12-08 16:59:50 +0000
891@@ -345,7 +345,7 @@
892 response = client.get(url)
893 self.assertIn('text/plain', response['Content-Type'])
894 self.assertItemsEqual(
895- MetaDataHandler.fields, [
896+ MetaDataHandler.subfields, [
897 field.decode(settings.DEFAULT_CHARSET)
898 for field in response.content.split()
899 ])