Merge lp:~jtv/maas/bug-987585 into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 514
Proposed branch: lp:~jtv/maas/bug-987585
Merge into: lp:~maas-committers/maas/trunk
Prerequisite: lp:~jtv/maas/pre-987585-cosmetics-and-test-helper
Diff against target: 238 lines (+81/-22)
4 files modified
src/maasserver/forms.py (+28/-8)
src/maasserver/tests/test_forms.py (+16/-2)
src/maasserver/tests/test_views_nodes.py (+20/-11)
src/maasserver/views/nodes.py (+17/-1)
To merge this branch: bzr merge lp:~jtv/maas/bug-987585
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+103332@code.launchpad.net

Commit message

Make “Start node” action acquire the node, if it was Ready.

Description of the change

The need for this was discussed with Julian, if memory serves. When applied to a node in Ready state, the Start Node action is meant not just to start it up but to acquire it first. This is of course not desired if it's already in Allocated state. (These are the two states for which the action is offered). The obvious solution, given how node actions are defined, is to define slightly different actions for the two original node states.

Acquiring a node involves grabbing the OAuth key that was used to make the request. We might get away with grabbing any old key for the user, but it seemed arbitrary; what if it's an obsolete one? So instead, I extended the actions methods to take the UI request as an argument.

You'll note that I changed the feedback messages. I wanted to make the difference between “I allocated this node for you and started it up” and “you have just told this node to start” explicitly clear; I hope the former conveys a proportionate sense of assumed responsibility. The text “Node started” text was also a bit optimistic in two respects: first, it seemed to imply that startup had already completed and second, it appears to exclude any possibility that the node might not comply. And so I changed the wording to say that the node has been _asked_ to start up.

Since there are conceivable (if hopefully improbable) circumstances under which a node action might fail, I also defined a new rule (which I also discussed with Raphaël): as a special favour, a node action may raise a MAASAPIException on failure. Normally such an exception in a non-API request would appear to the client as a server error, but the view responsible for node actions will catch it, extract its status code, and return it to the client in at least a somewhat friendly way.

Jeroen

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Looks good :)

[1]

+def start_commissioning_node(node, user, request=None):
+ """Start the commissioning process for a node."""
+ Node.start_commissioning(node, user)

node.start_commissioning(user) ?

[2]

+ notices = '\n'.join([
+ message.message for message in response.context['messages']])

No need for the [].

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

You came back in the middle of the night to unblock me! That's very much appreciated.

> [1]
>
> +def start_commissioning_node(node, user, request=None):
> + """Start the commissioning process for a node."""
> + Node.start_commissioning(node, user)
>
> node.start_commissioning(user) ?

Whoops. Definitely. This'll work, but it's pointless to do it that way. And it wasn't covered by tests, so I added a form test for the action.

> [2]
>
> + notices = '\n'.join([
> + message.message for message in response.context['messages']])
>
> No need for the [].

Right you are.

Revision history for this message
Francis J. Lacoste (flacoste) wrote :

Can we get this backported on the released branch also? I think this is a good thing to have as part of the SRU.

Revision history for this message
Francis J. Lacoste (flacoste) wrote :

> When applied to a node in Ready state, the Start Node action is meant not just to start it up but to acquire it first.
> This is of course not desired if it's already in Allocated state. (These are the two states for which the action is
> offered).

"Start node" shouldn't be available when the node is allocated. Although from an API possible, it's possible to acquire a node without starting it, in practice, there is no way for the user to achieve that (except by making API calls).

If you use juju bootstrap or juju deploy, the nodes will show up as 'Allocated' even though they were acquired _and_ started.

We should probably couple acquire and start, or use a separate status for nodes that were started.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Wednesday 25 April 2012 19:55:21 you wrote:
> We should probably couple acquire and start, or use a separate status for
> nodes that were started.

+1 for a separate status. It would be good to track this, as we might want to
power off stuff we started at some point.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Wednesday 25 April 2012 16:29:02 you wrote:
> Can we get this backported on the released branch also? I think this is a
> good thing to have as part of the SRU.

Provided you don't mean that this branch is going to be part of 12.04.1? I
want to cut a new branch for that as we're making *extensive* changes. If
those changes are too much for SRU then we have a problem and need to talk
ASAP.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

This one's not quite so easy to backport. It changes the node-action signature, and a significant portion of the changes are in code that is in different files in trunk than it was in 1.0.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

ISTM the new status should be not “started” but “deploying” — which the node would be in until it had installed its new operating system and disabled netboot. The allocating user would already be the owner of the node at this point, so that their SSH keys get set up as part of deployment, but we won't say “this node is ready for use” until we've had some kind of confirmation from the node that it's all ready.

That does mean extra machinery, of course. We may even need feedback signaling or a timeout on deployment like we do with commissioning!

