Merge lp:~julian-edwards/maas/only-ready-when-power-is-off into lp:~maas-committers/maas/trunk
- only-ready-when-power-is-off
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Julian Edwards | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 2994 | ||||
Proposed branch: | lp:~julian-edwards/maas/only-ready-when-power-is-off | ||||
Merge into: | lp:~maas-committers/maas/trunk | ||||
Diff against target: |
392 lines (+107/-82) 6 files modified
src/maasserver/api/tests/test_node.py (+8/-59) src/maasserver/enum.py (+3/-0) src/maasserver/models/node.py (+29/-2) src/maasserver/models/tests/test_node.py (+57/-13) src/maasserver/node_action.py (+0/-1) src/maasserver/tests/test_node_action.py (+10/-7) |
||||
To merge this branch: | bzr merge lp:~julian-edwards/maas/only-ready-when-power-is-off | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jeroen T. Vermeulen (community) | Approve | ||
Review via email:
|
Commit message
Only mark nodes READY once power off is complete. It does this by introducing a new state, RELEASING, which is transitioned to READY when the power job calls back successfully. This does not affect the transition to BROKEN if the power job failed.
Description of the change
There was some collateral with this branch, my changed exposed some other potential problems some of which I fixed:
- release() always tries to power off the node, even though it may have never been powered on. I've left this as is but I welcome your comments as the reviewer here.
- release() was being unit tested via the API (!) so I removed all of those tests in favour of a more simple enhanced unit test.
- The start node action could only operate on ALLOCATED or DEPLOYED nodes, however if it got a StaticIPAddress
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Julian Edwards (julian-edwards) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Jeroen T. Vermeulen (jtv) wrote : | # |
Thanks for cleaning up the tests. We have far too much logic, and tests, at the API level.
I think it's OK for release() to power off the node always, because our knowledge of its current power state may be out of date. I doubt we control all state transitions thoroughly at the moment, so there may be obscure paths like: node is switched off; power poller registers that it's off; node is marked as broken; power poller stops looking at the node because it's broken; admin powers node on and works their magic; admin marks node as fixed; user allocates node; MAAS issues power-on command; node misses the command because it's already on.
So as far as I'm concerned, the XXX comment can become just a regular comment note. In fact I probably wouldn't even bother with a special code path for the case where the node is already off: the power method can figure out that it's not needed, or if it prefers, it can issue a probably redundant power-off command for luck.
And looking at the code under that XXX comment, does it make sense to keep the owner set when releasing a node? The user is done with the node at that point. For debugging it may well be interesting to know who was using it, but that's historical information — maybe it belongs in the node event log. So I think I'd just clear the node's owner during release() either way.
One thing is completely unclear to me: what does the RESERVED state represent? It may have come up in London but if so I don't remember. There's a comment in the enum that talks about the node being ready for “named” deployment. I have no idea what that means. I'm not even sure whether it's talking about a DNS daemon or the normal adjective. I don't see any place in the code where a node might enter this state. Yet your test uses this state for a powered-off node and not for a powered-on node... Why? What's the distinction?
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Raphaël Badin (rvb) wrote : | # |
> I tested this locally and it seems to be ok, you can't start the node again until the poweroff
> script finishes.
What happens if powering the machine off fails? (if the template blows up or if the status of the node is never changed?) I'd say we need to introduce a RELEASING_FAILED state but I'd like to hear your thoughts on this.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Julian Edwards (julian-edwards) wrote : | # |
On Monday 15 Sep 2014 07:22:08 you wrote:
> > I tested this locally and it seems to be ok, you can't start the node
> > again until the poweroff script finishes.
>
> What happens if powering the machine off fails? (if the template blows up or
> if the status of the node is never changed?) I'd say we need to introduce
> a RELEASING_FAILED state but I'd like to hear your thoughts on this.
It will be marked BROKEN; that's already all been done.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Raphaël Badin (rvb) wrote : | # |
> On Monday 15 Sep 2014 07:22:08 you wrote:
> > > I tested this locally and it seems to be ok, you can't start the node
> > > again until the poweroff script finishes.
> >
> > What happens if powering the machine off fails? (if the template blows up or
> > if the status of the node is never changed?) I'd say we need to introduce
> > a RELEASING_FAILED state but I'd like to hear your thoughts on this.
>
> It will be marked BROKEN; that's already all been done.
That's not what BROKEN means. BROKEN is a state only a manual action from a user should put a node into.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Julian Edwards (julian-edwards) wrote : | # |
On Monday 15 Sep 2014 09:34:27 you wrote:
> > On Monday 15 Sep 2014 07:22:08 you wrote:
> > > > I tested this locally and it seems to be ok, you can't start the node
> > > > again until the poweroff script finishes.
> > >
> > > What happens if powering the machine off fails? (if the template blows
> > > up or if the status of the node is never changed?) I'd say we need to
> > > introduce a RELEASING_FAILED state but I'd like to hear your thoughts
> > > on this.>
> > It will be marked BROKEN; that's already all been done.
>
> That's not what BROKEN means. BROKEN is a state only a manual action from a
> user should put a node into.
Then whoever did the work on the power stuff needs to know that.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Julian Edwards (julian-edwards) wrote : | # |
On Monday 15 Sep 2014 07:07:25 you wrote:
> I think it's OK for release() to power off the node always, because our
> knowledge of its current power state may be out of date. I doubt we
> control all state transitions thoroughly at the moment, so there may be
> obscure paths like: node is switched off; power poller registers that it's
> off; node is marked as broken; power poller stops looking at the node
> because it's broken; admin powers node on and works their magic; admin
> marks node as fixed; user allocates node; MAAS issues power-on command;
> node misses the command because it's already on.
>
> So as far as I'm concerned, the XXX comment can become just a regular
> comment note. In fact I probably wouldn't even bother with a special code
> path for the case where the node is already off: the power method can
> figure out that it's not needed, or if it prefers, it can issue a probably
> redundant power-off command for luck.
I'll try it and see how it works out, but I am concerned it will lead to
concurrent power operations - we must avoid the situation where we need to
serialize them at the power driver level, that's just madness. It's good
practice to catch problems as early as you can!
> And looking at the code under that XXX comment, does it make sense to keep
> the owner set when releasing a node? The user is done with the node at
> that point. For debugging it may well be interesting to know who was using
> it, but that's historical information — maybe it belongs in the node event
> log. So I think I'd just clear the node's owner during release() either
> way.
It's not released until it's off IMO, which is why I kept the owner on. The
state RELEASING implies it's not released yet as well.
> One thing is completely unclear to me: what does the RESERVED state
> represent? It may have come up in London but if so I don't remember.
> There's a comment in the enum that talks about the node being ready for
> “named” deployment. I have no idea what that means. I'm not even sure
> whether it's talking about a DNS daemon or the normal adjective. I don't
> see any place in the code where a node might enter this state. Yet your
> test uses this state for a powered-off node and not for a powered-on
> node... Why? What's the distinction?
It can be deleted as we discussed on the call.
Thanks for the review!
Preview Diff
1 | === modified file 'src/maasserver/api/tests/test_node.py' | |||
2 | --- src/maasserver/api/tests/test_node.py 2014-09-10 16:20:31 +0000 | |||
3 | +++ src/maasserver/api/tests/test_node.py 2014-09-16 01:19:01 +0000 | |||
4 | @@ -47,6 +47,7 @@ | |||
5 | 47 | from maasserver.testing.osystems import make_usable_osystem | 47 | from maasserver.testing.osystems import make_usable_osystem |
6 | 48 | from maasserver.testing.testcase import MAASServerTestCase | 48 | from maasserver.testing.testcase import MAASServerTestCase |
7 | 49 | from maastesting.matchers import ( | 49 | from maastesting.matchers import ( |
8 | 50 | Equals, | ||
9 | 50 | MockCalledOnceWith, | 51 | MockCalledOnceWith, |
10 | 51 | MockNotCalled, | 52 | MockNotCalled, |
11 | 52 | ) | 53 | ) |
12 | @@ -402,7 +403,7 @@ | |||
13 | 402 | [httplib.OK] * len(owned_nodes), | 403 | [httplib.OK] * len(owned_nodes), |
14 | 403 | [response.status_code for response in responses]) | 404 | [response.status_code for response in responses]) |
15 | 404 | self.assertItemsEqual( | 405 | self.assertItemsEqual( |
17 | 405 | [NODE_STATUS.READY] * len(owned_nodes), | 406 | [NODE_STATUS.RELEASING] * len(owned_nodes), |
18 | 406 | [node.status for node in reload_objects(Node, owned_nodes)]) | 407 | [node.status for node in reload_objects(Node, owned_nodes)]) |
19 | 407 | 408 | ||
20 | 408 | def test_POST_release_releases_failed_node(self): | 409 | def test_POST_release_releases_failed_node(self): |
21 | @@ -414,60 +415,8 @@ | |||
22 | 414 | self.assertEqual( | 415 | self.assertEqual( |
23 | 415 | httplib.OK, response.status_code, response.content) | 416 | httplib.OK, response.status_code, response.content) |
24 | 416 | owned_node = Node.objects.get(id=owned_node.id) | 417 | owned_node = Node.objects.get(id=owned_node.id) |
79 | 417 | self.assertEqual( | 418 | self.expectThat(owned_node.status, Equals(NODE_STATUS.RELEASING)) |
80 | 418 | (NODE_STATUS.READY, None), | 419 | self.expectThat(owned_node.owner, Equals(self.logged_in_user)) |
27 | 419 | (owned_node.status, owned_node.owner)) | ||
28 | 420 | |||
29 | 421 | def test_POST_release_turns_on_netboot(self): | ||
30 | 422 | node = factory.make_Node( | ||
31 | 423 | status=NODE_STATUS.ALLOCATED, owner=self.logged_in_user) | ||
32 | 424 | node.set_netboot(on=False) | ||
33 | 425 | self.client.post(self.get_node_uri(node), {'op': 'release'}) | ||
34 | 426 | self.assertTrue(reload_object(node).netboot) | ||
35 | 427 | |||
36 | 428 | def test_POST_release_resets_osystem_and_distro_series(self): | ||
37 | 429 | osystem = factory.pick_OS() | ||
38 | 430 | release = factory.pick_release(osystem) | ||
39 | 431 | node = factory.make_Node( | ||
40 | 432 | status=NODE_STATUS.ALLOCATED, owner=self.logged_in_user, | ||
41 | 433 | osystem=osystem.name, distro_series=release) | ||
42 | 434 | self.client.post(self.get_node_uri(node), {'op': 'release'}) | ||
43 | 435 | self.assertEqual('', reload_object(node).osystem) | ||
44 | 436 | self.assertEqual('', reload_object(node).distro_series) | ||
45 | 437 | |||
46 | 438 | def test_POST_release_resets_license_key(self): | ||
47 | 439 | osystem = factory.pick_OS() | ||
48 | 440 | release = factory.pick_release(osystem) | ||
49 | 441 | license_key = factory.make_string() | ||
50 | 442 | node = factory.make_Node( | ||
51 | 443 | status=NODE_STATUS.ALLOCATED, owner=self.logged_in_user, | ||
52 | 444 | osystem=osystem.name, distro_series=release, | ||
53 | 445 | license_key=license_key) | ||
54 | 446 | self.client.post(self.get_node_uri(node), {'op': 'release'}) | ||
55 | 447 | self.assertEqual('', reload_object(node).license_key) | ||
56 | 448 | |||
57 | 449 | def test_POST_release_resets_agent_name(self): | ||
58 | 450 | agent_name = factory.make_name('agent-name') | ||
59 | 451 | osystem = factory.pick_OS() | ||
60 | 452 | release = factory.pick_release(osystem) | ||
61 | 453 | node = factory.make_Node( | ||
62 | 454 | status=NODE_STATUS.ALLOCATED, owner=self.logged_in_user, | ||
63 | 455 | osystem=osystem.name, distro_series=release, | ||
64 | 456 | agent_name=agent_name) | ||
65 | 457 | self.client.post(self.get_node_uri(node), {'op': 'release'}) | ||
66 | 458 | self.assertEqual('', reload_object(node).agent_name) | ||
67 | 459 | |||
68 | 460 | def test_POST_release_removes_token_and_user(self): | ||
69 | 461 | node = factory.make_Node(status=NODE_STATUS.READY) | ||
70 | 462 | self.client.post(reverse('nodes_handler'), {'op': 'acquire'}) | ||
71 | 463 | node = Node.objects.get(system_id=node.system_id) | ||
72 | 464 | self.assertEqual(NODE_STATUS.ALLOCATED, node.status) | ||
73 | 465 | self.assertEqual(self.logged_in_user, node.owner) | ||
74 | 466 | self.assertEqual(self.client.token.key, node.token.key) | ||
75 | 467 | self.client.post(self.get_node_uri(node), {'op': 'release'}) | ||
76 | 468 | node = Node.objects.get(system_id=node.system_id) | ||
77 | 469 | self.assertIs(None, node.owner) | ||
78 | 470 | self.assertIs(None, node.token) | ||
81 | 471 | 420 | ||
82 | 472 | def test_POST_release_does_nothing_for_unowned_node(self): | 421 | def test_POST_release_does_nothing_for_unowned_node(self): |
83 | 473 | node = factory.make_Node( | 422 | node = factory.make_Node( |
84 | @@ -530,8 +479,8 @@ | |||
85 | 530 | self.become_admin() | 479 | self.become_admin() |
86 | 531 | response = self.client.post( | 480 | response = self.client.post( |
87 | 532 | self.get_node_uri(node), {'op': 'release'}) | 481 | self.get_node_uri(node), {'op': 'release'}) |
90 | 533 | self.assertEqual(httplib.OK, response.status_code) | 482 | self.assertEqual(httplib.OK, response.status_code, response.content) |
91 | 534 | self.assertEqual(NODE_STATUS.READY, reload_object(node).status) | 483 | self.assertEqual(NODE_STATUS.RELEASING, reload_object(node).status) |
92 | 535 | 484 | ||
93 | 536 | def test_POST_release_combines_with_acquire(self): | 485 | def test_POST_release_combines_with_acquire(self): |
94 | 537 | node = factory.make_Node(status=NODE_STATUS.READY) | 486 | node = factory.make_Node(status=NODE_STATUS.READY) |
95 | @@ -540,8 +489,8 @@ | |||
96 | 540 | self.assertEqual(NODE_STATUS.ALLOCATED, reload_object(node).status) | 489 | self.assertEqual(NODE_STATUS.ALLOCATED, reload_object(node).status) |
97 | 541 | node_uri = json.loads(response.content)['resource_uri'] | 490 | node_uri = json.loads(response.content)['resource_uri'] |
98 | 542 | response = self.client.post(node_uri, {'op': 'release'}) | 491 | response = self.client.post(node_uri, {'op': 'release'}) |
101 | 543 | self.assertEqual(httplib.OK, response.status_code) | 492 | self.assertEqual(httplib.OK, response.status_code, response.content) |
102 | 544 | self.assertEqual(NODE_STATUS.READY, reload_object(node).status) | 493 | self.assertEqual(NODE_STATUS.RELEASING, reload_object(node).status) |
103 | 545 | 494 | ||
104 | 546 | def test_POST_commission_commissions_node(self): | 495 | def test_POST_commission_commissions_node(self): |
105 | 547 | node = factory.make_Node( | 496 | node = factory.make_Node( |
106 | 548 | 497 | ||
107 | === modified file 'src/maasserver/enum.py' | |||
108 | --- src/maasserver/enum.py 2014-09-15 10:35:35 +0000 | |||
109 | +++ src/maasserver/enum.py 2014-09-16 01:19:01 +0000 | |||
110 | @@ -74,6 +74,8 @@ | |||
111 | 74 | ALLOCATED = 10 | 74 | ALLOCATED = 10 |
112 | 75 | #: The deployment of the node failed. | 75 | #: The deployment of the node failed. |
113 | 76 | FAILED_DEPLOYMENT = 11 | 76 | FAILED_DEPLOYMENT = 11 |
114 | 77 | #: The node is powering down after a release request. | ||
115 | 78 | RELEASING = 12 | ||
116 | 77 | 79 | ||
117 | 78 | 80 | ||
118 | 79 | # Django choices for NODE_STATUS: sequence of tuples (key, UI | 81 | # Django choices for NODE_STATUS: sequence of tuples (key, UI |
119 | @@ -91,6 +93,7 @@ | |||
120 | 91 | (NODE_STATUS.RETIRED, "Retired"), | 93 | (NODE_STATUS.RETIRED, "Retired"), |
121 | 92 | (NODE_STATUS.BROKEN, "Broken"), | 94 | (NODE_STATUS.BROKEN, "Broken"), |
122 | 93 | (NODE_STATUS.FAILED_DEPLOYMENT, "Failed deployment"), | 95 | (NODE_STATUS.FAILED_DEPLOYMENT, "Failed deployment"), |
123 | 96 | (NODE_STATUS.RELEASING, "Releasing"), | ||
124 | 94 | ) | 97 | ) |
125 | 95 | 98 | ||
126 | 96 | 99 | ||
127 | 97 | 100 | ||
128 | === modified file 'src/maasserver/models/node.py' | |||
129 | --- src/maasserver/models/node.py 2014-09-15 14:28:28 +0000 | |||
130 | +++ src/maasserver/models/node.py 2014-09-16 01:19:01 +0000 | |||
131 | @@ -161,6 +161,7 @@ | |||
132 | 161 | NODE_STATUS.RETIRED, | 161 | NODE_STATUS.RETIRED, |
133 | 162 | NODE_STATUS.MISSING, | 162 | NODE_STATUS.MISSING, |
134 | 163 | NODE_STATUS.BROKEN, | 163 | NODE_STATUS.BROKEN, |
135 | 164 | NODE_STATUS.RELEASING, | ||
136 | 164 | ], | 165 | ], |
137 | 165 | NODE_STATUS.ALLOCATED: [ | 166 | NODE_STATUS.ALLOCATED: [ |
138 | 166 | NODE_STATUS.READY, | 167 | NODE_STATUS.READY, |
139 | @@ -168,6 +169,12 @@ | |||
140 | 168 | NODE_STATUS.MISSING, | 169 | NODE_STATUS.MISSING, |
141 | 169 | NODE_STATUS.BROKEN, | 170 | NODE_STATUS.BROKEN, |
142 | 170 | NODE_STATUS.DEPLOYING, | 171 | NODE_STATUS.DEPLOYING, |
143 | 172 | NODE_STATUS.RELEASING, | ||
144 | 173 | ], | ||
145 | 174 | NODE_STATUS.RELEASING: [ | ||
146 | 175 | NODE_STATUS.READY, | ||
147 | 176 | NODE_STATUS.BROKEN, | ||
148 | 177 | NODE_STATUS.MISSING, | ||
149 | 171 | ], | 178 | ], |
150 | 172 | NODE_STATUS.DEPLOYING: [ | 179 | NODE_STATUS.DEPLOYING: [ |
151 | 173 | NODE_STATUS.ALLOCATED, | 180 | NODE_STATUS.ALLOCATED, |
152 | @@ -176,18 +183,21 @@ | |||
153 | 176 | NODE_STATUS.FAILED_DEPLOYMENT, | 183 | NODE_STATUS.FAILED_DEPLOYMENT, |
154 | 177 | NODE_STATUS.DEPLOYED, | 184 | NODE_STATUS.DEPLOYED, |
155 | 178 | NODE_STATUS.READY, | 185 | NODE_STATUS.READY, |
156 | 186 | NODE_STATUS.RELEASING, | ||
157 | 179 | ], | 187 | ], |
158 | 180 | NODE_STATUS.FAILED_DEPLOYMENT: [ | 188 | NODE_STATUS.FAILED_DEPLOYMENT: [ |
159 | 181 | NODE_STATUS.ALLOCATED, | 189 | NODE_STATUS.ALLOCATED, |
160 | 182 | NODE_STATUS.MISSING, | 190 | NODE_STATUS.MISSING, |
161 | 183 | NODE_STATUS.BROKEN, | 191 | NODE_STATUS.BROKEN, |
162 | 184 | NODE_STATUS.READY, | 192 | NODE_STATUS.READY, |
163 | 193 | NODE_STATUS.RELEASING, | ||
164 | 185 | ], | 194 | ], |
165 | 186 | NODE_STATUS.DEPLOYED: [ | 195 | NODE_STATUS.DEPLOYED: [ |
166 | 187 | NODE_STATUS.ALLOCATED, | 196 | NODE_STATUS.ALLOCATED, |
167 | 188 | NODE_STATUS.MISSING, | 197 | NODE_STATUS.MISSING, |
168 | 189 | NODE_STATUS.BROKEN, | 198 | NODE_STATUS.BROKEN, |
169 | 190 | NODE_STATUS.READY, | 199 | NODE_STATUS.READY, |
170 | 200 | NODE_STATUS.RELEASING, | ||
171 | 191 | ], | 201 | ], |
172 | 192 | NODE_STATUS.MISSING: [ | 202 | NODE_STATUS.MISSING: [ |
173 | 193 | NODE_STATUS.NEW, | 203 | NODE_STATUS.NEW, |
174 | @@ -205,6 +215,7 @@ | |||
175 | 205 | NODE_STATUS.BROKEN: [ | 215 | NODE_STATUS.BROKEN: [ |
176 | 206 | NODE_STATUS.COMMISSIONING, | 216 | NODE_STATUS.COMMISSIONING, |
177 | 207 | NODE_STATUS.READY, | 217 | NODE_STATUS.READY, |
178 | 218 | NODE_STATUS.RELEASING, | ||
179 | 208 | ], | 219 | ], |
180 | 209 | } | 220 | } |
181 | 210 | 221 | ||
182 | @@ -1238,8 +1249,13 @@ | |||
183 | 1238 | self.delete_host_maps(deallocated_ips) | 1249 | self.delete_host_maps(deallocated_ips) |
184 | 1239 | from maasserver.dns.config import change_dns_zones | 1250 | from maasserver.dns.config import change_dns_zones |
185 | 1240 | change_dns_zones([self.nodegroup]) | 1251 | change_dns_zones([self.nodegroup]) |
188 | 1241 | self.status = NODE_STATUS.READY | 1252 | if self.power_state == POWER_STATE.OFF: |
189 | 1242 | self.owner = None | 1253 | self.status = NODE_STATUS.READY |
190 | 1254 | self.owner = None | ||
191 | 1255 | else: | ||
192 | 1256 | # update_power_state() will take care of making the node READY | ||
193 | 1257 | # and unowned when the power is finally off. | ||
194 | 1258 | self.status = NODE_STATUS.RELEASING | ||
195 | 1243 | self.token = None | 1259 | self.token = None |
196 | 1244 | self.agent_name = '' | 1260 | self.agent_name = '' |
197 | 1245 | self.set_netboot() | 1261 | self.set_netboot() |
198 | @@ -1300,7 +1316,10 @@ | |||
199 | 1300 | """ | 1316 | """ |
200 | 1301 | if self.status in RELEASABLE_STATUSES: | 1317 | if self.status in RELEASABLE_STATUSES: |
201 | 1302 | self.release() | 1318 | self.release() |
202 | 1319 | # release() normally sets the status to RELEASING and leaves the | ||
203 | 1320 | # owner in place, override that here as we're broken. | ||
204 | 1303 | self.status = NODE_STATUS.BROKEN | 1321 | self.status = NODE_STATUS.BROKEN |
205 | 1322 | self.owner = None | ||
206 | 1304 | self.error_description = error_description | 1323 | self.error_description = error_description |
207 | 1305 | self.save() | 1324 | self.save() |
208 | 1306 | 1325 | ||
209 | @@ -1317,6 +1336,14 @@ | |||
210 | 1317 | def update_power_state(self, power_state): | 1336 | def update_power_state(self, power_state): |
211 | 1318 | """Update a node's power state """ | 1337 | """Update a node's power state """ |
212 | 1319 | self.power_state = power_state | 1338 | self.power_state = power_state |
213 | 1339 | mark_ready = ( | ||
214 | 1340 | self.status == NODE_STATUS.RELEASING and | ||
215 | 1341 | power_state == POWER_STATE.OFF) | ||
216 | 1342 | if mark_ready: | ||
217 | 1343 | # Ensure the node is fully released after a successful power | ||
218 | 1344 | # down. | ||
219 | 1345 | self.status = NODE_STATUS.READY | ||
220 | 1346 | self.owner = None | ||
221 | 1320 | self.save() | 1347 | self.save() |
222 | 1321 | 1348 | ||
223 | 1322 | def claim_static_ip_addresses(self): | 1349 | def claim_static_ip_addresses(self): |
224 | 1323 | 1350 | ||
225 | === modified file 'src/maasserver/models/tests/test_node.py' | |||
226 | --- src/maasserver/models/tests/test_node.py 2014-09-15 14:28:28 +0000 | |||
227 | +++ src/maasserver/models/tests/test_node.py 2014-09-16 01:19:01 +0000 | |||
228 | @@ -91,6 +91,7 @@ | |||
229 | 91 | AfterPreprocessing, | 91 | AfterPreprocessing, |
230 | 92 | Equals, | 92 | Equals, |
231 | 93 | HasLength, | 93 | HasLength, |
232 | 94 | Is, | ||
233 | 94 | MatchesStructure, | 95 | MatchesStructure, |
234 | 95 | ) | 96 | ) |
235 | 96 | from twisted.internet import defer | 97 | from twisted.internet import defer |
236 | @@ -642,15 +643,37 @@ | |||
237 | 642 | (user, NODE_STATUS.ALLOCATED, agent_name), | 643 | (user, NODE_STATUS.ALLOCATED, agent_name), |
238 | 643 | (node.owner, node.status, node.agent_name)) | 644 | (node.owner, node.status, node.agent_name)) |
239 | 644 | 645 | ||
249 | 645 | def test_release(self): | 646 | def test_release_node_that_has_power_on(self): |
250 | 646 | agent_name = factory.make_name('agent-name') | 647 | agent_name = factory.make_name('agent-name') |
251 | 647 | node = factory.make_Node( | 648 | owner = factory.make_User() |
252 | 648 | status=NODE_STATUS.ALLOCATED, owner=factory.make_User(), | 649 | node = factory.make_Node( |
253 | 649 | agent_name=agent_name) | 650 | status=NODE_STATUS.ALLOCATED, owner=owner, agent_name=agent_name) |
254 | 650 | node.release() | 651 | node.power_state = POWER_STATE.ON |
255 | 651 | self.assertEqual( | 652 | node.release() |
256 | 652 | (NODE_STATUS.READY, None, node.agent_name), | 653 | self.expectThat(node.status, Equals(NODE_STATUS.RELEASING)) |
257 | 653 | (node.status, node.owner, '')) | 654 | self.expectThat(node.owner, Equals(owner)) |
258 | 655 | self.expectThat(node.agent_name, Equals('')) | ||
259 | 656 | self.expectThat(node.token, Is(None)) | ||
260 | 657 | self.expectThat(node.netboot, Is(True)) | ||
261 | 658 | self.expectThat(node.osystem, Equals('')) | ||
262 | 659 | self.expectThat(node.distro_series, Equals('')) | ||
263 | 660 | self.expectThat(node.license_key, Equals('')) | ||
264 | 661 | |||
265 | 662 | def test_release_node_that_has_power_off(self): | ||
266 | 663 | agent_name = factory.make_name('agent-name') | ||
267 | 664 | owner = factory.make_User() | ||
268 | 665 | node = factory.make_Node( | ||
269 | 666 | status=NODE_STATUS.ALLOCATED, owner=owner, agent_name=agent_name) | ||
270 | 667 | node.power_state = POWER_STATE.OFF | ||
271 | 668 | node.release() | ||
272 | 669 | self.expectThat(node.status, Equals(NODE_STATUS.READY)) | ||
273 | 670 | self.expectThat(node.owner, Is(None)) | ||
274 | 671 | self.expectThat(node.agent_name, Equals('')) | ||
275 | 672 | self.expectThat(node.token, Is(None)) | ||
276 | 673 | self.expectThat(node.netboot, Is(True)) | ||
277 | 674 | self.expectThat(node.osystem, Equals('')) | ||
278 | 675 | self.expectThat(node.distro_series, Equals('')) | ||
279 | 676 | self.expectThat(node.license_key, Equals('')) | ||
280 | 654 | 677 | ||
281 | 655 | def test_release_deletes_static_ip_host_maps(self): | 678 | def test_release_deletes_static_ip_host_maps(self): |
282 | 656 | remove_host_maps = self.patch_autospec( | 679 | remove_host_maps = self.patch_autospec( |
283 | @@ -1208,10 +1231,11 @@ | |||
284 | 1208 | def test_mark_broken_releases_allocated_node(self): | 1231 | def test_mark_broken_releases_allocated_node(self): |
285 | 1209 | node = factory.make_Node( | 1232 | node = factory.make_Node( |
286 | 1210 | status=NODE_STATUS.ALLOCATED, owner=factory.make_User()) | 1233 | status=NODE_STATUS.ALLOCATED, owner=factory.make_User()) |
291 | 1211 | node.mark_broken(factory.make_name('error-description')) | 1234 | err_desc = factory.make_name('error-description') |
292 | 1212 | node = reload_object(node) | 1235 | release = self.patch(node, 'release') |
293 | 1213 | self.assertEqual( | 1236 | node.mark_broken(err_desc) |
294 | 1214 | (NODE_STATUS.BROKEN, None), (node.status, node.owner)) | 1237 | self.expectThat(node.owner, Is(None)) |
295 | 1238 | self.assertThat(release, MockCalledOnceWith()) | ||
296 | 1215 | 1239 | ||
297 | 1216 | def test_mark_fixed_changes_status(self): | 1240 | def test_mark_fixed_changes_status(self): |
298 | 1217 | node = factory.make_Node(status=NODE_STATUS.BROKEN) | 1241 | node = factory.make_Node(status=NODE_STATUS.BROKEN) |
299 | @@ -1237,6 +1261,26 @@ | |||
300 | 1237 | node.update_power_state(state) | 1261 | node.update_power_state(state) |
301 | 1238 | self.assertEqual(state, reload_object(node).power_state) | 1262 | self.assertEqual(state, reload_object(node).power_state) |
302 | 1239 | 1263 | ||
303 | 1264 | def test_update_power_state_readies_node_if_releasing(self): | ||
304 | 1265 | node = factory.make_Node( | ||
305 | 1266 | power_state=POWER_STATE.ON, status=NODE_STATUS.RELEASING, | ||
306 | 1267 | owner=None) | ||
307 | 1268 | node.update_power_state(POWER_STATE.OFF) | ||
308 | 1269 | self.expectThat(node.status, Equals(NODE_STATUS.READY)) | ||
309 | 1270 | self.expectThat(node.owner, Is(None)) | ||
310 | 1271 | |||
311 | 1272 | def test_update_power_state_does_not_change_status_if_not_releasing(self): | ||
312 | 1273 | node = factory.make_Node( | ||
313 | 1274 | power_state=POWER_STATE.ON, status=NODE_STATUS.ALLOCATED) | ||
314 | 1275 | node.update_power_state(POWER_STATE.OFF) | ||
315 | 1276 | self.assertThat(node.status, Equals(NODE_STATUS.ALLOCATED)) | ||
316 | 1277 | |||
317 | 1278 | def test_update_power_state_does_not_change_status_if_not_off(self): | ||
318 | 1279 | node = factory.make_Node( | ||
319 | 1280 | power_state=POWER_STATE.OFF, status=NODE_STATUS.ALLOCATED) | ||
320 | 1281 | node.update_power_state(POWER_STATE.ON) | ||
321 | 1282 | self.expectThat(node.status, Equals(NODE_STATUS.ALLOCATED)) | ||
322 | 1283 | |||
323 | 1240 | def test_end_deployment_changes_state(self): | 1284 | def test_end_deployment_changes_state(self): |
324 | 1241 | node = factory.make_Node(status=NODE_STATUS.DEPLOYING) | 1285 | node = factory.make_Node(status=NODE_STATUS.DEPLOYING) |
325 | 1242 | node.end_deployment() | 1286 | node.end_deployment() |
326 | 1243 | 1287 | ||
327 | === modified file 'src/maasserver/node_action.py' | |||
328 | --- src/maasserver/node_action.py 2014-09-12 03:16:19 +0000 | |||
329 | +++ src/maasserver/node_action.py 2014-09-16 01:19:01 +0000 | |||
330 | @@ -284,7 +284,6 @@ | |||
331 | 284 | try: | 284 | try: |
332 | 285 | Node.objects.start_nodes([self.node.system_id], self.user) | 285 | Node.objects.start_nodes([self.node.system_id], self.user) |
333 | 286 | except StaticIPAddressExhaustion: | 286 | except StaticIPAddressExhaustion: |
334 | 287 | self.node.release() | ||
335 | 288 | raise NodeActionError( | 287 | raise NodeActionError( |
336 | 289 | "%s: Failed to start, static IP addresses are exhausted." | 288 | "%s: Failed to start, static IP addresses are exhausted." |
337 | 290 | % self.node.hostname) | 289 | % self.node.hostname) |
338 | 291 | 290 | ||
339 | === modified file 'src/maasserver/tests/test_node_action.py' | |||
340 | --- src/maasserver/tests/test_node_action.py 2014-09-10 16:20:31 +0000 | |||
341 | +++ src/maasserver/tests/test_node_action.py 2014-09-16 01:19:01 +0000 | |||
342 | @@ -23,6 +23,7 @@ | |||
343 | 23 | NODE_STATUS, | 23 | NODE_STATUS, |
344 | 24 | NODE_STATUS_CHOICES, | 24 | NODE_STATUS_CHOICES, |
345 | 25 | NODE_STATUS_CHOICES_DICT, | 25 | NODE_STATUS_CHOICES_DICT, |
346 | 26 | POWER_STATE, | ||
347 | 26 | ) | 27 | ) |
348 | 27 | from maasserver.exceptions import ( | 28 | from maasserver.exceptions import ( |
349 | 28 | NodeActionError, | 29 | NodeActionError, |
350 | @@ -50,6 +51,7 @@ | |||
351 | 50 | from maasserver.testing.testcase import MAASServerTestCase | 51 | from maasserver.testing.testcase import MAASServerTestCase |
352 | 51 | from maastesting.matchers import MockCalledOnceWith | 52 | from maastesting.matchers import MockCalledOnceWith |
353 | 52 | from mock import ANY | 53 | from mock import ANY |
354 | 54 | from testtools.matchers import Equals | ||
355 | 53 | 55 | ||
356 | 54 | 56 | ||
357 | 55 | ALL_STATUSES = NODE_STATUS_CHOICES_DICT.keys() | 57 | ALL_STATUSES = NODE_STATUS_CHOICES_DICT.keys() |
358 | @@ -290,7 +292,8 @@ | |||
359 | 290 | def test_StartNode_returns_error_when_no_more_static_IPs(self): | 292 | def test_StartNode_returns_error_when_no_more_static_IPs(self): |
360 | 291 | user = factory.make_User() | 293 | user = factory.make_User() |
361 | 292 | node = factory.make_node_with_mac_attached_to_nodegroupinterface( | 294 | node = factory.make_node_with_mac_attached_to_nodegroupinterface( |
363 | 293 | status=NODE_STATUS.ALLOCATED, power_type='ether_wake', owner=user) | 295 | status=NODE_STATUS.ALLOCATED, power_type='ether_wake', owner=user, |
364 | 296 | power_state=POWER_STATE.OFF) | ||
365 | 294 | ngi = node.get_primary_mac().cluster_interface | 297 | ngi = node.get_primary_mac().cluster_interface |
366 | 295 | 298 | ||
367 | 296 | # Narrow the available IP range and pre-claim the only address. | 299 | # Narrow the available IP range and pre-claim the only address. |
368 | @@ -300,10 +303,11 @@ | |||
369 | 300 | ngi.static_ip_range_high, ngi.static_ip_range_low) | 303 | ngi.static_ip_range_high, ngi.static_ip_range_low) |
370 | 301 | 304 | ||
371 | 302 | e = self.assertRaises(NodeActionError, StartNode(node, user).execute) | 305 | e = self.assertRaises(NodeActionError, StartNode(node, user).execute) |
376 | 303 | self.assertEqual( | 306 | self.expectThat( |
377 | 304 | "%s: Failed to start, static IP addresses are exhausted." % | 307 | e.message, Equals( |
378 | 305 | node.hostname, e.message) | 308 | "%s: Failed to start, static IP addresses are exhausted." % |
379 | 306 | self.assertEqual(NODE_STATUS.READY, node.status) | 309 | node.hostname)) |
380 | 310 | self.assertEqual(NODE_STATUS.ALLOCATED, node.status) | ||
381 | 307 | 311 | ||
382 | 308 | def test_StartNode_requires_edit_permission(self): | 312 | def test_StartNode_requires_edit_permission(self): |
383 | 309 | user = factory.make_User() | 313 | user = factory.make_User() |
384 | @@ -351,8 +355,7 @@ | |||
385 | 351 | 355 | ||
386 | 352 | ReleaseNode(node, user).execute() | 356 | ReleaseNode(node, user).execute() |
387 | 353 | 357 | ||
390 | 354 | self.assertEqual(NODE_STATUS.READY, node.status) | 358 | self.expectThat(node.status, Equals(NODE_STATUS.RELEASING)) |
389 | 355 | self.assertIsNone(node.owner) | ||
391 | 356 | self.assertThat( | 359 | self.assertThat( |
392 | 357 | stop_nodes, MockCalledOnceWith([node.system_id], user)) | 360 | stop_nodes, MockCalledOnceWith([node.system_id], user)) |
393 | 358 | 361 |
I tested this locally and it seems to be ok, you can't start the node again until the poweroff script finishes. In the meantime, the UI shows "Releasing". It'd be great if we had all this on longpoll :)