Merge lp:~trapnine/maas/events-2 into lp:~maas-committers/maas/trunk
- events-2
- Merge into trunk
Proposed by
Jeffrey C Jones
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Jeffrey C Jones | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 4284 | ||||
Proposed branch: | lp:~trapnine/maas/events-2 | ||||
Merge into: | lp:~maas-committers/maas/trunk | ||||
Prerequisite: | lp:~trapnine/maas/events-1 | ||||
Diff against target: |
476 lines (+236/-19) 3 files modified
src/maasserver/api/nodes.py (+41/-13) src/maasserver/api/tests/test_node.py (+194/-5) src/maasserver/api/tests/test_nodes.py (+1/-1) |
||||
To merge this branch: | bzr merge lp:~trapnine/maas/events-2 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Blake Rouse (community) | Approve | ||
Review via email: mp+271578@code.launchpad.net |
Commit message
An optional comment can be supplied to select Node operations via the API.
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/maasserver/api/nodes.py' |
2 | --- src/maasserver/api/nodes.py 2015-09-17 10:30:47 +0000 |
3 | +++ src/maasserver/api/nodes.py 2015-09-18 17:34:17 +0000 |
4 | @@ -402,15 +402,19 @@ |
5 | gracefully before powering off, while a hard power off |
6 | occurs immediately without any warning to the OS. |
7 | :type stop_mode: unicode |
8 | + :param comment: Optional comment for the event log. |
9 | + :type comment: unicode |
10 | |
11 | Returns 404 if the node is not found. |
12 | Returns 403 if the user does not have permission to stop the node. |
13 | """ |
14 | stop_mode = request.POST.get('stop_mode', 'hard') |
15 | + comment = get_optional_param(request.POST, 'comment') |
16 | node = Node.nodes.get_node_or_404( |
17 | system_id=system_id, user=request.user, |
18 | perm=NODE_PERMISSION.EDIT) |
19 | - power_action_sent = node.stop(request.user, stop_mode=stop_mode) |
20 | + power_action_sent = node.stop( |
21 | + request.user, stop_mode=stop_mode, comment=comment) |
22 | if power_action_sent: |
23 | return node |
24 | else: |
25 | @@ -429,6 +433,8 @@ |
26 | :param hwe_kernel: If present, this parameter specified the kernel to |
27 | be used on the node |
28 | :type hwe_kernel: unicode |
29 | + :param comment: Optional comment for the event log. |
30 | + :type comment: unicode |
31 | |
32 | Ideally we'd have MIME multipart and content-transfer-encoding etc. |
33 | deal with the encapsulation of binary data, but couldn't make it work |
34 | @@ -445,6 +451,7 @@ |
35 | series = request.POST.get('distro_series', None) |
36 | license_key = request.POST.get('license_key', None) |
37 | hwe_kernel = request.POST.get('hwe_kernel', None) |
38 | + comment = get_optional_param(request.POST, 'comment') |
39 | |
40 | node = Node.nodes.get_node_or_404( |
41 | system_id=system_id, user=request.user, |
42 | @@ -472,7 +479,7 @@ |
43 | raise MAASAPIValidationError(form.errors) |
44 | |
45 | try: |
46 | - node.start(request.user, user_data=user_data) |
47 | + node.start(request.user, user_data=user_data, comment=comment) |
48 | except StaticIPAddressExhaustion: |
49 | # The API response should contain error text with the |
50 | # system_id in it, as that is the primary API key to a node. |
51 | @@ -485,10 +492,14 @@ |
52 | def release(self, request, system_id): |
53 | """Release a node. Opposite of `NodesHandler.acquire`. |
54 | |
55 | + :param comment: Optional comment for the event log. |
56 | + :type comment: unicode |
57 | + |
58 | Returns 404 if the node is not found. |
59 | Returns 403 if the user does not have permission to release the node. |
60 | Returns 409 if the node is in a state where it may not be released. |
61 | """ |
62 | + comment = get_optional_param(request.POST, 'comment') |
63 | node = Node.nodes.get_node_or_404( |
64 | system_id=system_id, user=request.user, perm=NODE_PERMISSION.EDIT) |
65 | if node.status == NODE_STATUS.RELEASING or \ |
66 | @@ -498,7 +509,7 @@ |
67 | # postcondition is achieved, so call this success. |
68 | pass |
69 | elif node.status in RELEASABLE_STATUSES: |
70 | - node.release_or_erase(request.user) |
71 | + node.release_or_erase(request.user, comment) |
72 | else: |
73 | raise NodeStateViolation( |
74 | "Node cannot be released in its current state ('%s')." |
75 | @@ -665,9 +676,9 @@ |
76 | |
77 | If the node is allocated, release it first. |
78 | |
79 | - :param error_description: An optional description of the reason the |
80 | - node is being marked broken. |
81 | - :type error_description: unicode |
82 | + :param comment: Optional comment for the event log. Will be |
83 | + displayed on the Node as an error description until marked fixed. |
84 | + :type comment: unicode |
85 | |
86 | Returns 404 if the node is not found. |
87 | Returns 403 if the user does not have permission to mark the node |
88 | @@ -675,22 +686,28 @@ |
89 | """ |
90 | node = Node.nodes.get_node_or_404( |
91 | user=request.user, system_id=system_id, perm=NODE_PERMISSION.EDIT) |
92 | - error_description = get_optional_param( |
93 | - request.POST, 'error_description', '') |
94 | - node.mark_broken(request.user, error_description) |
95 | + comment = get_optional_param(request.POST, 'comment') |
96 | + if not comment: |
97 | + # read old error_description to for backward compatibility |
98 | + comment = get_optional_param(request.POST, 'error_description') |
99 | + node.mark_broken(request.user, comment) |
100 | return node |
101 | |
102 | @operation(idempotent=False) |
103 | def mark_fixed(self, request, system_id): |
104 | """Mark a broken node as fixed and set its status as 'ready'. |
105 | |
106 | + :param comment: Optional comment for the event log. |
107 | + :type comment: unicode |
108 | + |
109 | Returns 404 if the node is not found. |
110 | Returns 403 if the user does not have permission to mark the node |
111 | broken. |
112 | """ |
113 | + comment = get_optional_param(request.POST, 'comment') |
114 | node = Node.nodes.get_node_or_404( |
115 | user=request.user, system_id=system_id, perm=NODE_PERMISSION.ADMIN) |
116 | - node.mark_fixed(request.user) |
117 | + node.mark_fixed(request.user, comment) |
118 | maaslog.info( |
119 | "%s: User %s marked node as fixed", node.hostname, |
120 | request.user.username) |
121 | @@ -775,16 +792,20 @@ |
122 | def abort_operation(self, request, system_id): |
123 | """Abort a node's current operation. |
124 | |
125 | + :param comment: Optional comment for the event log. |
126 | + :type comment: unicode |
127 | + |
128 | This currently only supports aborting of the 'Disk Erasing' operation. |
129 | |
130 | Returns 404 if the node could not be found. |
131 | Returns 403 if the user does not have permission to abort the |
132 | current operation. |
133 | """ |
134 | + comment = get_optional_param(request.POST, 'comment') |
135 | node = Node.nodes.get_node_or_404( |
136 | system_id=system_id, user=request.user, |
137 | perm=NODE_PERMISSION.EDIT) |
138 | - node.abort_operation(request.user) |
139 | + node.abort_operation(request.user, comment) |
140 | return node |
141 | |
142 | @operation(idempotent=False) |
143 | @@ -1209,6 +1230,8 @@ |
144 | |
145 | :param nodes: system_ids of the nodes which are to be released. |
146 | (An empty list is acceptable). |
147 | + :param comment: Optional comment for the event log. |
148 | + :type comment: unicode |
149 | :return: The system_ids of any nodes that have their status |
150 | changed by this call. Thus, nodes that were already released |
151 | are excluded from the result. |
152 | @@ -1220,6 +1243,7 @@ |
153 | current state. |
154 | """ |
155 | system_ids = set(request.POST.getlist('nodes')) |
156 | + comment = get_optional_param(request.POST, 'comment') |
157 | # Check the existence of these nodes first. |
158 | self._check_system_ids_exist(system_ids) |
159 | # Make sure that the user has the required permission. |
160 | @@ -1239,7 +1263,7 @@ |
161 | # Nothing to do. |
162 | pass |
163 | elif node.status in RELEASABLE_STATUSES: |
164 | - node.release_or_erase(request.user) |
165 | + node.release_or_erase(request.user, comment) |
166 | released_ids.append(node.system_id) |
167 | else: |
168 | failed.append( |
169 | @@ -1329,11 +1353,14 @@ |
170 | :param agent_name: An optional agent name to attach to the |
171 | acquired node. |
172 | :type agent_name: unicode |
173 | + :param comment: Optional comment for the event log. |
174 | + :type comment: unicode |
175 | |
176 | Returns 409 if a suitable node matching the constraints could not be |
177 | found. |
178 | """ |
179 | form = AcquireNodeForm(data=request.data) |
180 | + comment = get_optional_param(request.POST, 'comment') |
181 | maaslog.info( |
182 | "Request from user %s to acquire a node with constraints %s", |
183 | request.user.username, request.data) |
184 | @@ -1369,7 +1396,8 @@ |
185 | request.user, get_oauth_token(request), |
186 | agent_name=agent_name, |
187 | storage_layout=storage_layout, |
188 | - storage_layout_params=storage_layout_params) |
189 | + storage_layout_params=storage_layout_params, |
190 | + comment=comment) |
191 | except StorageLayoutMissingBootDiskError: |
192 | raise MAASAPIBadRequest( |
193 | "Failed to acquire node; missing a boot disk (no storage " |
194 | |
195 | === modified file 'src/maasserver/api/tests/test_node.py' |
196 | --- src/maasserver/api/tests/test_node.py 2015-09-17 15:37:57 +0000 |
197 | +++ src/maasserver/api/tests/test_node.py 2015-09-18 17:34:17 +0000 |
198 | @@ -340,7 +340,7 @@ |
199 | self.assertEqual(httplib.OK, response.status_code) |
200 | self.assertIsNone(json.loads(response.content)) |
201 | self.assertThat(node_stop, MockCalledOnceWith( |
202 | - ANY, stop_mode=ANY)) |
203 | + ANY, stop_mode=ANY, comment=None)) |
204 | |
205 | def test_POST_stop_returns_node(self): |
206 | node = factory.make_Node(owner=self.logged_in_user) |
207 | @@ -363,11 +363,25 @@ |
208 | node = factory.make_Node(owner=self.logged_in_user) |
209 | node_stop = self.patch(node_module.Node, 'stop') |
210 | stop_mode = factory.make_name('stop_mode') |
211 | + comment = factory.make_name('comment') |
212 | + self.client.post( |
213 | + self.get_node_uri(node), |
214 | + {'op': 'stop', 'stop_mode': stop_mode, 'comment': comment}) |
215 | + self.assertThat( |
216 | + node_stop, |
217 | + MockCalledOnceWith( |
218 | + self.logged_in_user, stop_mode=stop_mode, comment=comment)) |
219 | + |
220 | + def test_POST_stop_handles_missing_comment(self): |
221 | + node = factory.make_Node(owner=self.logged_in_user) |
222 | + node_stop = self.patch(node_module.Node, 'stop') |
223 | + stop_mode = factory.make_name('stop_mode') |
224 | self.client.post( |
225 | self.get_node_uri(node), {'op': 'stop', 'stop_mode': stop_mode}) |
226 | self.assertThat( |
227 | node_stop, |
228 | - MockCalledOnceWith(self.logged_in_user, stop_mode=stop_mode)) |
229 | + MockCalledOnceWith( |
230 | + self.logged_in_user, stop_mode=stop_mode, comment=None)) |
231 | |
232 | def test_POST_stop_returns_503_when_power_op_already_in_progress(self): |
233 | node = factory.make_Node(owner=self.logged_in_user) |
234 | @@ -592,6 +606,44 @@ |
235 | self.assertEqual(httplib.OK, response.status_code) |
236 | self.assertEqual(user_data, NodeUserData.objects.get_user_data(node)) |
237 | |
238 | + def test_POST_start_passes_comment(self): |
239 | + node = factory.make_Node( |
240 | + owner=self.logged_in_user, interface=True, |
241 | + power_type='ether_wake', |
242 | + architecture=make_usable_architecture(self)) |
243 | + osystem = make_usable_osystem(self) |
244 | + distro_series = osystem['default_release'] |
245 | + comment = factory.make_name('comment') |
246 | + node_start = self.patch(node_module.Node, 'start') |
247 | + node_start.return_value = False |
248 | + self.client.post( |
249 | + self.get_node_uri(node), { |
250 | + 'op': 'start', |
251 | + 'user_data': None, |
252 | + 'distro_series': distro_series, |
253 | + 'comment': comment, |
254 | + }) |
255 | + self.assertThat(node_start, MockCalledOnceWith( |
256 | + self.logged_in_user, user_data=ANY, comment=comment)) |
257 | + |
258 | + def test_POST_start_handles_missing_comment(self): |
259 | + node = factory.make_Node( |
260 | + owner=self.logged_in_user, interface=True, |
261 | + power_type='ether_wake', |
262 | + architecture=make_usable_architecture(self)) |
263 | + osystem = make_usable_osystem(self) |
264 | + distro_series = osystem['default_release'] |
265 | + node_start = self.patch(node_module.Node, 'start') |
266 | + node_start.return_value = False |
267 | + self.client.post( |
268 | + self.get_node_uri(node), { |
269 | + 'op': 'start', |
270 | + 'user_data': None, |
271 | + 'distro_series': distro_series, |
272 | + }) |
273 | + self.assertThat(node_start, MockCalledOnceWith( |
274 | + self.logged_in_user, user_data=ANY, comment=None)) |
275 | + |
276 | def test_POST_release_releases_owned_node(self): |
277 | self.patch(node_module, 'power_off_node') |
278 | self.patch(node_module.Node, 'start_transition_monitor') |
279 | @@ -720,6 +772,32 @@ |
280 | self.assertEqual(httplib.OK, response.status_code, response.content) |
281 | self.assertEqual(NODE_STATUS.RELEASING, reload_object(node).status) |
282 | |
283 | + def test_POST_acquire_passes_comment(self): |
284 | + factory.make_Node( |
285 | + status=NODE_STATUS.READY, power_type='ipmi', |
286 | + power_state=POWER_STATE.ON, with_boot_disk=True) |
287 | + node_method = self.patch(node_module.Node, 'acquire') |
288 | + comment = factory.make_name('comment') |
289 | + self.client.post( |
290 | + reverse('nodes_handler'), |
291 | + {'op': 'acquire', 'comment': comment}) |
292 | + self.assertThat( |
293 | + node_method, MockCalledOnceWith( |
294 | + ANY, ANY, agent_name=ANY, storage_layout=ANY, |
295 | + storage_layout_params=ANY, comment=comment)) |
296 | + |
297 | + def test_POST_acquire_handles_missing_comment(self): |
298 | + factory.make_Node( |
299 | + status=NODE_STATUS.READY, power_type='ipmi', |
300 | + power_state=POWER_STATE.ON, with_boot_disk=True) |
301 | + node_method = self.patch(node_module.Node, 'acquire') |
302 | + self.client.post( |
303 | + reverse('nodes_handler'), {'op': 'acquire'}) |
304 | + self.assertThat( |
305 | + node_method, MockCalledOnceWith( |
306 | + ANY, ANY, agent_name=ANY, storage_layout=ANY, |
307 | + storage_layout_params=ANY, comment=None)) |
308 | + |
309 | def test_POST_release_frees_hwe_kernel(self): |
310 | self.patch(node_module, 'power_off_node') |
311 | self.patch(node_module.Node, 'start_transition_monitor') |
312 | @@ -733,6 +811,32 @@ |
313 | self.assertEqual(NODE_STATUS.RELEASING, reload_object(node).status) |
314 | self.assertEqual(None, reload_object(node).hwe_kernel) |
315 | |
316 | + def test_POST_release_passes_comment(self): |
317 | + node = factory.make_Node( |
318 | + status=NODE_STATUS.ALLOCATED, owner=factory.make_User(), |
319 | + power_type='ipmi', power_state=POWER_STATE.OFF) |
320 | + self.become_admin() |
321 | + comment = factory.make_name('comment') |
322 | + node_release = self.patch(node_module.Node, 'release_or_erase') |
323 | + self.client.post( |
324 | + self.get_node_uri(node), |
325 | + {'op': 'release', 'comment': comment}) |
326 | + self.assertThat( |
327 | + node_release, |
328 | + MockCalledOnceWith(self.logged_in_user, comment)) |
329 | + |
330 | + def test_POST_release_handles_missing_comment(self): |
331 | + node = factory.make_Node( |
332 | + status=NODE_STATUS.ALLOCATED, owner=factory.make_User(), |
333 | + power_type='ipmi', power_state=POWER_STATE.OFF) |
334 | + self.become_admin() |
335 | + node_release = self.patch(node_module.Node, 'release_or_erase') |
336 | + self.client.post( |
337 | + self.get_node_uri(node), {'op': 'release'}) |
338 | + self.assertThat( |
339 | + node_release, |
340 | + MockCalledOnceWith(self.logged_in_user, None)) |
341 | + |
342 | def test_POST_commission_commissions_node(self): |
343 | node = factory.make_Node( |
344 | status=NODE_STATUS.READY, owner=factory.make_User(), |
345 | @@ -1689,9 +1793,26 @@ |
346 | self.assertEqual(NODE_STATUS.BROKEN, reload_object(node).status) |
347 | |
348 | def test_mark_broken_updates_error_description(self): |
349 | - node = factory.make_Node( |
350 | - status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user) |
351 | - error_description = factory.make_name('error-description') |
352 | + # 'error_description' parameter was renamed 'comment' for consistency |
353 | + # make sure this comment updates the node's error_description |
354 | + node = factory.make_Node( |
355 | + status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user) |
356 | + comment = factory.make_name('comment') |
357 | + response = self.client.post( |
358 | + self.get_node_uri(node), |
359 | + {'op': 'mark_broken', 'comment': comment}) |
360 | + self.assertEqual(httplib.OK, response.status_code) |
361 | + node = reload_object(node) |
362 | + self.assertEqual( |
363 | + (NODE_STATUS.BROKEN, comment), |
364 | + (node.status, node.error_description) |
365 | + ) |
366 | + |
367 | + def test_mark_broken_updates_error_description_compatibility(self): |
368 | + # test old 'error_description' parameter is honored for compatibility |
369 | + node = factory.make_Node( |
370 | + status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user) |
371 | + error_description = factory.make_name('error_description') |
372 | response = self.client.post( |
373 | self.get_node_uri(node), |
374 | {'op': 'mark_broken', 'error_description': error_description}) |
375 | @@ -1702,6 +1823,28 @@ |
376 | (node.status, node.error_description) |
377 | ) |
378 | |
379 | + def test_mark_broken_passes_comment(self): |
380 | + node = factory.make_Node( |
381 | + status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user) |
382 | + node_mark_broken = self.patch(node_module.Node, 'mark_broken') |
383 | + comment = factory.make_name('comment') |
384 | + self.client.post( |
385 | + self.get_node_uri(node), |
386 | + {'op': 'mark_broken', 'comment': comment}) |
387 | + self.assertThat( |
388 | + node_mark_broken, |
389 | + MockCalledOnceWith(self.logged_in_user, comment)) |
390 | + |
391 | + def test_mark_broken_handles_missing_comment(self): |
392 | + node = factory.make_Node( |
393 | + status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user) |
394 | + node_mark_broken = self.patch(node_module.Node, 'mark_broken') |
395 | + self.client.post( |
396 | + self.get_node_uri(node), {'op': 'mark_broken'}) |
397 | + self.assertThat( |
398 | + node_mark_broken, |
399 | + MockCalledOnceWith(self.logged_in_user, None)) |
400 | + |
401 | def test_mark_broken_requires_ownership(self): |
402 | node = factory.make_Node(status=NODE_STATUS.COMMISSIONING) |
403 | response = self.client.post( |
404 | @@ -1742,6 +1885,28 @@ |
405 | self.get_node_uri(node), {'op': 'mark_fixed'}) |
406 | self.assertEqual(httplib.FORBIDDEN, response.status_code) |
407 | |
408 | + def test_mark_fixed_passes_comment(self): |
409 | + self.become_admin() |
410 | + node = factory.make_Node(status=NODE_STATUS.BROKEN) |
411 | + node_mark_fixed = self.patch(node_module.Node, 'mark_fixed') |
412 | + comment = factory.make_name('comment') |
413 | + self.client.post( |
414 | + self.get_node_uri(node), |
415 | + {'op': 'mark_fixed', 'comment': comment}) |
416 | + self.assertThat( |
417 | + node_mark_fixed, |
418 | + MockCalledOnceWith(self.logged_in_user, comment)) |
419 | + |
420 | + def test_mark_fixed_handles_missing_comment(self): |
421 | + self.become_admin() |
422 | + node = factory.make_Node(status=NODE_STATUS.BROKEN) |
423 | + node_mark_fixed = self.patch(node_module.Node, 'mark_fixed') |
424 | + self.client.post( |
425 | + self.get_node_uri(node), {'op': 'mark_fixed'}) |
426 | + self.assertThat( |
427 | + node_mark_fixed, |
428 | + MockCalledOnceWith(self.logged_in_user, None)) |
429 | + |
430 | |
431 | class TestPowerParameters(APITestCase): |
432 | def get_node_uri(self, node): |
433 | @@ -1801,6 +1966,30 @@ |
434 | self.get_node_uri(node), {'op': 'abort_operation'}) |
435 | self.assertEqual(httplib.FORBIDDEN, response.status_code) |
436 | |
437 | + def test_abort_operation_passes_comment(self): |
438 | + self.become_admin() |
439 | + node = factory.make_Node( |
440 | + status=NODE_STATUS.DISK_ERASING, owner=self.logged_in_user) |
441 | + node_method = self.patch(node_module.Node, 'abort_operation') |
442 | + comment = factory.make_name('comment') |
443 | + self.client.post( |
444 | + self.get_node_uri(node), |
445 | + {'op': 'abort_operation', 'comment': comment}) |
446 | + self.assertThat( |
447 | + node_method, |
448 | + MockCalledOnceWith(self.logged_in_user, comment)) |
449 | + |
450 | + def test_abort_operation_handles_missing_comment(self): |
451 | + self.become_admin() |
452 | + node = factory.make_Node( |
453 | + status=NODE_STATUS.DISK_ERASING, owner=self.logged_in_user) |
454 | + node_method = self.patch(node_module.Node, 'abort_operation') |
455 | + self.client.post( |
456 | + self.get_node_uri(node), {'op': 'abort_operation'}) |
457 | + self.assertThat( |
458 | + node_method, |
459 | + MockCalledOnceWith(self.logged_in_user, None)) |
460 | + |
461 | |
462 | class TestSetStorageLayout(APITestCase): |
463 | |
464 | |
465 | === modified file 'src/maasserver/api/tests/test_nodes.py' |
466 | --- src/maasserver/api/tests/test_nodes.py 2015-08-29 03:27:16 +0000 |
467 | +++ src/maasserver/api/tests/test_nodes.py 2015-09-18 17:34:17 +0000 |
468 | @@ -1357,7 +1357,7 @@ |
469 | mock_acquire, |
470 | MockCalledOnceWith( |
471 | ANY, ANY, agent_name=ANY, storage_layout='flat', |
472 | - storage_layout_params={'root_size': '50%'})) |
473 | + storage_layout_params={'root_size': '50%'}, comment=None)) |
474 | |
475 | def test_POST_accept_gets_node_out_of_declared_state(self): |
476 | # This will change when we add provisioning. Until then, |
Looks good. Just one issue with breaking compatibility. I am not going to prevent you from landing the branch just because of that issue. Just make sure to fix it and add a test for it.