Revision history for this message
Francis J. Lacoste (flacoste) wrote :

On 12-04-25 10:38 PM, Julian Edwards wrote:
> On Wednesday 25 April 2012 16:29:02 you wrote:
>> Can we get this backported on the released branch also? I think this is a
>> good thing to have as part of the SRU.
>
> Provided you don't mean that this branch is going to be part of 12.04.1? I
> want to cut a new branch for that as we're making *extensive* changes. If
> those changes are too much for SRU then we have a problem and need to talk
> ASAP.

What will be in 12.04.01 is still in the air. My initial understanding
was that it would be based on trunk, but it seems the Ubuntu release
team wants to stick to the SRU process and thus it might be just the bug
fixes and other small fixes based on the branch you already cut.

Don't worry about this, if they want to stick to the bug-fix only
branch, it just means that 12.04.01 will be closer to 12.04 than
originally anticipated. We should stick to trunk development, but
backport small fixes that aren't too much of a problem.

What will be 12.04.01 will be settled next week.

--
Francis J. Lacoste
<email address hidden>

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Thursday 26 April 2012 14:16:18 you wrote:
> What will be in 12.04.01 is still in the air. My initial understanding
> was that it would be based on trunk, but it seems the Ubuntu release
> team wants to stick to the SRU process and thus it might be just the bug
> fixes and other small fixes based on the branch you already cut.
>
> Don't worry about this, if they want to stick to the bug-fix only
> branch, it just means that 12.04.01 will be closer to 12.04 than
> originally anticipated. We should stick to trunk development, but
> backport small fixes that aren't too much of a problem.
>
> What will be 12.04.01 will be settled next week.

IMO the 1.0 branch should only contain
 * security fixes
 * serious bug fixes

Most other things are a pain to backport already (jtv wasted a lot of time
backporting something yesterday). If the Ubuntu guys won't take trunk in an
SRU this will be problematic for continuing features being added in 12.04.*

