Merge lp:~julian-edwards/maas/api-doc-bug-1391193 into lp:maas/trunk

Proposed by Julian Edwards on 2014-11-21
Status: Merged
Approved by: Julian Edwards on 2014-11-24
Approved revision: 3384
Merged at revision: 3385
Proposed branch: lp:~julian-edwards/maas/api-doc-bug-1391193
Merge into: lp:maas/trunk
Diff against target: 712 lines (+225/-30)
11 files modified
src/maasserver/api/files.py (+5/-0)
src/maasserver/api/ip_addresses.py (+5/-0)
src/maasserver/api/node_group_interfaces.py (+18/-2)
src/maasserver/api/node_groups.py (+38/-5)
src/maasserver/api/node_macs.py (+17/-4)
src/maasserver/api/nodes.py (+85/-9)
src/maasserver/api/ssh_keys.py (+9/-2)
src/maasserver/api/ssl_keys.py (+10/-2)
src/maasserver/api/tags.py (+23/-3)
src/maasserver/api/users.py (+2/-0)
src/maasserver/api/zones.py (+13/-3)
To merge this branch: bzr merge lp:~julian-edwards/maas/api-doc-bug-1391193
Reviewer Review Type Date Requested Status
Newell Jensen 2014-11-21 Approve on 2014-11-21
Review via email: mp+242543@code.launchpad.net

Commit message

Clarify return codes used in all the API docstrings.

Description of the change

I've not covered ValidationError yet, I want to make sure it always returns a 400.

To post a comment you must log in.
Newell Jensen (newell-jensen) wrote :

Approve. See inline comments.

review: Approve
Julian Edwards (julian-edwards) wrote :

Gosh darn I thought I had fixed those, thanks for spotting!

MAAS Lander (maas-lander) wrote :
Download full text (21.4 KiB)

The attempt to merge lp:~julian-edwards/maas/api-doc-bug-1391193 into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Hit http://security.ubuntu.com trusty-security Release.gpg
Hit http://security.ubuntu.com trusty-security Release
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release
Hit http://security.ubuntu.com trusty-security/main Sources
Hit http://security.ubuntu.com trusty-security/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Hit http://security.ubuntu.com trusty-security/main amd64 Packages
Hit http://security.ubuntu.com trusty-security/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools gjs ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make pep8 postgresql pyflakes python-amqplib python-bzrlib python-celery python-convoy python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-jinja2 python-jsonschema python-lockfile python-lxml python-mimeparse python-mock python-netaddr python-netifaces python-nose python-oauth python-oops python-oops-amqp python-oops-datedir-repo python-oops-twisted python-oops-wsgi python-openssl python-paramiko python-pexpect python-pip python-pocket-lint python-psycopg2 python-pyinotify python-seamicroclient python-simplejson python-simplestreams python...

3384. By Julian Edwards on 2014-11-24

