Merge lp:~ltrager/maas/rack_transitioning into lp:~maas-committers/maas/trunk
- rack_transitioning
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Lee Trager |
Approved revision: | no longer in the source branch. |
Merged at revision: | 5052 |
Proposed branch: | lp:~ltrager/maas/rack_transitioning |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
368 lines (+253/-3) 6 files modified
src/maasserver/models/node.py (+22/-0) src/maasserver/models/tests/test_node.py (+34/-0) src/maasserver/rpc/rackcontrollers.py (+1/-2) src/maasserver/rpc/tests/test_rackcontrollers.py (+7/-1) src/maasserver/triggers/tests/test_websocket_listener.py (+125/-0) src/maasserver/triggers/websocket.py (+64/-0) |
To merge this branch: | bzr merge lp:~ltrager/maas/rack_transitioning |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella (community) | Approve | ||
Review via email: mp+295552@code.launchpad.net |
Commit message
Allow a machine to transition to and from a rack controller
Description of the change
This allows you to deploy a machine, install a rack controller on it, then delete the rack controller converting it back to a usable but still deployed machine. I added a trigger so the transitions of any node type to any node type are shown in the UI instantly. During testing I realized that we were always setting the owner to the rack controller worker user. This only sets the user if one isn't defined.
Lee Trager (ltrager) wrote : | # |
Thanks for the review. I've made the corrections you've suggested and answered your question about testing below. I put _probably_
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~ltrager/maas/rack_transitioning into lp:maas failed. Below is the output from the failed tests.
Hit:1 http://
Get:2 http://
Get:3 http://
Hit:4 http://
Get:5 http://
Get:6 http://
Fetched 350 kB in 0s (754 kB/s)
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-2ubuntu3).
archdetect-deb is already the newest version (1.117ubuntu2).
authbind is already the newest version (2.1.1+nmu1).
bash is already the newest version (4.3-14ubuntu1).
build-essential is already the newest version (12.1ubuntu2).
bzr is already the newest version (2.7.0-2ubuntu1).
curl is already the newest version (7.47.0-1ubuntu2).
debhelper is already the newest version (9.20160115ubun
distro-info is already the newest version (0.14build1).
freeipmi-tools is already the newest version (1.4.11-1ubuntu1).
git is already the newest version (1:2.7.4-0ubuntu1).
isc-dhcp-common is already the newest version (4.3.3-5ubuntu12).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest ...
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~ltrager/maas/rack_transitioning into lp:maas failed. Below is the output from the failed tests.
Hit:1 http://
Hit:2 http://
Hit:3 http://
Get:4 http://
Fetched 94.5 kB in 0s (204 kB/s)
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-2ubuntu3).
archdetect-deb is already the newest version (1.117ubuntu2).
authbind is already the newest version (2.1.1+nmu1).
bash is already the newest version (4.3-14ubuntu1).
build-essential is already the newest version (12.1ubuntu2).
bzr is already the newest version (2.7.0-2ubuntu1).
curl is already the newest version (7.47.0-1ubuntu2).
debhelper is already the newest version (9.20160115ubun
distro-info is already the newest version (0.14build1).
freeipmi-tools is already the newest version (1.4.11-1ubuntu1).
git is already the newest version (1:2.7.4-0ubuntu1).
isc-dhcp-common is already the newest version (4.3.3-5ubuntu12).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
make is already the newest version (4.1-6).
postgresql is already the newest version (9.5+173).
pxelinux is already the newest version (3:6...
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~ltrager/maas/rack_transitioning into lp:maas failed. Below is the output from the failed tests.
Get:1 http://
Hit:2 http://
Get:3 http://
Hit:4 http://
Fetched 189 kB in 0s (482 kB/s)
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-2ubuntu3).
archdetect-deb is already the newest version (1.117ubuntu2).
authbind is already the newest version (2.1.1+nmu1).
bash is already the newest version (4.3-14ubuntu1).
build-essential is already the newest version (12.1ubuntu2).
bzr is already the newest version (2.7.0-2ubuntu1).
curl is already the newest version (7.47.0-1ubuntu2).
debhelper is already the newest version (9.20160115ubun
distro-info is already the newest version (0.14build1).
freeipmi-tools is already the newest version (1.4.11-1ubuntu1).
git is already the newest version (1:2.7.4-0ubuntu1).
isc-dhcp-common is already the newest version (4.3.3-5ubuntu12).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
make is already the newest version (4.1-6).
postgresql is already the newest version (9.5+173).
pxelinux is already the newest version (3:6.03+dfsg...
Preview Diff
1 | === modified file 'src/maasserver/models/node.py' |
2 | --- src/maasserver/models/node.py 2016-05-26 06:47:45 +0000 |
3 | +++ src/maasserver/models/node.py 2016-05-26 16:01:00 +0000 |
4 | @@ -3097,6 +3097,16 @@ |
5 | def __init__(self, *args, **kwargs): |
6 | super(Controller, self).__init__(*args, **kwargs) |
7 | |
8 | + def _was_probably_machine(self): |
9 | + """Best guess if a rack was a machine. |
10 | + |
11 | + MAAS doesn't track node transitions so we have to look at |
12 | + breadcrumbs. The first is the status. Only machines can have their |
13 | + status changed to something other than NEW and a rack controller should |
14 | + only be installable when the machine is in a deployed state. Second a |
15 | + machine must have power information.""" |
16 | + return self.status == NODE_STATUS.DEPLOYED and self.bmc is not None |
17 | + |
18 | |
19 | class RackController(Controller): |
20 | """A node which is running rackd.""" |
21 | @@ -3659,6 +3669,9 @@ |
22 | |
23 | def delete(self): |
24 | """Delete this rack controller.""" |
25 | + # Avoid circular imports |
26 | + from maasserver.models import RegionRackRPCConnection |
27 | + |
28 | primary_vlans = VLAN.objects.filter(primary_rack=self) |
29 | if len(primary_vlans) != 0: |
30 | raise ValidationError( |
31 | @@ -3667,6 +3680,10 @@ |
32 | (self.hostname, |
33 | ', '.join([str(vlan) for vlan in primary_vlans]))) |
34 | |
35 | + # Disable and delete all services related to this node |
36 | + Service.objects.mark_dead(self, dead_rack=True) |
37 | + Service.objects.filter(node=self).delete() |
38 | + |
39 | try: |
40 | client = getClientFor(self.system_id, timeout=1) |
41 | call = client(DisableAndShutoffRackd) |
42 | @@ -3676,6 +3693,8 @@ |
43 | # is currently disconnected or rackd was killed |
44 | pass |
45 | |
46 | + RegionRackRPCConnection.objects.filter(rack_controller=self).delete() |
47 | + |
48 | for vlan in VLAN.objects.filter(secondary_rack=self): |
49 | vlan.secondary_rack = None |
50 | vlan.save() |
51 | @@ -3683,6 +3702,9 @@ |
52 | if self.node_type == NODE_TYPE.REGION_AND_RACK_CONTROLLER: |
53 | self.node_type = NODE_TYPE.REGION_CONTROLLER |
54 | self.save() |
55 | + elif self._was_probably_machine(): |
56 | + self.node_type = NODE_TYPE.MACHINE |
57 | + self.save() |
58 | else: |
59 | super().delete() |
60 | |
61 | |
62 | === modified file 'src/maasserver/models/tests/test_node.py' |
63 | --- src/maasserver/models/tests/test_node.py 2016-05-26 06:47:45 +0000 |
64 | +++ src/maasserver/models/tests/test_node.py 2016-05-26 16:01:00 +0000 |
65 | @@ -4597,6 +4597,18 @@ |
66 | routable_racks, none_routable_racks) |
67 | |
68 | |
69 | +class TestController(MAASServerTestCase): |
70 | + |
71 | + def test__was_probably_machine_true(self): |
72 | + rack = factory.make_RackController(status=NODE_STATUS.DEPLOYED) |
73 | + rack.bmc = factory.make_BMC() |
74 | + rack.save() |
75 | + self.assertTrue(rack._was_probably_machine()) |
76 | + |
77 | + def test__was_probably_machine_false(self): |
78 | + self.assertFalse(factory.make_RackController()._was_probably_machine()) |
79 | + |
80 | + |
81 | class TestRackControllerUpdateInterfaces(MAASServerTestCase): |
82 | |
83 | def create_empty_rack_controller(self): |
84 | @@ -7022,6 +7034,18 @@ |
85 | RackController.DoesNotExist, |
86 | RackController.objects.get, system_id=rackcontroller.system_id) |
87 | |
88 | + def test_deletes_services(self): |
89 | + rack = factory.make_RackController() |
90 | + factory.make_Service(rack) |
91 | + rack.delete() |
92 | + self.assertItemsEqual([], Service.objects.all()) |
93 | + |
94 | + def test_deletes_region_rack_rpc_connections(self): |
95 | + rack = factory.make_RackController() |
96 | + factory.make_RegionRackRPCConnection(rack_controller=rack) |
97 | + rack.delete() |
98 | + self.assertItemsEqual([], RegionRackRPCConnection.objects.all()) |
99 | + |
100 | def test_delete_converts_region_and_rack_to_region(self): |
101 | region_and_rack = factory.make_Node( |
102 | node_type=NODE_TYPE.REGION_AND_RACK_CONTROLLER) |
103 | @@ -7031,6 +7055,16 @@ |
104 | NODE_TYPE.REGION_CONTROLLER, |
105 | Node.objects.get(system_id=system_id).node_type) |
106 | |
107 | + def test_delete_converts_rack_to_machine(self): |
108 | + rack = factory.make_RackController(status=NODE_STATUS.DEPLOYED) |
109 | + rack.power_parameters = { |
110 | + 'power_address': 'qemu+ssh://user@host/system', |
111 | + } |
112 | + rack.delete() |
113 | + self.assertEquals( |
114 | + NODE_TYPE.MACHINE, |
115 | + Node.objects.get(system_id=rack.system_id).node_type) |
116 | + |
117 | def test_update_rackd_status_calls_mark_dead_when_no_connections(self): |
118 | rack_controller = factory.make_RackController() |
119 | mock_mark_dead = self.patch(Service.objects, "mark_dead") |
120 | |
121 | === modified file 'src/maasserver/rpc/rackcontrollers.py' |
122 | --- src/maasserver/rpc/rackcontrollers.py 2016-05-13 15:14:05 +0000 |
123 | +++ src/maasserver/rpc/rackcontrollers.py 2016-05-26 16:01:00 +0000 |
124 | @@ -83,8 +83,7 @@ |
125 | if rackcontroller.url != url.geturl(): |
126 | rackcontroller.url = url.geturl() |
127 | update_fields.append("url") |
128 | - work_user = worker_user.get_worker_user() |
129 | - if rackcontroller.owner != work_user: |
130 | + if rackcontroller.owner is None: |
131 | rackcontroller.owner = worker_user.get_worker_user() |
132 | update_fields.append("owner") |
133 | rackcontroller.save(update_fields=update_fields) |
134 | |
135 | === modified file 'src/maasserver/rpc/tests/test_rackcontrollers.py' |
136 | --- src/maasserver/rpc/tests/test_rackcontrollers.py 2016-05-13 15:14:05 +0000 |
137 | +++ src/maasserver/rpc/tests/test_rackcontrollers.py 2016-05-26 16:01:00 +0000 |
138 | @@ -97,11 +97,17 @@ |
139 | |
140 | class TestRegisterRackController(MAASServerTestCase): |
141 | |
142 | - def test_sets_owner_to_worker(self): |
143 | + def test_sets_owner_to_worker_when_none(self): |
144 | node = factory.make_Node() |
145 | rack_registered = register_rackcontroller(system_id=node.system_id) |
146 | self.assertEqual(worker_user.get_worker_user(), rack_registered.owner) |
147 | |
148 | + def test_leaves_owner_when_owned(self): |
149 | + user = factory.make_User() |
150 | + node = factory.make_Machine(owner=user) |
151 | + rack_registered = register_rackcontroller(system_id=node.system_id) |
152 | + self.assertEqual(user, rack_registered.owner) |
153 | + |
154 | def test_finds_existing_node_by_system_id(self): |
155 | node = factory.make_Node() |
156 | rack_registered = register_rackcontroller(system_id=node.system_id) |
157 | |
158 | === modified file 'src/maasserver/triggers/tests/test_websocket_listener.py' |
159 | --- src/maasserver/triggers/tests/test_websocket_listener.py 2016-04-28 13:53:02 +0000 |
160 | +++ src/maasserver/triggers/tests/test_websocket_listener.py 2016-05-26 16:01:00 +0000 |
161 | @@ -2645,3 +2645,128 @@ |
162 | self.assertEqual(('delete', '%s' % snippet.id), dv.value) |
163 | finally: |
164 | yield listener.stopService() |
165 | + |
166 | + |
167 | +class TestNodeTypeChange( |
168 | + MAASTransactionServerTestCase, TransactionalHelpersMixin): |
169 | + """End-to-end test of node type change triggers code.""" |
170 | + |
171 | + scenarios = ( |
172 | + ('machine_to_rack', { |
173 | + 'from_type': NODE_TYPE.MACHINE, |
174 | + 'from_listener': 'machine', |
175 | + 'to_type': NODE_TYPE.RACK_CONTROLLER, |
176 | + 'to_listener': 'controller', |
177 | + }), |
178 | + ('machine_to_region', { |
179 | + 'from_type': NODE_TYPE.MACHINE, |
180 | + 'from_listener': 'machine', |
181 | + 'to_type': NODE_TYPE.REGION_CONTROLLER, |
182 | + 'to_listener': 'controller', |
183 | + }), |
184 | + ('machine_to_rack_and_region', { |
185 | + 'from_type': NODE_TYPE.MACHINE, |
186 | + 'from_listener': 'machine', |
187 | + 'to_type': NODE_TYPE.REGION_AND_RACK_CONTROLLER, |
188 | + 'to_listener': 'controller', |
189 | + }), |
190 | + ('machine_to_device', { |
191 | + 'from_type': NODE_TYPE.MACHINE, |
192 | + 'from_listener': 'machine', |
193 | + 'to_type': NODE_TYPE.DEVICE, |
194 | + 'to_listener': 'device', |
195 | + }), |
196 | + ('rack_to_machine', { |
197 | + 'from_type': NODE_TYPE.RACK_CONTROLLER, |
198 | + 'from_listener': 'controller', |
199 | + 'to_type': NODE_TYPE.MACHINE, |
200 | + 'to_listener': 'machine', |
201 | + }), |
202 | + ('rack_to_device', { |
203 | + 'from_type': NODE_TYPE.RACK_CONTROLLER, |
204 | + 'from_listener': 'controller', |
205 | + 'to_type': NODE_TYPE.DEVICE, |
206 | + 'to_listener': 'device', |
207 | + }), |
208 | + ('region_to_machine', { |
209 | + 'from_type': NODE_TYPE.REGION_CONTROLLER, |
210 | + 'from_listener': 'controller', |
211 | + 'to_type': NODE_TYPE.MACHINE, |
212 | + 'to_listener': 'machine', |
213 | + }), |
214 | + ('region_to_device', { |
215 | + 'from_type': NODE_TYPE.REGION_CONTROLLER, |
216 | + 'from_listener': 'controller', |
217 | + 'to_type': NODE_TYPE.DEVICE, |
218 | + 'to_listener': 'device', |
219 | + }), |
220 | + ('region_and_rack_to_machine', { |
221 | + 'from_type': NODE_TYPE.REGION_AND_RACK_CONTROLLER, |
222 | + 'from_listener': 'controller', |
223 | + 'to_type': NODE_TYPE.MACHINE, |
224 | + 'to_listener': 'machine', |
225 | + }), |
226 | + ('region_and_rack_to_device', { |
227 | + 'from_type': NODE_TYPE.REGION_AND_RACK_CONTROLLER, |
228 | + 'from_listener': 'controller', |
229 | + 'to_type': NODE_TYPE.DEVICE, |
230 | + 'to_listener': 'device', |
231 | + }), |
232 | + ('device_to_rack', { |
233 | + 'from_type': NODE_TYPE.DEVICE, |
234 | + 'from_listener': 'device', |
235 | + 'to_type': NODE_TYPE.RACK_CONTROLLER, |
236 | + 'to_listener': 'controller', |
237 | + }), |
238 | + ('device_to_region', { |
239 | + 'from_type': NODE_TYPE.DEVICE, |
240 | + 'from_listener': 'device', |
241 | + 'to_type': NODE_TYPE.REGION_CONTROLLER, |
242 | + 'to_listener': 'controller', |
243 | + }), |
244 | + ('device_to_rack_and_region', { |
245 | + 'from_type': NODE_TYPE.DEVICE, |
246 | + 'from_listener': 'device', |
247 | + 'to_type': NODE_TYPE.REGION_AND_RACK_CONTROLLER, |
248 | + 'to_listener': 'controller', |
249 | + }), |
250 | + ('device_to_machine', { |
251 | + 'from_type': NODE_TYPE.DEVICE, |
252 | + 'from_listener': 'device', |
253 | + 'to_type': NODE_TYPE.MACHINE, |
254 | + 'to_listener': 'machine', |
255 | + }), |
256 | + ) |
257 | + |
258 | + @wait_for_reactor |
259 | + @inlineCallbacks |
260 | + def test__transition_notifies(self): |
261 | + yield deferToDatabase(register_websocket_triggers) |
262 | + listener1 = self.make_listener_without_delay() |
263 | + listener2 = self.make_listener_without_delay() |
264 | + node = yield deferToDatabase( |
265 | + self.create_node, {'node_type': self.from_type}) |
266 | + q_from, q_to = DeferredQueue(), DeferredQueue() |
267 | + listener1.register(self.from_listener, lambda *args: q_from.put(args)) |
268 | + listener2.register(self.to_listener, lambda *args: q_to.put(args)) |
269 | + yield listener1.startService() |
270 | + yield listener2.startService() |
271 | + try: |
272 | + node.node_type = self.to_type |
273 | + yield deferToDatabase(node.save) |
274 | + self.assertEqual( |
275 | + ('delete', node.system_id), |
276 | + (yield deferWithTimeout(2, q_from.get))) |
277 | + self.assertEqual( |
278 | + { |
279 | + ('create', node.system_id), |
280 | + ('update', node.system_id), |
281 | + }, |
282 | + { |
283 | + (yield deferWithTimeout(2, q_to.get)), |
284 | + (yield deferWithTimeout(2, q_to.get)), |
285 | + } |
286 | + ) |
287 | + finally: |
288 | + yield listener1.stopService() |
289 | + yield listener2.stopService() |
290 | |
291 | === modified file 'src/maasserver/triggers/websocket.py' |
292 | --- src/maasserver/triggers/websocket.py 2016-04-01 22:54:51 +0000 |
293 | +++ src/maasserver/triggers/websocket.py 2016-05-26 16:01:00 +0000 |
294 | @@ -772,6 +772,66 @@ |
295 | NODE_TYPE.REGION_AND_RACK_CONTROLLER)) |
296 | |
297 | |
298 | +def node_type_change(): |
299 | + return dedent("""\ |
300 | + CREATE OR REPLACE FUNCTION node_type_change_notify() |
301 | + RETURNS trigger AS $$ |
302 | + BEGIN |
303 | + IF (OLD.node_type != NEW.node_type AND NOT ( |
304 | + ( |
305 | + OLD.node_type = {rack_controller} OR |
306 | + OLD.node_type = {region_controller} OR |
307 | + OLD.node_type = {region_and_rack_controller} |
308 | + ) AND ( |
309 | + NEW.node_type = {rack_controller} OR |
310 | + NEW.node_type = {region_controller} OR |
311 | + NEW.node_type = {region_and_rack_controller} |
312 | + ))) THEN |
313 | + CASE OLD.node_type |
314 | + WHEN {machine} THEN |
315 | + PERFORM pg_notify('machine_delete',CAST( |
316 | + OLD.system_id AS TEXT)); |
317 | + WHEN {device} THEN |
318 | + PERFORM pg_notify('device_delete',CAST( |
319 | + OLD.system_id AS TEXT)); |
320 | + WHEN {rack_controller} THEN |
321 | + PERFORM pg_notify('controller_delete',CAST( |
322 | + OLD.system_id AS TEXT)); |
323 | + WHEN {region_controller} THEN |
324 | + PERFORM pg_notify('controller_delete',CAST( |
325 | + OLD.system_id AS TEXT)); |
326 | + WHEN {region_and_rack_controller} THEN |
327 | + PERFORM pg_notify('controller_delete',CAST( |
328 | + OLD.system_id AS TEXT)); |
329 | + END CASE; |
330 | + CASE NEW.node_type |
331 | + WHEN {machine} THEN |
332 | + PERFORM pg_notify('machine_create',CAST( |
333 | + NEW.system_id AS TEXT)); |
334 | + WHEN {device} THEN |
335 | + PERFORM pg_notify('device_create',CAST( |
336 | + NEW.system_id AS TEXT)); |
337 | + WHEN {rack_controller} THEN |
338 | + PERFORM pg_notify('controller_create',CAST( |
339 | + NEW.system_id AS TEXT)); |
340 | + WHEN {region_controller} THEN |
341 | + PERFORM pg_notify('controller_create',CAST( |
342 | + NEW.system_id AS TEXT)); |
343 | + WHEN {region_and_rack_controller} THEN |
344 | + PERFORM pg_notify('controller_create',CAST( |
345 | + NEW.system_id AS TEXT)); |
346 | + END CASE; |
347 | + END IF; |
348 | + RETURN NEW; |
349 | + END; |
350 | + $$ LANGUAGE plpgsql; |
351 | + """.format( |
352 | + machine=NODE_TYPE.MACHINE, device=NODE_TYPE.DEVICE, |
353 | + rack_controller=NODE_TYPE.RACK_CONTROLLER, |
354 | + region_controller=NODE_TYPE.REGION_CONTROLLER, |
355 | + region_and_rack_controller=NODE_TYPE.REGION_AND_RACK_CONTROLLER)) |
356 | + |
357 | + |
358 | @transactional |
359 | def register_websocket_triggers(): |
360 | """Register all websocket triggers into the database.""" |
361 | @@ -1414,3 +1474,7 @@ |
362 | "maasserver_dhcpsnippet", "dhcpsnippet_update_notify", "update") |
363 | register_trigger( |
364 | "maasserver_dhcpsnippet", "dhcpsnippet_delete_notify", "delete") |
365 | + |
366 | + register_procedure(node_type_change()) |
367 | + register_trigger( |
368 | + "maasserver_node", "node_type_change_notify", "update") |
This is good stuff. I really like the scenarios in TestNodeTypeChange. I have one big question though, and I think TestNodeTypeChange is actually broken right now (though it's not far off), so Needs Fixing.