Cheers.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/forms.py'
--- src/maasserver/forms.py 2012-04-24 17:07:03 +0000
+++ src/maasserver/forms.py 2012-04-25 02:26:22 +0000
@@ -232,11 +232,25 @@
232 return node232 return node
233233
234234
235def start_node(node, user):235def start_node(node, user, request=None):
236 """Start a node from the UI. It will have no meta_data."""236 """Start a node from the UI. It will have no meta_data."""
237 Node.objects.start_nodes([node.system_id], user)237 Node.objects.start_nodes([node.system_id], user)
238238
239239
240def acquire_and_start_node(node, user, request=None):
241 """Acquire and start a node from the UI. It will have no meta_data."""
242 # Avoid circular imports.
243 from maasserver.api import get_oauth_token
244
245 node.acquire(get_oauth_token(request))
246 start_node(node=node, user=user, request=request)
247
248
249def start_commissioning_node(node, user, request=None):
250 """Start the commissioning process for a node."""
251 node.start_commissioning(user)
252
253
240# Node actions per status.254# Node actions per status.
241#255#
242# This maps each NODE_STATUS to a list of actions applicable to a node256# This maps each NODE_STATUS to a list of actions applicable to a node
@@ -258,7 +272,11 @@
258# 'display': "Paint node",272# 'display': "Paint node",
259# # Permission required to perform action.273# # Permission required to perform action.
260# 'permission': NODE_PERMISSION.EDIT,274# 'permission': NODE_PERMISSION.EDIT,
261# # Callable that performs action. Takes parameters (node, user).275# # Callable that performs action.
276# # Takes parameters (node, user, request).
277# # Even though this is not the API, the action may raise a
278# # MAASAPIException if it wants to convey failure with a specific
279# # http status code.
262# 'execute': lambda node, user: paint_node(280# 'execute': lambda node, user: paint_node(
263# node, favourite_colour(user)),281# node, favourite_colour(user)),
264# }282# }
@@ -268,16 +286,18 @@
268 {286 {
269 'display': "Accept & commission",287 'display': "Accept & commission",
270 'permission': NODE_PERMISSION.ADMIN,288 'permission': NODE_PERMISSION.ADMIN,
271 'execute': lambda node, user: Node.start_commissioning(node, user),289 'execute': start_commissioning_node,
272 'message': "Node commissioning started."290 'message': "Node commissioning started."
273 },291 },
274 ],292 ],
275 NODE_STATUS.READY: [293 NODE_STATUS.READY: [
276 {294 {
277 'display': "Start node",295 'display': "Start node",
278 'permission': NODE_PERMISSION.EDIT,296 'permission': NODE_PERMISSION.VIEW,
279 'execute': start_node,297 'execute': acquire_and_start_node,
280 'message': "Node started."298 'message': (
299 "This node is now allocated to you. "
300 "It has been asked to start up."),
281 },301 },
282 ],302 ],
283 NODE_STATUS.ALLOCATED: [303 NODE_STATUS.ALLOCATED: [
@@ -285,7 +305,7 @@
285 'display': "Start node",305 'display': "Start node",
286 'permission': NODE_PERMISSION.EDIT,306 'permission': NODE_PERMISSION.EDIT,
287 'execute': start_node,307 'execute': start_node,
288 'message': "Node started."308 'message': "The node has been asked to start up.",
289 },309 },
290 ],310 ],
291}311}
@@ -346,7 +366,7 @@
346 raise PermissionDenied()366 raise PermissionDenied()
347 if not self.user.has_perm(permission, self.node):367 if not self.user.has_perm(permission, self.node):
348 raise PermissionDenied()368 raise PermissionDenied()
349 execute(self.node, self.user)369 execute(node=self.node, user=self.user, request=self.request)
350 self.display_message(message)370 self.display_message(message)
351371
352372
353373
=== modified file 'src/maasserver/tests/test_forms.py'
--- src/maasserver/tests/test_forms.py 2012-04-24 17:07:03 +0000
+++ src/maasserver/tests/test_forms.py 2012-04-25 02:26:22 +0000
@@ -18,6 +18,7 @@
18 ValidationError,18 ValidationError,
19 )19 )
20from django.http import QueryDict20from django.http import QueryDict
21import maasserver.api
21from maasserver.enum import (22from maasserver.enum import (
22 ARCHITECTURE,23 ARCHITECTURE,
23 NODE_AFTER_COMMISSIONING_ACTION_CHOICES,24 NODE_AFTER_COMMISSIONING_ACTION_CHOICES,
@@ -340,14 +341,19 @@
340341
341 self.assertRaises(PermissionDenied, form.save)342 self.assertRaises(PermissionDenied, form.save)
342343
343 def test_start_action_starts_ready_node_for_admin(self):344 def test_start_action_acquires_and_starts_ready_node_for_user(self):
344 node = factory.make_node(status=NODE_STATUS.READY)345 node = factory.make_node(status=NODE_STATUS.READY)
345 form = get_action_form(factory.make_admin())(346 user = factory.make_user()
347 consumer, token = user.get_profile().create_authorisation_token()
348 self.patch(maasserver.api, 'get_oauth_token', lambda request: token)
349 form = get_action_form(user)(
346 node, {NodeActionForm.input_name: "Start node"})350 node, {NodeActionForm.input_name: "Start node"})
347 form.save()351 form.save()
348352
349 power_status = get_provisioning_api_proxy().power_status353 power_status = get_provisioning_api_proxy().power_status
350 self.assertEqual('start', power_status.get(node.system_id))354 self.assertEqual('start', power_status.get(node.system_id))
355 self.assertEqual(NODE_STATUS.ALLOCATED, node.status)
356 self.assertEqual(user, node.owner)
351357
352 def test_start_action_starts_allocated_node_for_owner(self):358 def test_start_action_starts_allocated_node_for_owner(self):
353 node = factory.make_node(359 node = factory.make_node(
@@ -359,6 +365,14 @@
359 power_status = get_provisioning_api_proxy().power_status365 power_status = get_provisioning_api_proxy().power_status
360 self.assertEqual('start', power_status.get(node.system_id))366 self.assertEqual('start', power_status.get(node.system_id))
361367
368 def test_accept_and_commission_starts_commissioning(self):
369 admin = factory.make_admin()
370 node = factory.make_node(status=NODE_STATUS.DECLARED)
371 form = get_action_form(admin)(
372 node, {NodeActionForm.input_name: "Accept & commission"})
373 form.save()
374 self.assertEqual(NODE_STATUS.COMMISSIONING, node.status)
375
362376
363class TestHostnameFormField(TestCase):377class TestHostnameFormField(TestCase):
364378
365379
=== modified file 'src/maasserver/tests/test_views_nodes.py'
--- src/maasserver/tests/test_views_nodes.py 2012-04-24 17:07:03 +0000
+++ src/maasserver/tests/test_views_nodes.py 2012-04-25 02:26:22 +0000
@@ -19,6 +19,7 @@
19from django.core.urlresolvers import reverse19from django.core.urlresolvers import reverse
20from lxml.html import fromstring20from lxml.html import fromstring
21from maasserver import messages21from maasserver import messages
22import maasserver.api
22from maasserver.enum import (23from maasserver.enum import (
23 NODE_AFTER_COMMISSIONING_ACTION,24 NODE_AFTER_COMMISSIONING_ACTION,
24 NODE_STATUS,25 NODE_STATUS,
@@ -280,7 +281,7 @@
280 if redirect != node_link:281 if redirect != node_link:
281 self.fail(282 self.fail(
282 "Odd: POST on %s redirected to %s." % (node_link, redirect))283 "Odd: POST on %s redirected to %s." % (node_link, redirect))
283 return self.client.get(node_link)284 return self.client.get(redirect)
284285
285 def test_start_commisioning_displays_message(self):286 def test_start_commisioning_displays_message(self):
286 self.become_admin()287 self.become_admin()
@@ -292,23 +293,31 @@
292 [message.message for message in response.context['messages']])293 [message.message for message in response.context['messages']])
293294
294 def test_start_node_from_ready_displays_message(self):295 def test_start_node_from_ready_displays_message(self):
295 node = factory.make_node(296 profile = self.logged_in_user.get_profile()
296 status=NODE_STATUS.READY, owner=self.logged_in_user)297 consumer, token = profile.create_authorisation_token()
297 response = self.perform_action_and_get_node_page(298 self.patch(maasserver.api, 'get_oauth_token', lambda request: token)
298 node, "Start node")299 node = factory.make_node(status=NODE_STATUS.READY)
299 self.assertIn(300 response = self.perform_action_and_get_node_page(node, "Start node")
300 "Node started.",301 notices = '\n'.join(
301 [message.message for message in response.context['messages']])302 message.message for message in response.context['messages'])
303 self.assertIn("This node is now allocated to you.", notices)
304 self.assertIn("asked to start up.", notices)
302305
303 def test_start_node_from_allocated_displays_message(self):306 def test_start_node_from_allocated_displays_message(self):
304 node = factory.make_node(307 node = factory.make_node(
305 status=NODE_STATUS.ALLOCATED, owner=self.logged_in_user)308 status=NODE_STATUS.ALLOCATED, owner=self.logged_in_user)
306 response = self.perform_action_and_get_node_page(309 response = self.perform_action_and_get_node_page(node, "Start node")
307 node, "Start node")
308 self.assertEqual(310 self.assertEqual(
309 ["Node started."],311 ["The node has been asked to start up."],
310 [message.message for message in response.context['messages']])312 [message.message for message in response.context['messages']])
311313
314 def test_start_node_without_auth_returns_Unauthorized(self):
315 node = factory.make_node(status=NODE_STATUS.READY)
316 response = self.client.post(
317 reverse('node-view', args=[node.system_id]),
318 data={NodeActionForm.input_name: "Start node"})
319 self.assertEqual(httplib.UNAUTHORIZED, response.status_code)
320
312321
313class AdminNodeViewsTest(AdminLoggedInTestCase):322class AdminNodeViewsTest(AdminLoggedInTestCase):
314323
315324
=== modified file 'src/maasserver/views/nodes.py'
--- src/maasserver/views/nodes.py 2012-04-24 17:07:03 +0000
+++ src/maasserver/views/nodes.py 2012-04-25 02:26:22 +0000
@@ -22,6 +22,7 @@
22from django.contrib import messages22from django.contrib import messages
23from django.core.exceptions import PermissionDenied23from django.core.exceptions import PermissionDenied
24from django.core.urlresolvers import reverse24from django.core.urlresolvers import reverse
25from django.http import HttpResponse
25from django.utils.safestring import mark_safe26from django.utils.safestring import mark_safe
26from django.views.generic import (27from django.views.generic import (
27 ListView,28 ListView,
@@ -31,7 +32,10 @@
31 NODE_PERMISSION,32 NODE_PERMISSION,
32 NODE_STATUS,33 NODE_STATUS,
33 )34 )
34from maasserver.exceptions import NoRabbit35from maasserver.exceptions import (
36 MAASAPIException,
37 NoRabbit,
38 )
35from maasserver.forms import (39from maasserver.forms import (
36 get_action_form,40 get_action_form,
37 UIAdminNodeEditForm,41 UIAdminNodeEditForm,
@@ -114,6 +118,18 @@
114 node.error if node.status != NODE_STATUS.FAILED_TESTS else None)118 node.error if node.status != NODE_STATUS.FAILED_TESTS else None)
115 return context119 return context
116120
121 def dispatch(self, *args, **kwargs):
122 """Override from Django `View`: Handle MAAS exceptions.
123
124 Node actions may raise exceptions derived from
125 :class:`MAASAPIException`. This type of exception contains an
126 http status code that we will forward to the client.
127 """
128 try:
129 return super(NodeView, self).dispatch(*args, **kwargs)
130 except MAASAPIException as e:
131 return HttpResponse(unicode(e), status=e.api_error)
132
117 def get_success_url(self):133 def get_success_url(self):
118 return reverse('node-view', args=[self.get_object().system_id])134 return reverse('node-view', args=[self.get_object().system_id])
119135