ridiculous formatting errors for api doc

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/files.py'
2--- src/maasserver/api/files.py 2014-08-16 10:34:55 +0000
3+++ src/maasserver/api/files.py 2014-11-24 18:23:46 +0000
4@@ -184,6 +184,11 @@
5 :type filename: string
6 :param file: Actual file data with content type
7 application/octet-stream
8+
9+ Returns 400 if any of these conditions apply:
10+ - The filename is missing from the parameters
11+ - The file data is missing
12+ - More than one file is supplied
13 """
14 filename = request.data.get("filename", None)
15 if not filename:
16
17=== modified file 'src/maasserver/api/ip_addresses.py'
18--- src/maasserver/api/ip_addresses.py 2014-10-22 10:35:09 +0000
19+++ src/maasserver/api/ip_addresses.py 2014-11-24 18:23:46 +0000
20@@ -79,6 +79,9 @@
21 :param network: CIDR representation of the network on which the IP
22 reservation is required. e.g. 10.1.2.0/24
23 :type network: unicode
24+
25+ Returns 400 if there is no network in MAAS matching the provided one.
26+ Returns 503 if there are no more IP addresses available.
27 """
28 network = get_mandatory_param(request.POST, "network")
29 requested_address = get_optional_param(
30@@ -111,6 +114,8 @@
31
32 :param ip: The IP address to release.
33 :type ip: unicode
34+
35+ Returns 404 if the provided IP address is not found.
36 """
37 ip = get_mandatory_param(request.POST, "ip")
38 staticaddress = get_object_or_404(
39
40=== modified file 'src/maasserver/api/node_group_interfaces.py'
41--- src/maasserver/api/node_group_interfaces.py 2014-09-10 16:20:31 +0000
42+++ src/maasserver/api/node_group_interfaces.py 2014-11-24 18:23:46 +0000
43@@ -94,6 +94,10 @@
44 :param static_ip_range_high: Highest static IP address to assign to
45 clients.
46 :type static_ip_range_high: unicode (IP Address)
47+
48+ Returns 404 if the node group (cluster) is not found.
49+ Returns 403 if the user does not have permission to access the
50+ interface.
51 """
52 nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
53 if not request.user.is_superuser:
54@@ -139,7 +143,10 @@
55 return nodegroupinterface
56
57 def read(self, request, uuid, name):
58- """List of NodeGroupInterfaces of a NodeGroup."""
59+ """List of NodeGroupInterfaces of a NodeGroup.
60+
61+ Returns 404 if the nodegroup (cluster) is not found.
62+ """
63 # Read-only access is allowed to any user.
64 nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
65 return get_object_or_404(
66@@ -171,6 +178,10 @@
67 :param static_ip_range_high: Highest static IP address to assign to
68 clients.
69 :type static_ip_range_high: unicode (IP Address)
70+
71+ Returns 404 if the nodegroup (cluster) is not found.
72+ Returns 403 if the user does not have permission to access the
73+ interface.
74 """
75 nodegroupinterface = self.get_interface(request, uuid, name)
76 form = NodeGroupInterfaceForm(
77@@ -181,7 +192,12 @@
78 raise ValidationError(form.errors)
79
80 def delete(self, request, uuid, name):
81- """Delete a specific NodeGroupInterface."""
82+ """Delete a specific NodeGroupInterface.
83+
84+ Returns 404 if the nodegroup (cluster) is not found.
85+ Returns 403 if the user does not have permission to access the
86+ interface.
87+ """
88 nodegroupinterface = self.get_interface(request, uuid, name)
89 nodegroupinterface.delete()
90 return rc.DELETED
91
92=== modified file 'src/maasserver/api/node_groups.py'
93--- src/maasserver/api/node_groups.py 2014-10-09 20:24:04 +0000
94+++ src/maasserver/api/node_groups.py 2014-11-24 18:23:46 +0000
95@@ -92,7 +92,10 @@
96 :param uuid: The UUID (or list of UUIDs) of the nodegroup(s) to accept.
97 :type name: unicode (or list of unicodes)
98
99- This method is reserved to admin users.
100+ This method is reserved to admin users and returns 403 if the
101+ user is not an admin.
102+
103+ Returns 404 if the nodegroup (cluster) is not found.
104 """
105 uuids = request.data.getlist('uuid')
106 for uuid in uuids:
107@@ -125,7 +128,10 @@
108 :param uuid: The UUID (or list of UUIDs) of the nodegroup(s) to reject.
109 :type name: unicode (or list of unicodes)
110
111- This method is reserved to admin users.
112+ This method is reserved to admin users and returns 403 if the
113+ user is not an admin.
114+
115+ Returns 404 if the nodegroup (cluster) is not found.
116 """
117 uuids = request.data.getlist('uuid')
118 for uuid in uuids:
119@@ -173,7 +179,10 @@
120 fields = DISPLAYED_NODEGROUP_FIELDS
121
122 def read(self, request, uuid):
123- """GET a node group."""
124+ """GET a node group.
125+
126+ Returns 404 if the nodegroup (cluster) is not found.
127+ """
128 return get_object_or_404(NodeGroup, uuid=uuid)
129
130 @classmethod
131@@ -195,6 +204,8 @@
132 :param status: The new status for this cluster (see
133 vocabulary `NODEGROUP_STATUS`).
134 :type status: int
135+
136+ Returns 404 if the nodegroup (cluster) is not found.
137 """
138 nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
139 form = NodeGroupEdit(instance=nodegroup, data=request.data)
140@@ -206,7 +217,10 @@
141 @admin_method
142 @operation(idempotent=False)
143 def import_boot_images(self, request, uuid):
144- """Import the pxe files on this cluster controller."""
145+ """Import the pxe files on this cluster controller.
146+
147+ Returns 404 if the nodegroup (cluster) is not found.
148+ """
149 nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
150 nodegroup.import_boot_images()
151 return HttpResponse(
152@@ -215,7 +229,10 @@
153
154 @operation(idempotent=True)
155 def list_nodes(self, request, uuid):
156- """Get the list of node ids that are part of this group."""
157+ """Get the list of node ids that are part of this group.
158+
159+ Returns 404 if the nodegroup (cluster) is not found.
160+ """
161 nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
162 if not request.user.is_superuser:
163 check_nodegroup_access(request, nodegroup)
164@@ -250,6 +267,8 @@
165 b) Requests for nodes that are not part of the nodegroup are
166 just ignored.
167
168+ Returns 404 if the nodegroup (cluster) is not found.
169+ Returns 403 if the user does not have access to the nodegroup.
170 """
171 nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
172 if not request.user.is_superuser:
173@@ -318,6 +337,10 @@
174 :param error: Optional error string. A download that has submitted an
175 error with its last progress report is considered to have failed.
176 :type error: unicode
177+
178+ Returns 404 if the nodegroup (cluster) is not found.
179+ Returns 403 if the user does not have access to the nodegroup.
180+ Returns 400 if the required parameters were not passed.
181 """
182 nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
183 check_nodegroup_access(request, nodegroup)
184@@ -372,6 +395,10 @@
185 :param power_pass: The password to use, when qemu+ssh is given as a
186 connection string and ssh key authentication is not being used.
187 :type power_pass: unicode
188+
189+ Returns 404 if the nodegroup (cluster) is not found.
190+ Returns 403 if the user does not have access to the nodegroup.
191+ Returns 400 if the required parameters were not passed.
192 """
193 nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
194
195@@ -409,6 +436,9 @@
196 :param password: The password for the API.
197 :type password: unicode
198
199+ Returns 404 if the nodegroup (cluster) is not found.
200+ Returns 403 if the user does not have access to the nodegroup.
201+ Returns 400 if the required parameters were not passed.
202 """
203 nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
204
205@@ -432,6 +462,9 @@
206 :param password: The password for the MSCM.
207 :type password: unicode
208
209+ Returns 404 if the nodegroup (cluster) is not found.
210+ Returns 403 if the user does not have access to the nodegroup.
211+ Returns 400 if the required parameters were not passed.
212 """
213 nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
214
215
216=== modified file 'src/maasserver/api/node_macs.py'
217--- src/maasserver/api/node_macs.py 2014-08-18 13:27:34 +0000
218+++ src/maasserver/api/node_macs.py 2014-11-24 18:23:46 +0000
219@@ -41,14 +41,20 @@
220 update = delete = None
221
222 def read(self, request, system_id):
223- """Read all MAC addresses related to a Node."""
224+ """Read all MAC addresses related to a Node.
225+
226+ Returns 404 if the node is not found.
227+ """
228 node = Node.objects.get_node_or_404(
229 user=request.user, system_id=system_id, perm=NODE_PERMISSION.VIEW)
230
231 return MACAddress.objects.filter(node=node).order_by('id')
232
233 def create(self, request, system_id):
234- """Create a MAC address for a specified Node."""
235+ """Create a MAC address for a specified Node.
236+
237+ Returns 404 if the node is not found.
238+ """
239 node = Node.objects.get_node_or_404(
240 user=request.user, system_id=system_id, perm=NODE_PERMISSION.EDIT)
241 mac = node.add_mac_address(request.data.get('mac_address', None))
242@@ -71,7 +77,10 @@
243 model = MACAddress
244
245 def read(self, request, system_id, mac_address):
246- """Read a MAC address related to a Node."""
247+ """Read a MAC address related to a Node.
248+
249+ Returns 404 if the node or the MAC address is not found.
250+ """
251 node = Node.objects.get_node_or_404(
252 user=request.user, system_id=system_id, perm=NODE_PERMISSION.VIEW)
253
254@@ -80,7 +89,11 @@
255 MACAddress, node=node, mac_address=mac_address)
256
257 def delete(self, request, system_id, mac_address):
258- """Delete a specific MAC address for the specified Node."""
259+ """Delete a specific MAC address for the specified Node.
260+
261+ Returns 404 if the node or the MAC address is not found.
262+ Returns 204 after the MAC address is successfully deleted.
263+ """
264 validate_mac(mac_address)
265 node = Node.objects.get_node_or_404(
266 user=request.user, system_id=system_id, perm=NODE_PERMISSION.EDIT)
267
268=== modified file 'src/maasserver/api/nodes.py'
269--- src/maasserver/api/nodes.py 2014-11-05 15:10:12 +0000
270+++ src/maasserver/api/nodes.py 2014-11-24 18:23:46 +0000
271@@ -197,7 +197,10 @@
272 return node.owner.username
273
274 def read(self, request, system_id):
275- """Read a specific Node."""
276+ """Read a specific Node.
277+
278+ Returns 404 if the node is not found.
279+ """
280 return Node.objects.get_node_or_404(
281 system_id=system_id, user=request.user, perm=NODE_PERMISSION.VIEW)
282
283@@ -231,6 +234,9 @@
284 :type power_parameters_skip_check: unicode
285 :param zone: Name of a valid physical zone in which to place this node
286 :type zone: unicode
287+
288+ Returns 404 if the node is node found.
289+ Returns 403 if the user does not have permission to update the node.
290 """
291 node = Node.objects.get_node_or_404(
292 system_id=system_id, user=request.user, perm=NODE_PERMISSION.EDIT)
293@@ -243,7 +249,12 @@
294 raise ValidationError(form.errors)
295
296 def delete(self, request, system_id):
297- """Delete a specific Node."""
298+ """Delete a specific Node.
299+
300+ Returns 404 if the node is not found.
301+ Returns 403 if the user does not have permission to delete the node.
302+ Returns 204 if the node is successfully deleted.
303+ """
304 node = Node.objects.get_node_or_404(
305 system_id=system_id, user=request.user,
306 perm=NODE_PERMISSION.ADMIN)
307@@ -274,6 +285,9 @@
308 gracefully before powering off, while a hard power off
309 occurs immediately without any warning to the OS.
310 :type stop_mode: unicode
311+
312+ Returns 404 if the node is not found.
313+ Returns 403 if the user does not have permission to stop the node.
314 """
315 stop_mode = request.POST.get('stop_mode', 'hard')
316 node = Node.objects.get_node_or_404(
317@@ -300,6 +314,12 @@
318 deal with the encapsulation of binary data, but couldn't make it work
319 with the framework in reasonable time so went for a dumb, manual
320 encoding instead.
321+
322+ Returns 404 if the node is not found.
323+ Returns 403 if the user does not have permission to stop the node.
324+ Returns 503 if the start-up attempted to allocate an IP address,
325+ and there were no IP addresses available on the relevant cluster
326+ interface.
327 """
328 user_data = request.POST.get('user_data', None)
329 series = request.POST.get('distro_series', None)
330@@ -335,7 +355,12 @@
331
332 @operation(idempotent=False)
333 def release(self, request, system_id):
334- """Release a node. Opposite of `NodesHandler.acquire`."""
335+ """Release a node. Opposite of `NodesHandler.acquire`.
336+
337+ Returns 404 if the node is not found.
338+ Returns 403 if the user does not have permission to release the node.
339+ Returns 409 if the node is in a state where it may not be released.
340+ """
341 node = Node.objects.get_node_or_404(
342 system_id=system_id, user=request.user, perm=NODE_PERMISSION.EDIT)
343 if node.status == NODE_STATUS.READY:
344@@ -360,6 +385,8 @@
345 already in the 'ready' state this is considered a re-commissioning
346 process which is useful if commissioning tests were changed after
347 it previously commissioned.
348+
349+ Returns 404 if the node is not found.
350 """
351 node = get_object_or_404(Node, system_id=system_id)
352 form_class = get_action_form(user=request.user)
353@@ -383,6 +410,8 @@
354 Note that this is returned as BSON and not JSON. This is for
355 efficiency, but mainly because JSON can't do binary content
356 without applying additional encoding like base-64.
357+
358+ Returns 404 if the node is not found.
359 """
360 node = get_object_or_404(Node, system_id=system_id)
361 probe_details = get_single_probed_details(node.system_id)
362@@ -415,6 +444,12 @@
363 an admin to give a node a stable IP, since normally an automatic
364 IP is allocated to a node only during the time a user has
365 acquired and started a node.
366+
367+ Returns 404 if the node is not found.
368+ Returns 409 if the node is in an allocated state.
369+ Returns 400 if the mac_address is not found on the node.
370+ Returns 503 if there are not enough IPs left on the cluster interface
371+ to which the mac_address is linked.
372 """
373 node = get_object_or_404(Node, system_id=system_id)
374 if node.status == NODE_STATUS.ALLOCATED:
375@@ -449,6 +484,10 @@
376 :param error_description: An optional description of the reason the
377 node is being marked broken.
378 :type error_description: unicode
379+
380+ Returns 404 if the node is not found.
381+ Returns 403 if the user does not have permission to mark the node
382+ broken.
383 """
384 node = Node.objects.get_node_or_404(
385 user=request.user, system_id=system_id, perm=NODE_PERMISSION.EDIT)
386@@ -459,7 +498,12 @@
387
388 @operation(idempotent=False)
389 def mark_fixed(self, request, system_id):
390- """Mark a broken node as fixed and set its status as 'ready'."""
391+ """Mark a broken node as fixed and set its status as 'ready'.
392+
393+ Returns 404 if the node is not found.
394+ Returns 403 if the user does not have permission to mark the node
395+ broken.
396+ """
397 node = Node.objects.get_node_or_404(
398 user=request.user, system_id=system_id, perm=NODE_PERMISSION.ADMIN)
399 node.mark_fixed()
400@@ -473,11 +517,14 @@
401 def power_parameters(self, request, system_id):
402 """Obtain power parameters.
403
404- This method is reserved for admin users.
405+ This method is reserved for admin users and returns a 403 if the
406+ user is not one.
407
408 This returns the power parameters, if any, configured for a
409 node. For some types of power control this will include private
410 information such as passwords and secret keys.
411+
412+ Returns 404 if the node is not found.
413 """
414 node = get_object_or_404(Node, system_id=system_id)
415 return node.power_parameters
416@@ -492,12 +539,13 @@
417 Use this method sparingly as it ties up an appserver thread
418 while waiting.
419
420- If there is a problem, SERVICE_UNAVAILABLE is returned with text
421- explaining why.
422-
423 :param system_id: The node to query.
424 :return: a dict whose key is "state" with a value of one of
425 'on' or 'off'.
426+
427+ Returns 404 if the node is not found.
428+ Returns 503 (with explanatory text) if the power state could not
429+ be queried.
430 """
431 node = get_object_or_404(Node, system_id=system_id)
432 ng = node.nodegroup
433@@ -540,6 +588,10 @@
434 """Abort a node's current operation.
435
436 This currently only supports aborting of the 'Disk Erasing' operation.
437+
438+ Returns 404 if the node could not be found.
439+ Returns 403 if the user does not have permission to abort the
440+ current operation.
441 """
442 node = Node.objects.get_node_or_404(
443 system_id=system_id, user=request.user,
444@@ -648,6 +700,8 @@
445 :type mac_address: unicode
446 :return: 'true' or 'false'.
447 :rtype: unicode
448+
449+ Returns 400 if any mandatory parameters are missing.
450 """
451 mac_address = get_mandatory_param(request.GET, 'mac_address')
452 mac_addresses = MACAddress.objects.filter(mac_address=mac_address)
453@@ -656,7 +710,10 @@
454
455 @operation(idempotent=False)
456 def accept(self, request):
457- """Accept a node's enlistment: not allowed to anonymous users."""
458+ """Accept a node's enlistment: not allowed to anonymous users.
459+
460+ Always returns 401.
461+ """
462 raise Unauthorized("You must be logged in to accept nodes.")
463
464 @classmethod
465@@ -726,6 +783,9 @@
466 :return: The system_ids of any nodes that have their status changed
467 by this call. Thus, nodes that were already accepted are
468 excluded from the result.
469+
470+ Returns 400 if any of the nodes do not exist.
471+ Returns 403 if the user is not an admin.
472 """
473 system_ids = set(request.POST.getlist('nodes'))
474 # Check the existence of these nodes first.
475@@ -813,6 +873,12 @@
476 :return: The system_ids of any nodes that have their status
477 changed by this call. Thus, nodes that were already released
478 are excluded from the result.
479+
480+ Returns 400 if any of the nodes cannot be found.
481+ Returns 403 if the user does not have permission to release any of
482+ the nodes.
483+ Returns a 409 if any of the nodes could not be released due to their
484+ current state.
485 """
486 system_ids = set(request.POST.getlist('nodes'))
487 # Check the existence of these nodes first.
488@@ -964,6 +1030,9 @@
489 :param agent_name: An optional agent name to attach to the
490 acquired node.
491 :type agent_name: unicode
492+
493+ Returns 409 if a suitable node matching the constraints could not be
494+ found.
495 """
496 form = AcquireNodeForm(data=request.data)
497 maaslog.info(
498@@ -1006,6 +1075,8 @@
499 will be taken out of their physical zones.
500 :param nodes: system_ids of the nodes whose zones are to be set.
501 (An empty list is acceptable).
502+
503+ Raises 403 if the user is not an admin.
504 """
505 data = {
506 'action': 'set_zone',
507@@ -1027,6 +1098,8 @@
508 :type id: iterable
509
510 :return: A dictionary of power parameters, keyed by node system_id.
511+
512+ Raises 403 if the user is not an admin.
513 """
514 match_ids = get_optional_list(request.GET, 'id')
515
516@@ -1043,6 +1116,9 @@
517
518 :param nodes: Mandatory list of system IDs for nodes whose status
519 you wish to check.
520+
521+ Returns 400 if mandatory parameters are missing.
522+ Returns 403 if the user has no permission to view any of the nodes.
523 """
524 system_ids = set(request.GET.getlist('nodes'))
525 # Check the existence of these nodes first.
526
527=== modified file 'src/maasserver/api/ssh_keys.py'
528--- src/maasserver/api/ssh_keys.py 2014-08-16 15:05:54 +0000
529+++ src/maasserver/api/ssh_keys.py 2014-11-24 18:23:46 +0000
530@@ -83,13 +83,20 @@
531 create = update = None
532
533 def read(self, request, keyid):
534- """GET an SSH key."""
535+ """GET an SSH key.
536+
537+ Returns 404 if the key does not exist.
538+ """
539 key = get_object_or_404(SSHKey, id=keyid)
540 return key
541
542 @operation(idempotent=False)
543 def delete(self, request, keyid):
544- """DELETE an SSH key."""
545+ """DELETE an SSH key.
546+
547+ Returns 404 if the key does not exist.
548+ Returns 401 if the key does not belong to the calling user.
549+ """
550 key = get_object_or_404(SSHKey, id=keyid)
551 if key.user != request.user:
552 return HttpResponse(
553
554=== modified file 'src/maasserver/api/ssl_keys.py'
555--- src/maasserver/api/ssl_keys.py 2014-08-17 01:01:12 +0000
556+++ src/maasserver/api/ssl_keys.py 2014-11-24 18:23:46 +0000
557@@ -83,7 +83,11 @@
558 create = update = None
559
560 def read(self, request, keyid):
561- """GET an SSL key."""
562+ """GET an SSL key.
563+
564+ Returns 404 if the keyid is not found.
565+ Returns 401 if the key does not belong to the requesting user.
566+ """
567 key = get_object_or_404(SSLKey, id=keyid)
568 if key.user != request.user:
569 return HttpResponse(
570@@ -92,7 +96,11 @@
571
572 @operation(idempotent=True)
573 def delete(self, request, keyid):
574- """DELETE an SSL key."""
575+ """DELETE an SSL key.
576+
577+ Returns 401 if the key does not belong to the requesting user.
578+ Returns 204 if the key is successfully deleted.
579+ """
580 key = get_object_or_404(SSLKey, id=keyid)
581 if key.user != request.user:
582 return HttpResponse(
583
584=== modified file 'src/maasserver/api/tags.py'
585--- src/maasserver/api/tags.py 2014-08-18 11:43:18 +0000
586+++ src/maasserver/api/tags.py 2014-11-24 18:23:46 +0000
587@@ -62,7 +62,10 @@
588 )
589
590 def read(self, request, name):
591- """Read a specific Tag"""
592+ """Read a specific Tag.
593+
594+ Returns 404 if the tag is not found.
595+ """
596 return Tag.objects.get_tag_or_404(name=name, user=request.user)
597
598 def update(self, request, name):
599@@ -74,6 +77,8 @@
600 It is meant as a human readable description of the tag.
601 :param definition: An XPATH query that will be evaluated against the
602 hardware_details stored for all nodes (output of `lshw -xml`).
603+
604+ Returns 404 if the tag is not found.
605 """
606 tag = Tag.objects.get_tag_or_404(
607 name=name, user=request.user, to_edit=True)
608@@ -90,7 +95,11 @@
609 raise ValidationError(form.errors)
610
611 def delete(self, request, name):
612- """Delete a specific Tag."""
613+ """Delete a specific Tag.
614+
615+ Returns 404 if the tag is not found.
616+ Returns 204 if the tag is successfully deleted.
617+ """
618 tag = Tag.objects.get_tag_or_404(
619 name=name, user=request.user, to_edit=True)
620 tag.delete()
621@@ -98,7 +107,10 @@
622
623 @operation(idempotent=True)
624 def nodes(self, request, name):
625- """Get the list of nodes that have this tag."""
626+ """Get the list of nodes that have this tag.
627+
628+ Returns 404 if the tag is not found.
629+ """
630 tag = Tag.objects.get_tag_or_404(name=name, user=request.user)
631 return Node.objects.get_nodes(
632 request.user, NODE_PERMISSION.VIEW, from_nodes=tag.node_set.all())
633@@ -120,6 +132,8 @@
634 This is considered a maintenance operation, which should normally not
635 be necessary. Adding nodes or updating a tag's definition should
636 automatically trigger the appropriate changes.
637+
638+ Returns 404 if the tag is not found.
639 """
640 tag = Tag.objects.get_tag_or_404(name=name, user=request.user,
641 to_edit=True)
642@@ -141,6 +155,10 @@
643 supplied, then the requester must be the worker associated with
644 that nodegroup, and only nodes that are part of that nodegroup can
645 be updated.
646+
647+ Returns 404 if the tag is not found.
648+ Returns 401 if the user does not have permission to update the nodes.
649+ Returns 409 if 'definition' doesn't match the current definition.
650 """
651 tag = Tag.objects.get_tag_or_404(name=name, user=request.user)
652 nodegroup = None
653@@ -196,6 +214,8 @@
654 value overrides the global 'kernel_opts' setting. If more than one
655 tag is associated with a node, the one with the lowest alphabetical
656 name will be picked (eg 01-my-tag will be taken over 99-tag-name).
657+
658+ Returns 401 if the user is not an admin.
659 """
660 if not request.user.is_superuser:
661 raise PermissionDenied()
662
663=== modified file 'src/maasserver/api/users.py'
664--- src/maasserver/api/users.py 2014-08-16 15:14:46 +0000
665+++ src/maasserver/api/users.py 2014-11-24 18:23:46 +0000
666@@ -59,6 +59,8 @@
667 :type password: unicode
668 :param is_superuser: Whether the new user is to be an administrator.
669 :type is_superuser: bool ('0' for False, '1' for True)
670+
671+ Returns 400 if any mandatory parameters are missing.
672 """
673 username = get_mandatory_param(request.data, 'username')
674 email = get_mandatory_param(request.data, 'email')
675
676=== modified file 'src/maasserver/api/zones.py'
677--- src/maasserver/api/zones.py 2014-08-16 14:54:27 +0000
678+++ src/maasserver/api/zones.py 2014-11-24 18:23:46 +0000
679@@ -48,12 +48,18 @@
680 create = None
681
682 def read(self, request, name):
683- """GET request. Return zone."""
684+ """GET request. Return zone.
685+
686+ Returns 404 if the zone is not found.
687+ """
688 return get_object_or_404(Zone, name=name)
689
690 @admin_method
691 def update(self, request, name):
692- """PUT request. Update zone."""
693+ """PUT request. Update zone.
694+
695+ Returns 404 if the zone is not found.
696+ """
697 zone = get_object_or_404(Zone, name=name)
698 form = ZoneForm(instance=zone, data=request.data)
699 if not form.is_valid():
700@@ -62,7 +68,11 @@
701
702 @admin_method
703 def delete(self, request, name):
704- """DELETE request. Delete zone."""
705+ """DELETE request. Delete zone.
706+
707+ Returns 404 if the zone is not found.
708+ Returns 204 if the zone is successfully deleted.
709+ """
710 zone = get_one(Zone.objects.filter(name=name))
711 if zone is not None:
712 zone.delete()