Merge lp:~blake-rouse/maas/commissioning-get-disk-info into lp:~maas-committers/maas/trunk
- commissioning-get-disk-info
- Merge into trunk
Status: | Merged | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Approved by: | Blake Rouse | ||||||||||||
Approved revision: | no longer in the source branch. | ||||||||||||
Merged at revision: | 3443 | ||||||||||||
Proposed branch: | lp:~blake-rouse/maas/commissioning-get-disk-info | ||||||||||||
Merge into: | lp:~maas-committers/maas/trunk | ||||||||||||
Prerequisite: | lp:~blake-rouse/maas/physical-block-device-model | ||||||||||||
Diff against target: |
532 lines (+467/-0) 2 files modified
src/metadataserver/models/commissioningscript.py (+121/-0) src/metadataserver/models/tests/test_noderesults.py (+346/-0) |
||||||||||||
To merge this branch: | bzr merge lp:~blake-rouse/maas/commissioning-get-disk-info | ||||||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Raphaël Badin (community) | Approve | ||
Review via email: mp+245585@code.launchpad.net |
Commit message
Populates the PhysicalBlockDevice model for a node when commissioning. Uses a combination of lsblk, udevadm, and blockdev to gather the information from the node during commissioning.
Description of the change
Blake Rouse (blake-rouse) wrote : | # |
I think at the moment we should leave it how it is. I understand your concern, but the issue is that all the calls that are made are required and not optional. Currently if this script fails then all of commissioning fails, which would allow the user to view the output where the exception would be outputted. The only way I can think around this is to remove disks as commands for that disk fail, which might be even more confusing for the user.
Raphaël Badin (rvb) wrote : | # |
> Currently if this script fails then all of commissioning fails, which would allow the user to
> view the output where the exception would be outputted. The only way I can think around this is
> to remove disks as commands for that disk fail, which might be even more confusing for the user.
I'm a bit concerned about releasing code that might prevent the nodes from being commissioned at all if the parsing fails (given that the commands we run and the parsing we do are non-trivial). Now, I'm fine with releasing this as is as long as we preform extensive QA on this and test it on all the machines that we have: the lab, the garage MAAS, Jason's lab, OIL, etc. Can you please do this?
fwiw, I really think this ugly code could be best done with a regular expression but I'm not going to block this branch for it :)
+ info_line = info_line.strip()
+ if info_line == "":
+ continue
+ _, info = info_line.split(" ", 1)
+ if "=" not in info:
+ continue
+ k, v = info.split("=", 1)
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~blake-rouse/maas/commissioning-get-disk-info into lp:maas failed. Below is the output from the failed tests.
Ign http://
Get:1 http://
Ign http://
Get:2 http://
Ign http://
Hit http://
Get:3 http://
Hit http://
Get:4 http://
Get:5 http://
Hit http://
Get:6 http://
Get:7 http://
Hit http://
Hit http://
Hit http://
Get:8 http://
Hit http://
Hit http://
Get:9 http://
Hit http://
Hit http://
Get:10 http://
Get:11 http://
Get:12 http://
Hit http://
Hit http://
Ign http://
Ign http://
Fetched 1,348 kB in 3s (441 kB/s)
Reading package lists...
sudo DEBIAN_
--
Preview Diff
1 | === modified file 'src/metadataserver/models/commissioningscript.py' |
2 | --- src/metadataserver/models/commissioningscript.py 2014-12-05 21:07:48 +0000 |
3 | +++ src/metadataserver/models/commissioningscript.py 2015-01-09 14:32:14 +0000 |
4 | @@ -43,6 +43,7 @@ |
5 | ) |
6 | from lxml import etree |
7 | from maasserver.fields import MAC |
8 | +from maasserver.models.physicalblockdevice import PhysicalBlockDevice |
9 | from maasserver.models.tag import Tag |
10 | from metadataserver import DefaultMeta |
11 | from metadataserver.enum import RESULT_TYPE |
12 | @@ -354,6 +355,122 @@ |
13 | 'find /sys -name modalias -print0 | xargs -0 cat | sort -u' |
14 | |
15 | |
16 | +def gather_physical_block_devices(print_output=True): |
17 | + """Gathers information about a nodes physical block devices. |
18 | + |
19 | + The following commands are ran in order to gather the required information. |
20 | + |
21 | + lsblk Gathers the initial block devices not including slaves or |
22 | + holders. Gets the name, read-only, removable, model, and |
23 | + if rotary. |
24 | + |
25 | + udevadm Grabs the device path, serial number, and if connected over |
26 | + SATA. |
27 | + |
28 | + blockdev Grabs the block size and size of the disk in bytes. |
29 | + |
30 | + :param print_output: False will return the output instead of |
31 | + printing. (Used only for testing.) |
32 | + """ |
33 | + import shlex |
34 | + from subprocess import check_output |
35 | + |
36 | + # Grab the block devices from lsblk. |
37 | + blockdevs = [] |
38 | + block_list = check_output( |
39 | + ("lsblk", "-d", "-P", "-o", "NAME,RO,RM,MODEL,ROTA")) |
40 | + for blockdev in block_list.splitlines(): |
41 | + tokens = shlex.split(blockdev) |
42 | + current_block = {} |
43 | + for token in tokens: |
44 | + k, v = token.split("=", 1) |
45 | + current_block[k] = v.strip() |
46 | + blockdevs.append(current_block) |
47 | + |
48 | + # Grab the device path, serial number, and sata connection. |
49 | + UDEV_MAPPINGS = { |
50 | + "DEVNAME": "PATH", |
51 | + "ID_SERIAL_SHORT": "SERIAL", |
52 | + "ID_ATA_SATA": "SATA", |
53 | + } |
54 | + del_blocks = [] |
55 | + for block_info in blockdevs: |
56 | + # Some RAID devices return the name of the device seperated with "!", |
57 | + # but udevadm expects it to be a "/". |
58 | + block_name = block_info["NAME"].replace("!", "/") |
59 | + udev_info = check_output( |
60 | + ("udevadm", "info", "-q", "all", "-n", block_name)) |
61 | + for info_line in udev_info.splitlines(): |
62 | + info_line = info_line.strip() |
63 | + if info_line == "": |
64 | + continue |
65 | + _, info = info_line.split(" ", 1) |
66 | + if "=" not in info: |
67 | + continue |
68 | + k, v = info.split("=", 1) |
69 | + if k in UDEV_MAPPINGS: |
70 | + block_info[UDEV_MAPPINGS[k]] = v.strip() |
71 | + if k == "ID_CDROM" and v == "1": |
72 | + # Remove any type of CDROM from the blockdevs, as we |
73 | + # cannot use this device for installation. |
74 | + del_blocks.append(block_name) |
75 | + |
76 | + # Remove any devices that need to be removed. |
77 | + blockdevs = [ |
78 | + block_info |
79 | + for block_info in blockdevs |
80 | + if block_info["NAME"] not in del_blocks |
81 | + ] |
82 | + |
83 | + # Grab the size of the device and block size. |
84 | + for block_info in blockdevs: |
85 | + block_path = block_info["PATH"] |
86 | + device_size = check_output( |
87 | + ("blockdev", "--getsize64", block_path)) |
88 | + device_block_size = check_output( |
89 | + ("blockdev", "--getbsz", block_path)) |
90 | + block_info["SIZE"] = device_size.strip() |
91 | + block_info["BLOCK_SIZE"] = device_block_size.strip() |
92 | + |
93 | + # Output block device information in json |
94 | + json_output = json.dumps(blockdevs, indent=True) |
95 | + if print_output: |
96 | + print(json_output) |
97 | + else: |
98 | + return json_output |
99 | + |
100 | + |
101 | +def update_node_physical_block_devices(node, output, exit_status): |
102 | + """Process the results of `gather_physical_block_devices`. |
103 | + |
104 | + This updates the physical block devices that are attached to a node. |
105 | + |
106 | + If `exit_status` is non-zero, this function returns without doing |
107 | + anything. |
108 | + """ |
109 | + assert isinstance(output, bytes) |
110 | + if exit_status != 0: |
111 | + return |
112 | + blockdevs = json.loads(output) |
113 | + PhysicalBlockDevice.objects.filter(node=node).delete() |
114 | + for block_info in blockdevs: |
115 | + # Skip the read-only devices. We keep them in the output for |
116 | + # the user to view but they do not get an entry in the database. |
117 | + if block_info["RO"] == "1": |
118 | + continue |
119 | + model = block_info.get("MODEL", "") |
120 | + serial = block_info.get("SERIAL", "") |
121 | + PhysicalBlockDevice.objects.create( |
122 | + node=node, |
123 | + name=block_info["NAME"], |
124 | + path=block_info["PATH"], |
125 | + size=long(block_info["SIZE"]), |
126 | + block_size=int(block_info["BLOCK_SIZE"]), |
127 | + model=model, |
128 | + serial=serial, |
129 | + ) |
130 | + |
131 | + |
132 | def null_hook(node, output, exit_status): |
133 | """Intentionally do nothing. |
134 | |
135 | @@ -401,6 +518,10 @@ |
136 | 'content': make_function_call_script(dhcp_explore), |
137 | 'hook': null_hook, |
138 | }, |
139 | + '00-maas-06-block-devices.out': { |
140 | + 'content': make_function_call_script(gather_physical_block_devices), |
141 | + 'hook': update_node_physical_block_devices, |
142 | + }, |
143 | '99-maas-01-wait-for-lldpd.out': { |
144 | 'content': make_function_call_script( |
145 | lldpd_wait, "/var/run/lldpd.socket", time_delay=60), |
146 | |
147 | === modified file 'src/metadataserver/models/tests/test_noderesults.py' |
148 | --- src/metadataserver/models/tests/test_noderesults.py 2014-12-05 21:07:48 +0000 |
149 | +++ src/metadataserver/models/tests/test_noderesults.py 2015-01-09 14:32:14 +0000 |
150 | @@ -17,11 +17,13 @@ |
151 | import doctest |
152 | from inspect import getsource |
153 | from io import BytesIO |
154 | +import json |
155 | from math import ( |
156 | ceil, |
157 | floor, |
158 | ) |
159 | import os.path |
160 | +import random |
161 | from random import randint |
162 | import subprocess |
163 | from subprocess import ( |
164 | @@ -35,6 +37,7 @@ |
165 | |
166 | from fixtures import FakeLogger |
167 | from maasserver.fields import MAC |
168 | +from maasserver.models.physicalblockdevice import PhysicalBlockDevice |
169 | from maasserver.models.tag import Tag |
170 | from maasserver.testing.factory import factory |
171 | from maasserver.testing.orm import reload_object |
172 | @@ -56,6 +59,7 @@ |
173 | from metadataserver.models.commissioningscript import ( |
174 | ARCHIVE_PREFIX, |
175 | extract_router_mac_addresses, |
176 | + gather_physical_block_devices, |
177 | inject_lldp_result, |
178 | inject_lshw_result, |
179 | inject_result, |
180 | @@ -65,6 +69,7 @@ |
181 | set_node_routers, |
182 | set_virtual_tag, |
183 | update_hardware_details, |
184 | + update_node_physical_block_devices, |
185 | ) |
186 | from metadataserver.models.noderesult import NodeResult |
187 | from mock import ( |
188 | @@ -676,3 +681,344 @@ |
189 | logger = self.useFixture(FakeLogger(name='commissioningscript')) |
190 | update_hardware_details(factory.make_Node(), b"garbage", exit_status=1) |
191 | self.assertEqual("", logger.output) |
192 | + |
193 | + |
194 | +class TestGatherPhysicalBlockDevices(MAASServerTestCase): |
195 | + |
196 | + def make_lsblk_output( |
197 | + self, name=None, read_only=False, removable=False, |
198 | + model=None, rotary=True): |
199 | + if name is None: |
200 | + name = factory.make_name('name') |
201 | + if model is None: |
202 | + model = factory.make_name('model') |
203 | + read_only = "1" if read_only else "0" |
204 | + removable = "1" if removable else "0" |
205 | + rotary = "1" if rotary else "0" |
206 | + return 'NAME="%s" RO="%s" RM="%s" MODEL="%s" ROTA="%s"' % ( |
207 | + name, read_only, removable, model, rotary) |
208 | + |
209 | + def make_udevadm_output(self, name, serial=None, sata=True, cdrom=False): |
210 | + if serial is None: |
211 | + serial = factory.make_name('serial') |
212 | + sata = "1" if sata else "0" |
213 | + output = dedent("""\ |
214 | + P: /devices/pci0000:00/ata3/host2/target2:0:0/2:0:0:0/block/{name} |
215 | + N: {name} |
216 | + E: DEVNAME=/dev/{name} |
217 | + E: DEVTYPE=disk |
218 | + E: ID_ATA_SATA={sata} |
219 | + E: ID_SERIAL_SHORT={serial} |
220 | + """).format(name=name, serial=serial, sata=sata) |
221 | + if cdrom: |
222 | + output += "E: ID_CDROM=1" |
223 | + return output |
224 | + |
225 | + def call_gather_physical_block_devices(self): |
226 | + return json.loads(gather_physical_block_devices(print_output=False)) |
227 | + |
228 | + def test__calls_lsblk(self): |
229 | + check_output = self.patch(subprocess, "check_output") |
230 | + check_output.return_value = "" |
231 | + self.call_gather_physical_block_devices() |
232 | + self.assertThat(check_output, MockCalledOnceWith( |
233 | + ("lsblk", "-d", "-P", "-o", "NAME,RO,RM,MODEL,ROTA"))) |
234 | + |
235 | + def test__returns_empty_list_when_no_disks(self): |
236 | + check_output = self.patch(subprocess, "check_output") |
237 | + check_output.return_value = "" |
238 | + self.assertEquals([], self.call_gather_physical_block_devices()) |
239 | + |
240 | + def test__calls_lsblk_then_udevadm(self): |
241 | + name = factory.make_name('name') |
242 | + check_output = self.patch(subprocess, "check_output") |
243 | + check_output.side_effect = [ |
244 | + self.make_lsblk_output( |
245 | + name=name), |
246 | + self.make_udevadm_output( |
247 | + name, cdrom=True), |
248 | + ] |
249 | + self.call_gather_physical_block_devices() |
250 | + self.assertThat(check_output, MockCallsMatch( |
251 | + call(("lsblk", "-d", "-P", "-o", "NAME,RO,RM,MODEL,ROTA")), |
252 | + call(("udevadm", "info", "-q", "all", "-n", name)))) |
253 | + |
254 | + def test__returns_empty_list_when_cdrom_only(self): |
255 | + name = factory.make_name('name') |
256 | + check_output = self.patch(subprocess, "check_output") |
257 | + check_output.side_effect = [ |
258 | + self.make_lsblk_output( |
259 | + name=name), |
260 | + self.make_udevadm_output( |
261 | + name, cdrom=True), |
262 | + ] |
263 | + self.assertEquals([], self.call_gather_physical_block_devices()) |
264 | + |
265 | + def test__calls_lsblk_udevadm_then_blockdev(self): |
266 | + name = factory.make_name('name') |
267 | + model = factory.make_name('model') |
268 | + serial = factory.make_name('serial') |
269 | + size = random.randint(1000 * 1000, 1000 * 1000 * 1000) |
270 | + block_size = random.choice([512, 1024, 4096]) |
271 | + check_output = self.patch(subprocess, "check_output") |
272 | + check_output.side_effect = [ |
273 | + self.make_lsblk_output(name=name, model=model), |
274 | + self.make_udevadm_output(name, serial=serial), |
275 | + '%s' % size, |
276 | + '%s' % block_size, |
277 | + ] |
278 | + self.call_gather_physical_block_devices() |
279 | + self.assertThat(check_output, MockCallsMatch( |
280 | + call(("lsblk", "-d", "-P", "-o", "NAME,RO,RM,MODEL,ROTA")), |
281 | + call(("udevadm", "info", "-q", "all", "-n", name)), |
282 | + call(("blockdev", "--getsize64", "/dev/%s" % name)), |
283 | + call(("blockdev", "--getbsz", "/dev/%s" % name)))) |
284 | + |
285 | + def test__returns_block_device(self): |
286 | + name = factory.make_name('name') |
287 | + model = factory.make_name('model') |
288 | + serial = factory.make_name('serial') |
289 | + size = random.randint(1000 * 1000, 1000 * 1000 * 1000) |
290 | + block_size = random.choice([512, 1024, 4096]) |
291 | + check_output = self.patch(subprocess, "check_output") |
292 | + check_output.side_effect = [ |
293 | + self.make_lsblk_output(name=name, model=model), |
294 | + self.make_udevadm_output(name, serial=serial), |
295 | + '%s' % size, |
296 | + '%s' % block_size, |
297 | + ] |
298 | + self.assertEquals([{ |
299 | + "NAME": name, |
300 | + "PATH": "/dev/%s" % name, |
301 | + "RO": "0", |
302 | + "RM": "0", |
303 | + "MODEL": model, |
304 | + "ROTA": "1", |
305 | + "SATA": "1", |
306 | + "SERIAL": serial, |
307 | + "SIZE": "%s" % size, |
308 | + "BLOCK_SIZE": "%s" % block_size, |
309 | + }], self.call_gather_physical_block_devices()) |
310 | + |
311 | + def test__returns_block_device_readonly(self): |
312 | + name = factory.make_name('name') |
313 | + model = factory.make_name('model') |
314 | + serial = factory.make_name('serial') |
315 | + size = random.randint(1000 * 1000, 1000 * 1000 * 1000) |
316 | + block_size = random.choice([512, 1024, 4096]) |
317 | + check_output = self.patch(subprocess, "check_output") |
318 | + check_output.side_effect = [ |
319 | + self.make_lsblk_output(name=name, model=model, read_only=True), |
320 | + self.make_udevadm_output(name, serial=serial), |
321 | + '%s' % size, |
322 | + '%s' % block_size, |
323 | + ] |
324 | + self.assertEquals([{ |
325 | + "NAME": name, |
326 | + "PATH": "/dev/%s" % name, |
327 | + "RO": "1", |
328 | + "RM": "0", |
329 | + "MODEL": model, |
330 | + "ROTA": "1", |
331 | + "SATA": "1", |
332 | + "SERIAL": serial, |
333 | + "SIZE": "%s" % size, |
334 | + "BLOCK_SIZE": "%s" % block_size, |
335 | + }], self.call_gather_physical_block_devices()) |
336 | + |
337 | + def test__returns_block_device_ssd(self): |
338 | + name = factory.make_name('name') |
339 | + model = factory.make_name('model') |
340 | + serial = factory.make_name('serial') |
341 | + size = random.randint(1000 * 1000, 1000 * 1000 * 1000) |
342 | + block_size = random.choice([512, 1024, 4096]) |
343 | + check_output = self.patch(subprocess, "check_output") |
344 | + check_output.side_effect = [ |
345 | + self.make_lsblk_output(name=name, model=model, rotary=False), |
346 | + self.make_udevadm_output(name, serial=serial), |
347 | + '%s' % size, |
348 | + '%s' % block_size, |
349 | + ] |
350 | + self.assertEquals([{ |
351 | + "NAME": name, |
352 | + "PATH": "/dev/%s" % name, |
353 | + "RO": "0", |
354 | + "RM": "0", |
355 | + "MODEL": model, |
356 | + "ROTA": "0", |
357 | + "SATA": "1", |
358 | + "SERIAL": serial, |
359 | + "SIZE": "%s" % size, |
360 | + "BLOCK_SIZE": "%s" % block_size, |
361 | + }], self.call_gather_physical_block_devices()) |
362 | + |
363 | + def test__returns_block_device_not_sata(self): |
364 | + name = factory.make_name('name') |
365 | + model = factory.make_name('model') |
366 | + serial = factory.make_name('serial') |
367 | + size = random.randint(1000 * 1000, 1000 * 1000 * 1000) |
368 | + block_size = random.choice([512, 1024, 4096]) |
369 | + check_output = self.patch(subprocess, "check_output") |
370 | + check_output.side_effect = [ |
371 | + self.make_lsblk_output(name=name, model=model), |
372 | + self.make_udevadm_output(name, serial=serial, sata=False), |
373 | + '%s' % size, |
374 | + '%s' % block_size, |
375 | + ] |
376 | + self.assertEquals([{ |
377 | + "NAME": name, |
378 | + "PATH": "/dev/%s" % name, |
379 | + "RO": "0", |
380 | + "RM": "0", |
381 | + "MODEL": model, |
382 | + "ROTA": "1", |
383 | + "SATA": "0", |
384 | + "SERIAL": serial, |
385 | + "SIZE": "%s" % size, |
386 | + "BLOCK_SIZE": "%s" % block_size, |
387 | + }], self.call_gather_physical_block_devices()) |
388 | + |
389 | + def test__returns_block_device_removable(self): |
390 | + name = factory.make_name('name') |
391 | + model = factory.make_name('model') |
392 | + serial = factory.make_name('serial') |
393 | + size = random.randint(1000 * 1000, 1000 * 1000 * 1000) |
394 | + block_size = random.choice([512, 1024, 4096]) |
395 | + check_output = self.patch(subprocess, "check_output") |
396 | + check_output.side_effect = [ |
397 | + self.make_lsblk_output(name=name, model=model, removable=True), |
398 | + self.make_udevadm_output(name, serial=serial), |
399 | + '%s' % size, |
400 | + '%s' % block_size, |
401 | + ] |
402 | + self.assertEquals([{ |
403 | + "NAME": name, |
404 | + "PATH": "/dev/%s" % name, |
405 | + "RO": "0", |
406 | + "RM": "1", |
407 | + "MODEL": model, |
408 | + "ROTA": "1", |
409 | + "SATA": "1", |
410 | + "SERIAL": serial, |
411 | + "SIZE": "%s" % size, |
412 | + "BLOCK_SIZE": "%s" % block_size, |
413 | + }], self.call_gather_physical_block_devices()) |
414 | + |
415 | + def test__returns_multiple_block_devices_in_order(self): |
416 | + names = [factory.make_name('name') for _ in range(3)] |
417 | + lsblk = [ |
418 | + self.make_lsblk_output(name=name) |
419 | + for name in names |
420 | + ] |
421 | + call_outputs = [] |
422 | + call_outputs.append("\n".join(lsblk)) |
423 | + for name in names: |
424 | + call_outputs.append(self.make_udevadm_output(name)) |
425 | + for name in names: |
426 | + call_outputs.append( |
427 | + "%s" % random.randint(1000 * 1000, 1000 * 1000 * 1000)) |
428 | + call_outputs.append( |
429 | + "%s" % random.choice([512, 1024, 4096])) |
430 | + check_output = self.patch(subprocess, "check_output") |
431 | + check_output.side_effect = call_outputs |
432 | + device_names = [ |
433 | + block_info['NAME'] |
434 | + for block_info in self.call_gather_physical_block_devices() |
435 | + ] |
436 | + self.assertEquals(names, device_names) |
437 | + |
438 | + |
439 | +class TestUpdateNodePhysicalBlockDevices(MAASServerTestCase): |
440 | + |
441 | + def make_block_device( |
442 | + self, name=None, path=None, size=None, block_size=None, |
443 | + model=None, serial=None): |
444 | + if name is None: |
445 | + name = factory.make_name('name') |
446 | + if path is None: |
447 | + path = '/dev/%s' % name |
448 | + if size is None: |
449 | + size = random.randint(1000 * 1000, 1000 * 1000 * 1000) |
450 | + if block_size is None: |
451 | + block_size = random.choice([512, 1024, 4096]) |
452 | + if model is None: |
453 | + model = factory.make_name('model') |
454 | + if serial is None: |
455 | + serial = factory.make_name('serial') |
456 | + return { |
457 | + "NAME": name, |
458 | + "PATH": path, |
459 | + "SIZE": '%s' % size, |
460 | + "BLOCK_SIZE": '%s' % block_size, |
461 | + "MODEL": model, |
462 | + "SERIAL": serial, |
463 | + "RO": "0", |
464 | + "RM": "0", |
465 | + "ROTA": "1", |
466 | + } |
467 | + |
468 | + def test__does_nothing_when_exit_status_is_not_zero(self): |
469 | + node = factory.make_Node() |
470 | + block_device = factory.make_PhysicalBlockDevice(node=node) |
471 | + update_node_physical_block_devices(node, b"garbage", exit_status=1) |
472 | + self.assertIsNotNone(reload_object(block_device)) |
473 | + |
474 | + def test__clears_previous_physical_block_devices(self): |
475 | + node = factory.make_Node() |
476 | + block_device = factory.make_PhysicalBlockDevice(node=node) |
477 | + update_node_physical_block_devices(node, b"[]", 0) |
478 | + self.assertIsNone(reload_object(block_device)) |
479 | + |
480 | + def test__creates_physical_block_devices(self): |
481 | + devices = [self.make_block_device() for _ in range(3)] |
482 | + device_names = [device['NAME'] for device in devices] |
483 | + node = factory.make_Node() |
484 | + json_output = json.dumps(devices).encode('utf-8') |
485 | + update_node_physical_block_devices(node, json_output, 0) |
486 | + created_names = [ |
487 | + device.name |
488 | + for device in PhysicalBlockDevice.objects.filter(node=node) |
489 | + ] |
490 | + self.assertItemsEqual(device_names, created_names) |
491 | + |
492 | + def test__creates_physical_block_devices_in_order(self): |
493 | + devices = [self.make_block_device() for _ in range(3)] |
494 | + device_names = [device['NAME'] for device in devices] |
495 | + node = factory.make_Node() |
496 | + json_output = json.dumps(devices).encode('utf-8') |
497 | + update_node_physical_block_devices(node, json_output, 0) |
498 | + created_names = [ |
499 | + device.name |
500 | + for device in ( |
501 | + PhysicalBlockDevice.objects.filter(node=node).order_by('id')) |
502 | + ] |
503 | + self.assertEquals(device_names, created_names) |
504 | + |
505 | + def test__creates_physical_block_device(self): |
506 | + name = factory.make_name('name') |
507 | + path = '/dev/%s' % name |
508 | + size = random.randint(1000 * 1000, 1000 * 1000 * 1000) |
509 | + block_size = random.choice([512, 1024, 4096]) |
510 | + model = factory.make_name('model') |
511 | + serial = factory.make_name('serial') |
512 | + device = self.make_block_device( |
513 | + name=name, path=path, size=size, block_size=block_size, |
514 | + model=model, serial=serial) |
515 | + node = factory.make_Node() |
516 | + json_output = json.dumps([device]).encode('utf-8') |
517 | + update_node_physical_block_devices(node, json_output, 0) |
518 | + self.assertThat( |
519 | + PhysicalBlockDevice.objects.filter(node=node).first(), |
520 | + MatchesStructure.byEquality( |
521 | + name=name, path=path, size=size, block_size=block_size, |
522 | + model=model, serial=serial)) |
523 | + |
524 | + def test__creates_physical_block_device_only_for_node(self): |
525 | + device = self.make_block_device() |
526 | + node = factory.make_Node() |
527 | + other_node = factory.make_Node() |
528 | + json_output = json.dumps([device]).encode('utf-8') |
529 | + update_node_physical_block_devices(node, json_output, 0) |
530 | + self.assertEquals( |
531 | + 0, PhysicalBlockDevice.objects.filter(node=other_node).count(), |
532 | + "Created physical block device for the incorrect node.") |
This looks generally good. I've got a bunch of comments but more importantly, I'm wondering about the failure modes for this. See below.
This uses a lot of commands and a fair share of parsing. All of which could go wrong at some point and break the commissioning. I think it's worth taking a step back and thinking about the failure modes here:
- which part of the code are likely to fail (command failure, parsing failure, etc)? Can we make this more robust against non-fatal error?
- what's the failure mode? In other words, what is going to happen when this fails, what will the consequences be, how easy will it be to the user to get information about what's wrong?
What do you think?