Merge lp:~allenap/maas/fix-resource-uri-model-mismatch into lp:~maas-committers/maas/trunk
- fix-resource-uri-model-mismatch
- Merge into trunk
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 |
Related bugs: |
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.
Description of the change
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?
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/
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!
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.
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!
Mike Pontillo (mpontillo) wrote : | # |
Thanks for the fixes!
Preview Diff
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 | ]) |
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'?