Merge lp:~trapnine/maas/events-2 into lp:~maas-committers/maas/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
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.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

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.

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/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,