Merge lp:~blake-rouse/maas/commissioning-get-disk-info into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
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
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.

To post a comment you must log in.
Revision history for this message
Raphaël Badin (rvb) wrote :

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?

review: Needs Information
Revision history for this message
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.

Revision history for this message
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)

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (20.4 KiB)

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://security.ubuntu.com trusty-security InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Get:2 http://security.ubuntu.com trusty-security Release [62.0 kB]
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Get:3 http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg [933 B]
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Get:4 http://nova.clouds.archive.ubuntu.com trusty-updates Release [62.0 kB]
Get:5 http://security.ubuntu.com trusty-security/main Sources [59.7 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Get:6 http://security.ubuntu.com trusty-security/universe Sources [17.4 kB]
Get:7 http://security.ubuntu.com trusty-security/main amd64 Packages [187 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Get:8 http://security.ubuntu.com trusty-security/universe amd64 Packages [82.2 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Get:9 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [152 kB]
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Get:10 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [96.5 kB]
Get:11 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [392 kB]
Get:12 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [236 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Fetched 1,348 kB in 3s (441 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools gjs ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make pep8 postgresql pyflakes python-amqplib python-bzrlib python-celery python-convoy python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-jinja2 python-jsonschema python-lockfile python-lxml python-mimeparse python-mock python-netaddr python-netifaces python-nose python-oauth python-oops python-oops-amqp python-oops-datedir-repo python-oops-twisted python-oops-wsg...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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.")