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
1=== modified file 'src/maasserver/forms.py'
2--- src/maasserver/forms.py 2012-04-24 17:07:03 +0000
3+++ src/maasserver/forms.py 2012-04-25 02:26:22 +0000
4@@ -232,11 +232,25 @@
5 return node
6
7
8-def start_node(node, user):
9+def start_node(node, user, request=None):
10 """Start a node from the UI. It will have no meta_data."""
11 Node.objects.start_nodes([node.system_id], user)
12
13
14+def acquire_and_start_node(node, user, request=None):
15+ """Acquire and start a node from the UI. It will have no meta_data."""
16+ # Avoid circular imports.
17+ from maasserver.api import get_oauth_token
18+
19+ node.acquire(get_oauth_token(request))
20+ start_node(node=node, user=user, request=request)
21+
22+
23+def start_commissioning_node(node, user, request=None):
24+ """Start the commissioning process for a node."""
25+ node.start_commissioning(user)
26+
27+
28 # Node actions per status.
29 #
30 # This maps each NODE_STATUS to a list of actions applicable to a node
31@@ -258,7 +272,11 @@
32 # 'display': "Paint node",
33 # # Permission required to perform action.
34 # 'permission': NODE_PERMISSION.EDIT,
35-# # Callable that performs action. Takes parameters (node, user).
36+# # Callable that performs action.
37+# # Takes parameters (node, user, request).
38+# # Even though this is not the API, the action may raise a
39+# # MAASAPIException if it wants to convey failure with a specific
40+# # http status code.
41 # 'execute': lambda node, user: paint_node(
42 # node, favourite_colour(user)),
43 # }
44@@ -268,16 +286,18 @@
45 {
46 'display': "Accept & commission",
47 'permission': NODE_PERMISSION.ADMIN,
48- 'execute': lambda node, user: Node.start_commissioning(node, user),
49+ 'execute': start_commissioning_node,
50 'message': "Node commissioning started."
51 },
52 ],
53 NODE_STATUS.READY: [
54 {
55 'display': "Start node",
56- 'permission': NODE_PERMISSION.EDIT,
57- 'execute': start_node,
58- 'message': "Node started."
59+ 'permission': NODE_PERMISSION.VIEW,
60+ 'execute': acquire_and_start_node,
61+ 'message': (
62+ "This node is now allocated to you. "
63+ "It has been asked to start up."),
64 },
65 ],
66 NODE_STATUS.ALLOCATED: [
67@@ -285,7 +305,7 @@
68 'display': "Start node",
69 'permission': NODE_PERMISSION.EDIT,
70 'execute': start_node,
71- 'message': "Node started."
72+ 'message': "The node has been asked to start up.",
73 },
74 ],
75 }
76@@ -346,7 +366,7 @@
77 raise PermissionDenied()
78 if not self.user.has_perm(permission, self.node):
79 raise PermissionDenied()
80- execute(self.node, self.user)
81+ execute(node=self.node, user=self.user, request=self.request)
82 self.display_message(message)
83
84
85
86=== modified file 'src/maasserver/tests/test_forms.py'
87--- src/maasserver/tests/test_forms.py 2012-04-24 17:07:03 +0000
88+++ src/maasserver/tests/test_forms.py 2012-04-25 02:26:22 +0000
89@@ -18,6 +18,7 @@
90 ValidationError,
91 )
92 from django.http import QueryDict
93+import maasserver.api
94 from maasserver.enum import (
95 ARCHITECTURE,
96 NODE_AFTER_COMMISSIONING_ACTION_CHOICES,
97@@ -340,14 +341,19 @@
98
99 self.assertRaises(PermissionDenied, form.save)
100
101- def test_start_action_starts_ready_node_for_admin(self):
102+ def test_start_action_acquires_and_starts_ready_node_for_user(self):
103 node = factory.make_node(status=NODE_STATUS.READY)
104- form = get_action_form(factory.make_admin())(
105+ user = factory.make_user()
106+ consumer, token = user.get_profile().create_authorisation_token()
107+ self.patch(maasserver.api, 'get_oauth_token', lambda request: token)
108+ form = get_action_form(user)(
109 node, {NodeActionForm.input_name: "Start node"})
110 form.save()
111
112 power_status = get_provisioning_api_proxy().power_status
113 self.assertEqual('start', power_status.get(node.system_id))
114+ self.assertEqual(NODE_STATUS.ALLOCATED, node.status)
115+ self.assertEqual(user, node.owner)
116
117 def test_start_action_starts_allocated_node_for_owner(self):
118 node = factory.make_node(
119@@ -359,6 +365,14 @@
120 power_status = get_provisioning_api_proxy().power_status
121 self.assertEqual('start', power_status.get(node.system_id))
122
123+ def test_accept_and_commission_starts_commissioning(self):
124+ admin = factory.make_admin()
125+ node = factory.make_node(status=NODE_STATUS.DECLARED)
126+ form = get_action_form(admin)(
127+ node, {NodeActionForm.input_name: "Accept & commission"})
128+ form.save()
129+ self.assertEqual(NODE_STATUS.COMMISSIONING, node.status)
130+
131
132 class TestHostnameFormField(TestCase):
133
134
135=== modified file 'src/maasserver/tests/test_views_nodes.py'
136--- src/maasserver/tests/test_views_nodes.py 2012-04-24 17:07:03 +0000
137+++ src/maasserver/tests/test_views_nodes.py 2012-04-25 02:26:22 +0000
138@@ -19,6 +19,7 @@
139 from django.core.urlresolvers import reverse
140 from lxml.html import fromstring
141 from maasserver import messages
142+import maasserver.api
143 from maasserver.enum import (
144 NODE_AFTER_COMMISSIONING_ACTION,
145 NODE_STATUS,
146@@ -280,7 +281,7 @@
147 if redirect != node_link:
148 self.fail(
149 "Odd: POST on %s redirected to %s." % (node_link, redirect))
150- return self.client.get(node_link)
151+ return self.client.get(redirect)
152
153 def test_start_commisioning_displays_message(self):
154 self.become_admin()
155@@ -292,23 +293,31 @@
156 [message.message for message in response.context['messages']])
157
158 def test_start_node_from_ready_displays_message(self):
159- node = factory.make_node(
160- status=NODE_STATUS.READY, owner=self.logged_in_user)
161- response = self.perform_action_and_get_node_page(
162- node, "Start node")
163- self.assertIn(
164- "Node started.",
165- [message.message for message in response.context['messages']])
166+ profile = self.logged_in_user.get_profile()
167+ consumer, token = profile.create_authorisation_token()
168+ self.patch(maasserver.api, 'get_oauth_token', lambda request: token)
169+ node = factory.make_node(status=NODE_STATUS.READY)
170+ response = self.perform_action_and_get_node_page(node, "Start node")
171+ notices = '\n'.join(
172+ message.message for message in response.context['messages'])
173+ self.assertIn("This node is now allocated to you.", notices)
174+ self.assertIn("asked to start up.", notices)
175
176 def test_start_node_from_allocated_displays_message(self):
177 node = factory.make_node(
178 status=NODE_STATUS.ALLOCATED, owner=self.logged_in_user)
179- response = self.perform_action_and_get_node_page(
180- node, "Start node")
181+ response = self.perform_action_and_get_node_page(node, "Start node")
182 self.assertEqual(
183- ["Node started."],
184+ ["The node has been asked to start up."],
185 [message.message for message in response.context['messages']])
186
187+ def test_start_node_without_auth_returns_Unauthorized(self):
188+ node = factory.make_node(status=NODE_STATUS.READY)
189+ response = self.client.post(
190+ reverse('node-view', args=[node.system_id]),
191+ data={NodeActionForm.input_name: "Start node"})
192+ self.assertEqual(httplib.UNAUTHORIZED, response.status_code)
193+
194
195 class AdminNodeViewsTest(AdminLoggedInTestCase):
196
197
198=== modified file 'src/maasserver/views/nodes.py'
199--- src/maasserver/views/nodes.py 2012-04-24 17:07:03 +0000
200+++ src/maasserver/views/nodes.py 2012-04-25 02:26:22 +0000
201@@ -22,6 +22,7 @@
202 from django.contrib import messages
203 from django.core.exceptions import PermissionDenied
204 from django.core.urlresolvers import reverse
205+from django.http import HttpResponse
206 from django.utils.safestring import mark_safe
207 from django.views.generic import (
208 ListView,
209@@ -31,7 +32,10 @@
210 NODE_PERMISSION,
211 NODE_STATUS,
212 )
213-from maasserver.exceptions import NoRabbit
214+from maasserver.exceptions import (
215+ MAASAPIException,
216+ NoRabbit,
217+ )
218 from maasserver.forms import (
219 get_action_form,
220 UIAdminNodeEditForm,
221@@ -114,6 +118,18 @@
222 node.error if node.status != NODE_STATUS.FAILED_TESTS else None)
223 return context
224
225+ def dispatch(self, *args, **kwargs):
226+ """Override from Django `View`: Handle MAAS exceptions.
227+
228+ Node actions may raise exceptions derived from
229+ :class:`MAASAPIException`. This type of exception contains an
230+ http status code that we will forward to the client.
231+ """
232+ try:
233+ return super(NodeView, self).dispatch(*args, **kwargs)
234+ except MAASAPIException as e:
235+ return HttpResponse(unicode(e), status=e.api_error)
236+
237 def get_success_url(self):
238 return reverse('node-view', args=[self.get_object().system_id])
239