Merge lp:~jderose/dmedia/drives-plus into lp:dmedia

Proposed by Jason Gerard DeRose
Status: Merged
Merged at revision: 759
Proposed branch: lp:~jderose/dmedia/drives-plus
Merge into: lp:dmedia
Diff against target: 1081 lines (+703/-166)
4 files modified
dmedia-provision-drive (+12/-4)
dmedia-service (+3/-2)
dmedia/drives.py (+181/-135)
dmedia/tests/test_drives.py (+507/-25)
To merge this branch: bzr merge lp:~jderose/dmedia/drives-plus
Reviewer Review Type Date Requested Status
David Jordan Approve
Review via email: mp+193727@code.launchpad.net

Description of the change

For background, please see this bug:
https://bugs.launchpad.net/dmedia/+bug/1247699

Despite a fairly large diff, there is no functionality change here. This is simply a cleanup and refactoring in order to make it easier to write unit tests, plus providing said unit tests.

Currently the only two consumers of the `dmedia.drives` module are the `dmedia-provision-drive` script and the `dmedia-service` script.

To post a comment you must log in.
Revision history for this message
David Jordan (dmj726) wrote :

looks fine

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'dmedia-provision-drive'
2--- dmedia-provision-drive 2013-08-04 14:03:47 +0000
3+++ dmedia-provision-drive 2013-11-04 01:28:24 +0000
4@@ -44,11 +44,12 @@
5
6 import argparse
7 import os
8+import tempfile
9
10 from filestore import _dumps
11 from dbase32 import random_id, isdb32
12
13-from dmedia.drives import Drive, VALID_DRIVE
14+from dmedia.drives import Drive, Devices, VALID_DRIVE
15
16
17 parser = argparse.ArgumentParser()
18@@ -90,7 +91,14 @@
19
20 if os.getuid() != 0:
21 raise SystemExit('Error: must be run as root')
22+
23 drive = Drive(args.dev)
24-doc = drive.provision(args.label, args.id)
25-print(_dumps(doc))
26-
27+partition = drive.provision(args.label, args.id)
28+devices = Devices()
29+info = devices.get_partition_info(partition.dev)
30+tmpdir = tempfile.mkdtemp(prefix='dmedia.')
31+try:
32+ doc = partition.create_filestore(tmpdir, args.id, 1, **info)
33+ print(_dumps(doc))
34+finally:
35+ os.rmdir(tmpdir)
36
37=== modified file 'dmedia-service'
38--- dmedia-service 2013-10-29 03:31:24 +0000
39+++ dmedia-service 2013-11-04 01:28:24 +0000
40@@ -51,7 +51,7 @@
41 from dmedia.service.background import Snapshots, LazyAccess, Downloads
42 from dmedia.service.avahi import Avahi
43 from dmedia.service.peers import Browser, Publisher
44-from dmedia.drives import get_parentdir_info
45+from dmedia.drives import Devices
46
47
48 BUS = dmedia.BUS
49@@ -60,6 +60,7 @@
50 session = dbus.SessionBus()
51 mainloop = GLib.MainLoop()
52 VolumeMonitor = Gio.VolumeMonitor.get()
53+devices = Devices()
54
55
56 def on_sighup(signum, frame):
57@@ -206,7 +207,7 @@
58 if isfilestore(self.couch.basedir):
59 self.core.connect_filestore(self.couch.basedir)
60 else:
61- info = get_parentdir_info(self.couch.basedir)
62+ info = devices.get_parentdir_info(self.couch.basedir)
63 self.core.create_filestore(self.couch.basedir, **info)
64 VolumeMonitor.connect('mount-added', self.on_mount_added)
65 VolumeMonitor.connect('mount-pre-unmount', self.on_mount_pre_unmount)
66
67=== modified file 'dmedia/drives.py'
68--- dmedia/drives.py 2013-11-02 19:06:33 +0000
69+++ dmedia/drives.py 2013-11-04 01:28:24 +0000
70@@ -24,10 +24,9 @@
71 """
72
73 from uuid import UUID
74-from subprocess import check_call, check_output
75+import subprocess
76 import re
77 import time
78-import tempfile
79 import os
80 from os import path
81
82@@ -38,9 +37,20 @@
83 from .units import bytes10
84
85
86-udev_client = GUdev.Client.new(['block'])
87 VALID_DRIVE = re.compile('^/dev/[sv]d[a-z]$')
88-VALID_PARTITION = re.compile('^/dev/[sv]d[a-z][1-9]$')
89+VALID_PARTITION = re.compile('^(/dev/[sv]d[a-z])([1-9])$')
90+
91+
92+def check_drive_dev(dev):
93+ if not VALID_DRIVE.match(dev):
94+ raise ValueError('Invalid drive device file: {!r}'.format(dev))
95+ return dev
96+
97+
98+def check_partition_dev(dev):
99+ if not VALID_PARTITION.match(dev):
100+ raise ValueError('Invalid partition device file: {!r}'.format(dev))
101+ return dev
102
103
104 def db32_to_uuid(store_id):
105@@ -91,17 +101,6 @@
106 return string.replace('\\x20', ' ').strip()
107
108
109-class NoSuchDevice(Exception):
110- pass
111-
112-
113-def get_device(dev):
114- device = udev_client.query_by_device_file(dev)
115- if device is None:
116- raise NoSuchDevice('No such device: {!r}'.format(dev))
117- return device
118-
119-
120 def get_drive_info(device):
121 physical = device.get_sysfs_attr_as_uint64('queue/physical_block_size')
122 logical = device.get_sysfs_attr_as_uint64('queue/logical_block_size')
123@@ -110,15 +109,20 @@
124 return {
125 'drive_block_physical': physical,
126 'drive_block_logical': logical,
127+ 'drive_alignment_offset': device.get_sysfs_attr_as_int('alignment_offset'),
128+ 'drive_discard_alignment': device.get_sysfs_attr_as_int('discard_alignment'),
129 'drive_bytes': drive_bytes,
130 'drive_size': bytes10(drive_bytes),
131- 'drive_model': device.get_property('ID_MODEL'),
132+ 'drive_model': unfuck(device.get_property('ID_MODEL_ENC')),
133 'drive_model_id': device.get_property('ID_MODEL_ID'),
134 'drive_revision': device.get_property('ID_REVISION'),
135 'drive_serial': device.get_property('ID_SERIAL_SHORT'),
136 'drive_wwn': device.get_property('ID_WWN_WITH_EXTENSION'),
137- 'drive_vendor': device.get_property('ID_VENDOR'),
138+ 'drive_vendor': unfuck(device.get_property('ID_VENDOR_ENC')),
139+ 'drive_vendor_id': device.get_property('ID_VENDOR_ID'),
140 'drive_removable': bool(device.get_sysfs_attr_as_int('removable')),
141+ 'drive_bus': device.get_property('ID_BUS'),
142+ 'drive_rpm': device.get_property('ID_ATA_ROTATION_RATE_RPM'),
143 }
144
145
146@@ -151,8 +155,8 @@
147 'partition_scheme': device.get_property('ID_PART_ENTRY_SCHEME'),
148 'partition_number': device.get_property_as_int('ID_PART_ENTRY_NUMBER'),
149 'partition_bytes': part_bytes,
150+ 'partition_start_bytes': part_start_sector * logical,
151 'partition_size': bytes10(part_bytes),
152- 'partition_start_bytes': part_start_sector * logical,
153
154 'filesystem_type': device.get_property('ID_FS_TYPE'),
155 'filesystem_uuid': device.get_property('ID_FS_UUID'),
156@@ -169,22 +173,68 @@
157 raise ValueError('Could not find disk size with unit=MiB')
158
159
160-class Drive:
161- def __init__(self, dev):
162- if not VALID_DRIVE.match(dev):
163- raise ValueError('Invalid drive device file: {!r}'.format(dev))
164- self.dev = dev
165+def parse_mounts(procdir='/proc'):
166+ text = open(path.join(procdir, 'mounts'), 'r').read()
167+ mounts = {}
168+ for line in text.splitlines():
169+ (dev, mount, type_, options, dump, pass_) = line.split()
170+ mounts[mount.replace('\\040', ' ')] = dev
171+ return mounts
172+
173+
174+class Mockable:
175+ """
176+ Mock calls to `subprocess.check_call()`, `subprocess.check_output()`.
177+ """
178+
179+ def __init__(self, mocking=False):
180+ assert isinstance(mocking, bool)
181+ self.mocking = mocking
182+ self.calls = []
183+ self.outputs = []
184+
185+ def reset(self, mocking=False, outputs=None):
186+ assert isinstance(mocking, bool)
187+ self.mocking = mocking
188+ self.calls.clear()
189+ self.outputs.clear()
190+ if outputs:
191+ assert mocking is True
192+ for value in outputs:
193+ assert isinstance(value, bytes)
194+ self.outputs.append(value)
195+
196+ def check_call(self, cmd):
197+ assert isinstance(cmd, list)
198+ if self.mocking:
199+ self.calls.append(('check_call', cmd))
200+ else:
201+ subprocess.check_call(cmd)
202+
203+ def check_output(self, cmd):
204+ assert isinstance(cmd, list)
205+ if self.mocking:
206+ self.calls.append(('check_output', cmd))
207+ return self.outputs.pop(0)
208+ else:
209+ return subprocess.check_output(cmd)
210+
211+
212+class Drive(Mockable):
213+ def __init__(self, dev, mocking=False):
214+ super().__init__(mocking)
215+ self.dev = check_drive_dev(dev)
216
217 def get_partition(self, index):
218 assert isinstance(index, int)
219 assert index >= 1
220- return Partition('{}{}'.format(self.dev, index))
221+ return Partition('{}{}'.format(self.dev, index), mocking=self.mocking)
222
223 def rereadpt(self):
224- check_call(['blockdev', '--rereadpt', self.dev])
225+ self.check_call(['blockdev', '--rereadpt', self.dev])
226
227 def zero(self):
228- check_call(['dd',
229+ self.check_call(['dd',
230 'if=/dev/zero',
231 'of={}'.format(self.dev),
232 'bs=4M',
233@@ -201,37 +251,34 @@
234 return cmd
235
236 def mklabel(self):
237- check_call(self.parted('mklabel', 'gpt'))
238+ self.check_call(self.parted('mklabel', 'gpt'))
239
240 def print(self):
241 cmd = self.parted('print')
242- return check_output(cmd).decode('utf-8')
243+ return self.check_output(cmd).decode('utf-8')
244
245 def init_partition_table(self):
246 self.rereadpt() # Make sure existing partitions aren't mounted
247 self.zero()
248- time.sleep(2)
249+ time.sleep(1)
250 self.rereadpt()
251 self.mklabel()
252-
253- text = self.print()
254- print(text)
255- self.size = parse_drive_size(text)
256+ self.size = parse_drive_size(self.print())
257 self.index = 0
258 self.start = 1
259 self.stop = self.size - 1
260 assert self.start < self.stop
261
262- @property
263- def remaining(self):
264- return self.stop - self.start
265-
266 def mkpart(self, start, stop):
267 assert isinstance(start, int)
268 assert isinstance(stop, int)
269 assert 1 <= start < stop <= self.stop
270 cmd = self.parted('mkpart', 'primary', 'ext2', str(start), str(stop))
271- check_call(cmd)
272+ self.check_call(cmd)
273+
274+ @property
275+ def remaining(self):
276+ return self.stop - self.start
277
278 def add_partition(self, size):
279 assert isinstance(size, int)
280@@ -246,19 +293,14 @@
281 self.init_partition_table()
282 partition = self.add_partition(self.remaining)
283 partition.mkfs_ext4(label, store_id)
284- time.sleep(2)
285- doc = partition.create_filestore(store_id)
286- return doc
287-
288-
289-class Partition:
290- def __init__(self, dev):
291- if not VALID_PARTITION.match(dev):
292- raise ValueError('Invalid partition device file: {!r}'.format(dev))
293- self.dev = dev
294-
295- def get_info(self):
296- return get_partition_info(get_device(self.dev))
297+ time.sleep(1)
298+ return partition
299+
300+
301+class Partition(Mockable):
302+ def __init__(self, dev, mocking=False):
303+ super().__init__(mocking)
304+ self.dev = check_partition_dev(dev)
305
306 def mkfs_ext4(self, label, store_id):
307 cmd = ['mkfs.ext4', self.dev,
308@@ -266,97 +308,101 @@
309 '-U', db32_to_uuid(store_id),
310 '-m', '0', # 0% reserved blocks
311 ]
312- check_call(cmd)
313+ self.check_call(cmd)
314
315- def create_filestore(self, store_id, copies=1):
316- kw = self.get_info()
317- tmpdir = tempfile.mkdtemp(prefix='dmedia.')
318+ def create_filestore(self, mount, store_id=None, copies=1, **kw):
319 fs = None
320- check_call(['mount', self.dev, tmpdir])
321+ self.check_call(['mount', self.dev, mount])
322 try:
323- fs = FileStore.create(tmpdir, store_id, 1, **kw)
324- check_call(['chmod', '0777', tmpdir])
325+ fs = FileStore.create(mount, store_id, copies, **kw)
326+ self.check_call(['chmod', '0777', mount])
327 return fs.doc
328 finally:
329 del fs
330- check_call(['umount', tmpdir])
331- os.rmdir(tmpdir)
332+ self.check_call(['umount', self.dev])
333+
334+
335+class DeviceNotFound(Exception):
336+ def __init__(self, dev):
337+ self.dev = dev
338+ super().__init__('No such device: {!r}'.format(dev))
339
340
341 class Devices:
342+ """
343+ Gather disk and partition info using udev.
344+ """
345+
346 def __init__(self):
347- self.client = GUdev.Client.new(['block'])
348- self.partitions = {}
349-
350- def run(self):
351- self.client.connect('uevent', self.on_uevent)
352- for device in self.client.query_by_subsystem('block'):
353- self.on_uevent(None, 'add', device)
354-
355- def on_uevent(self, client, action, device):
356- _type = device.get_devtype()
357- dev = device.get_device_file()
358- if _type == 'partition':
359- print(action, _type, dev)
360- if action == 'add':
361- self.add_partition(device)
362- elif action == 'remove':
363- self.remove_partition(device)
364-
365- def add_partition(self, device):
366- dev = device.get_device_file()
367- print('add_partition({!r})'.format(dev))
368-
369- def remove_partition(self, device):
370- dev = device.get_device_file()
371- print('add_partition({!r})'.format(dev))
372-
373-
374-def parse_mounts(procdir='/proc'):
375- text = open(path.join(procdir, 'mounts'), 'r').read()
376- mounts = {}
377- for line in text.splitlines():
378- (dev, mount, type_, options, dump, pass_) = line.split()
379- mounts[mount.replace('\\040', ' ')] = dev
380- return mounts
381-
382-
383-def get_homedir_info(homedir):
384- mounts = parse_mounts()
385- mountdir = homedir
386- while True:
387- if mountdir in mounts:
388- try:
389- device = get_device(mounts[mountdir])
390- return get_partition_info(device)
391- except NoSuchDevice:
392- pass
393- if mountdir == '/':
394- return {}
395- mountdir = path.dirname(mountdir)
396-
397-
398-def get_parentdir_info(parentdir):
399- mounts = parse_mounts()
400- mountdir = parentdir
401- while True:
402- if mountdir in mounts:
403- try:
404- device = get_device(mounts[mountdir])
405- return get_partition_info(device)
406- except NoSuchDevice:
407- pass
408- if mountdir == '/':
409- return {}
410- mountdir = path.dirname(mountdir)
411-
412-
413-def get_mountdir_info(mountdir):
414- mounts = parse_mounts()
415- if mountdir in mounts:
416- try:
417- device = get_device(mounts[mountdir])
418- return get_partition_info(device)
419- except NoSuchDevice:
420- pass
421- return {}
422+ self.udev_client = self.get_udev_client()
423+
424+ def get_udev_client(self):
425+ """
426+ Making this easy to override for mocking purposes.
427+ """
428+ return GUdev.Client.new(['block'])
429+
430+ def get_device(self, dev):
431+ """
432+ Get a device object by its dev path (eg, ``'/dev/sda'``).
433+ """
434+ device = self.udev_client.query_by_device_file(dev)
435+ if device is None:
436+ raise DeviceNotFound(dev)
437+ return device
438+
439+ def get_drive_info(self, dev):
440+ device = self.get_device(check_drive_dev(dev))
441+ return get_drive_info(device)
442+
443+ def get_partition_info(self, dev):
444+ device = self.get_device(check_partition_dev(dev))
445+ return get_partition_info(device)
446+
447+ def iter_drives(self):
448+ for device in self.udev_client.query_by_subsystem('block'):
449+ if device.get_devtype() != 'disk':
450+ continue
451+ if VALID_DRIVE.match(device.get_device_file()):
452+ yield device
453+
454+ def iter_partitions(self):
455+ for device in self.udev_client.query_by_subsystem('block'):
456+ if device.get_devtype() != 'partition':
457+ continue
458+ if VALID_PARTITION.match(device.get_device_file()):
459+ yield device
460+
461+ def get_parentdir_info(self, parentdir):
462+ assert path.abspath(parentdir) == parentdir
463+ mounts = parse_mounts()
464+ mountdir = parentdir
465+ while True:
466+ if mountdir in mounts:
467+ try:
468+ device = self.get_device(mounts[mountdir])
469+ return get_partition_info(device)
470+ except DeviceNotFound:
471+ pass
472+ if mountdir == '/':
473+ return {}
474+ mountdir = path.dirname(mountdir)
475+
476+ def get_info(self):
477+ return {
478+ 'drives': dict(
479+ (drive.get_device_file(), get_drive_info(drive))
480+ for drive in self.iter_drives()
481+ ),
482+ 'partitions': dict(
483+ (partition.get_device_file(), get_drive_info(partition))
484+ for partition in self.iter_partitions()
485+ ),
486+ }
487+
488+
489+if __name__ == '__main__':
490+ d = Devices()
491+ print(_dumps(d.get_info()))
492+ print(_dumps(parse_mounts()))
493+ print(_dumps(d.get_parentdir_info('/')))
494
495=== modified file 'dmedia/tests/test_drives.py'
496--- dmedia/tests/test_drives.py 2013-07-21 20:04:46 +0000
497+++ dmedia/tests/test_drives.py 2013-11-04 01:28:24 +0000
498@@ -26,12 +26,21 @@
499 from unittest import TestCase
500 import uuid
501 import os
502-
503-from dbase32 import db32enc
504-
505+import string
506+import re
507+import subprocess
508+from random import SystemRandom
509+
510+from filestore import FileStore
511+from dbase32 import db32enc, random_id
512+from gi.repository import GUdev
513+
514+from .base import TempDir
515 from dmedia import drives
516
517
518+random = SystemRandom()
519+
520 PARTED_PRINT = """
521 Model: ATA WDC WD30EZRX-00D (scsi)
522 Disk /dev/sdd: 2861588MiB
523@@ -42,6 +51,76 @@
524 1 1.00MiB 2861587MiB 2861586MiB ext4 primary
525 """
526
527+EXPECTED_DRIVE_KEYS = (
528+ 'drive_block_physical',
529+ 'drive_block_logical',
530+ 'drive_alignment_offset',
531+ 'drive_discard_alignment',
532+ 'drive_bytes',
533+ 'drive_size',
534+ 'drive_model',
535+ 'drive_model_id',
536+ 'drive_revision',
537+ 'drive_serial',
538+ 'drive_wwn',
539+ 'drive_vendor',
540+ 'drive_vendor_id',
541+ 'drive_removable',
542+ 'drive_bus',
543+ 'drive_rpm',
544+)
545+
546+EXPECTED_PARTITION_KEYS = EXPECTED_DRIVE_KEYS + (
547+ 'partition_scheme',
548+ 'partition_number',
549+ 'partition_bytes',
550+ 'partition_start_bytes',
551+ 'partition_size',
552+ 'partition_size',
553+ 'filesystem_type',
554+ 'filesystem_uuid',
555+ 'filesystem_label',
556+)
557+
558+
559+def random_drive_dev():
560+ base = random.choice(['/dev/sd', '/dev/vd'])
561+ letter = random.choice(string.ascii_lowercase)
562+ dev = '{}{}'.format(base, letter)
563+ assert drives.VALID_DRIVE.match(dev)
564+ return dev
565+
566+
567+def random_partition_dev():
568+ drive_dev = random_drive_dev()
569+ number = random.randint(1, 9)
570+ dev = '{}{:d}'.format(drive_dev, number)
571+ assert drives.VALID_PARTITION.match(dev)
572+ return dev
573+
574+
575+class TestConstants(TestCase):
576+ def test_VALID_DRIVE(self):
577+ self.assertIsInstance(drives.VALID_DRIVE, re._pattern_type)
578+ for base in ('/dev/sd', '/dev/vd'):
579+ for letter in string.ascii_lowercase:
580+ dev = base + letter
581+ m = drives.VALID_DRIVE.match(dev)
582+ self.assertIsNotNone(m)
583+ self.assertEqual(m.group(0), dev)
584+
585+ def test_VALID_PARTITION(self):
586+ self.assertIsInstance(drives.VALID_PARTITION, re._pattern_type)
587+ for base in ('/dev/sd', '/dev/vd'):
588+ for letter in string.ascii_lowercase:
589+ for number in range(1, 10):
590+ dev = '{}{}{:d}'.format(base, letter, number)
591+ m = drives.VALID_PARTITION.match(dev)
592+ self.assertIsNotNone(m)
593+ self.assertEqual(m.group(0), dev)
594+ self.assertEqual(m.group(1), base + letter)
595+ self.assertEqual(m.group(2), str(number))
596+
597
598 class TestFunctions(TestCase):
599 def test_db32_to_uuid(self):
600@@ -60,7 +139,7 @@
601 drives.uuid_to_db32(str(uuid.UUID(bytes=data))),
602 db32enc(data[:15])
603 )
604-
605+
606 def test_unfuck(self):
607 self.assertIsNone(drives.unfuck(None))
608 fucked = 'WDC\\x20WD30EZRX-00DC0B0\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20'
609@@ -69,12 +148,144 @@
610 def test_parse_drive_size(self):
611 self.assertEqual(drives.parse_drive_size(PARTED_PRINT), 2861588)
612
613+ def test_get_drive_info(self):
614+ d = drives.Devices()
615+ for device in d.iter_drives():
616+ info = drives.get_drive_info(device)
617+ self.assertEqual(set(info), set(EXPECTED_DRIVE_KEYS))
618+
619+ def test_get_partition_info(self):
620+ d = drives.Devices()
621+ for device in d.iter_partitions():
622+ info = drives.get_partition_info(device)
623+ self.assertEqual(set(info), set(EXPECTED_PARTITION_KEYS))
624+ m = drives.VALID_PARTITION.match(device.get_device_file())
625+ self.assertIsNotNone(m)
626+ drive_device = d.get_device(m.group(1))
627+ sub = dict(
628+ (key, info[key]) for key in EXPECTED_DRIVE_KEYS
629+ )
630+ self.assertEqual(sub, drives.get_drive_info(drive_device))
631+
632+ def test_parse_mounts(self):
633+ mounts = drives.parse_mounts()
634+ self.assertIsInstance(mounts, dict)
635+ for (key, value) in mounts.items():
636+ self.assertIsInstance(key, str)
637+ self.assertIsInstance(value, str)
638+
639+
640+class TestMockable(TestCase):
641+ def test_init(self):
642+ inst = drives.Mockable()
643+ self.assertIs(inst.mocking, False)
644+ self.assertEqual(inst.calls, [])
645+ self.assertEqual(inst.outputs, [])
646+
647+ inst = drives.Mockable(mocking=True)
648+ self.assertIs(inst.mocking, True)
649+ self.assertEqual(inst.calls, [])
650+ self.assertEqual(inst.outputs, [])
651+
652+ def test_reset(self):
653+ inst = drives.Mockable()
654+ calls = inst.calls
655+ outputs = inst.outputs
656+ self.assertIsNone(inst.reset())
657+ self.assertIs(inst.mocking, False)
658+ self.assertIs(inst.calls, calls)
659+ self.assertEqual(inst.calls, [])
660+ self.assertIs(inst.outputs, outputs)
661+ self.assertEqual(inst.outputs, [])
662+
663+ inst = drives.Mockable(mocking=True)
664+ calls = inst.calls
665+ outputs = inst.outputs
666+ inst.calls.extend(
667+ [('check_call', ['stuff']), ('check_output', ['junk'])]
668+ )
669+ inst.outputs.extend([b'foo', b'bar'])
670+ self.assertIsNone(inst.reset())
671+ self.assertIs(inst.mocking, False)
672+ self.assertIs(inst.calls, calls)
673+ self.assertEqual(inst.calls, [])
674+ self.assertIs(inst.outputs, outputs)
675+ self.assertEqual(inst.outputs, [])
676+
677+ inst = drives.Mockable(mocking=True)
678+ calls = inst.calls
679+ outputs = inst.outputs
680+ inst.calls.extend(
681+ [('check_call', ['stuff']), ('check_output', ['junk'])]
682+ )
683+ inst.outputs.extend([b'foo', b'bar'])
684+ self.assertIsNone(inst.reset(mocking=True, outputs=[b'aye', b'bee']))
685+ self.assertIs(inst.mocking, True)
686+ self.assertIs(inst.calls, calls)
687+ self.assertEqual(inst.calls, [])
688+ self.assertIs(inst.outputs, outputs)
689+ self.assertEqual(inst.outputs, [b'aye', b'bee'])
690+
691+ def test_check_call(self):
692+ inst = drives.Mockable()
693+ self.assertIsNone(inst.check_call(['/bin/true']))
694+ self.assertEqual(inst.calls, [])
695+ self.assertEqual(inst.outputs, [])
696+ with self.assertRaises(subprocess.CalledProcessError) as cm:
697+ inst.check_call(['/bin/false'])
698+ self.assertEqual(cm.exception.cmd, ['/bin/false'])
699+ self.assertEqual(cm.exception.returncode, 1)
700+ self.assertEqual(inst.calls, [])
701+ self.assertEqual(inst.outputs, [])
702+
703+ inst = drives.Mockable(mocking=True)
704+ self.assertIsNone(inst.check_call(['/bin/true']))
705+ self.assertEqual(inst.calls, [
706+ ('check_call', ['/bin/true']),
707+ ])
708+ self.assertEqual(inst.outputs, [])
709+ self.assertIsNone(inst.check_call(['/bin/false']))
710+ self.assertEqual(inst.calls, [
711+ ('check_call', ['/bin/true']),
712+ ('check_call', ['/bin/false']),
713+ ])
714+ self.assertEqual(inst.outputs, [])
715+
716+ def test_check_output(self):
717+ inst = drives.Mockable()
718+ self.assertEqual(inst.check_output(['/bin/echo', 'foobar']), b'foobar\n')
719+ self.assertEqual(inst.calls, [])
720+ self.assertEqual(inst.outputs, [])
721+ with self.assertRaises(subprocess.CalledProcessError) as cm:
722+ inst.check_output(['/bin/false', 'stuff'])
723+ self.assertEqual(cm.exception.cmd, ['/bin/false', 'stuff'])
724+ self.assertEqual(cm.exception.returncode, 1)
725+ self.assertEqual(inst.calls, [])
726+ self.assertEqual(inst.outputs, [])
727+
728+ inst.reset(mocking=True, outputs=[b'foo', b'bar'])
729+ self.assertEqual(inst.check_output(['/bin/echo', 'stuff']), b'foo')
730+ self.assertEqual(inst.calls, [
731+ ('check_output', ['/bin/echo', 'stuff']),
732+ ])
733+ self.assertEqual(inst.outputs, [b'bar'])
734+ self.assertEqual(inst.check_output(['/bin/false', 'stuff']), b'bar')
735+ self.assertEqual(inst.calls, [
736+ ('check_output', ['/bin/echo', 'stuff']),
737+ ('check_output', ['/bin/false', 'stuff']),
738+ ])
739+ self.assertEqual(inst.outputs, [])
740+
741
742 class TestDrive(TestCase):
743 def test_init(self):
744 for dev in ('/dev/sda', '/dev/sdz', '/dev/vda', '/dev/vdz'):
745 inst = drives.Drive(dev)
746 self.assertIs(inst.dev, dev)
747+ self.assertIs(inst.mocking, False)
748+ inst = drives.Drive(dev, mocking=True)
749+ self.assertIs(inst.dev, dev)
750+ self.assertIs(inst.mocking, True)
751 with self.assertRaises(ValueError) as cm:
752 drives.Drive('/dev/sda1')
753 self.assertEqual(str(cm.exception),
754@@ -92,31 +303,157 @@
755 )
756
757 def test_get_partition(self):
758- inst = drives.Drive('/dev/sdb')
759- part = inst.get_partition(1)
760- self.assertIsInstance(part, drives.Partition)
761- self.assertEqual(part.dev, '/dev/sdb1')
762- part = inst.get_partition(2)
763- self.assertIsInstance(part, drives.Partition)
764- self.assertEqual(part.dev, '/dev/sdb2')
765-
766- inst = drives.Drive('/dev/vdz')
767- part = inst.get_partition(8)
768- self.assertIsInstance(part, drives.Partition)
769- self.assertEqual(part.dev, '/dev/vdz8')
770- part = inst.get_partition(9)
771- self.assertIsInstance(part, drives.Partition)
772- self.assertEqual(part.dev, '/dev/vdz9')
773+ for base in ('/dev/sd', '/dev/vd'):
774+ for letter in string.ascii_lowercase:
775+ dev = base + letter
776+ for number in range(1, 10):
777+ # mocking=True
778+ inst = drives.Drive(dev)
779+ part = inst.get_partition(number)
780+ self.assertIsInstance(part, drives.Partition)
781+ self.assertEqual(part.dev, '{}{:d}'.format(dev, number))
782+ self.assertIs(part.mocking, False)
783+ # mocking=False
784+ inst = drives.Drive(dev, mocking=True)
785+ part = inst.get_partition(number)
786+ self.assertIsInstance(part, drives.Partition)
787+ self.assertEqual(part.dev, '{}{:d}'.format(dev, number))
788+ self.assertIs(part.mocking, True)
789+
790+ def test_rereadpt(self):
791+ dev = random_drive_dev()
792+ inst = drives.Drive(dev, mocking=True)
793+ self.assertIsNone(inst.rereadpt())
794+ self.assertEqual(inst.calls, [
795+ ('check_call', ['blockdev', '--rereadpt', dev]),
796+ ])
797+
798+ def test_zero(self):
799+ dev = random_drive_dev()
800+ inst = drives.Drive(dev, mocking=True)
801+ self.assertIsNone(inst.zero())
802+ self.assertEqual(inst.calls, [
803+ ('check_call', ['dd', 'if=/dev/zero', 'of={}'.format(dev), 'bs=4M', 'count=1', 'oflag=sync']),
804+ ])
805+
806+ def test_parted(self):
807+ dev = random_drive_dev()
808+ inst = drives.Drive(dev, mocking=True)
809+ self.assertEqual(inst.parted(),
810+ ['parted', '-s', dev, 'unit', 'MiB']
811+ )
812+ self.assertEqual(inst.calls, [])
813+ self.assertEqual(inst.parted('print'),
814+ ['parted', '-s', dev, 'unit', 'MiB', 'print']
815+ )
816+ self.assertEqual(inst.calls, [])
817+ self.assertEqual(inst.parted('mklabel', 'gpt'),
818+ ['parted', '-s', dev, 'unit', 'MiB', 'mklabel', 'gpt']
819+ )
820+ self.assertEqual(inst.calls, [])
821+
822+ def test_mklabel(self):
823+ dev = random_drive_dev()
824+ inst = drives.Drive(dev, mocking=True)
825+ self.assertIsNone(inst.mklabel())
826+ self.assertEqual(inst.calls, [
827+ ('check_call', ['parted', '-s', dev, 'unit', 'MiB', 'mklabel', 'gpt']),
828+ ])
829+
830+ def test_print(self):
831+ dev = random_drive_dev()
832+ inst = drives.Drive(dev)
833+ marker = random_id()
834+ inst.reset(mocking=True, outputs=[marker.encode('utf-8')])
835+ self.assertEqual(inst.print(), marker)
836+ self.assertEqual(inst.calls, [
837+ ('check_output', ['parted', '-s', dev, 'unit', 'MiB', 'print']),
838+ ])
839+ self.assertEqual(inst.outputs, [])
840+
841+ def test_init_partition_table(self):
842+ dev = random_drive_dev()
843+ inst = drives.Drive(dev)
844+ inst.reset(mocking=True, outputs=[PARTED_PRINT.encode('utf-8')])
845+ self.assertIsNone(inst.init_partition_table())
846+ self.assertEqual(inst.calls, [
847+ ('check_call', ['blockdev', '--rereadpt', dev]),
848+ ('check_call', ['dd', 'if=/dev/zero', 'of={}'.format(dev), 'bs=4M', 'count=1', 'oflag=sync']),
849+ ('check_call', ['blockdev', '--rereadpt', dev]),
850+ ('check_call', ['parted', '-s', dev, 'unit', 'MiB', 'mklabel', 'gpt']),
851+ ('check_output', ['parted', '-s', dev, 'unit', 'MiB', 'print']),
852+ ])
853+ self.assertEqual(inst.outputs, [])
854+ self.assertEqual(inst.size, 2861588)
855+ self.assertEqual(inst.index, 0)
856+ self.assertEqual(inst.start, 1)
857+ self.assertEqual(inst.stop, 2861587)
858+
859+ def test_mkpart(self):
860+ dev = random_drive_dev()
861+ inst = drives.Drive(dev, mocking=True)
862+ inst.start = 1
863+ inst.stop = 2861587
864+ self.assertIsNone(inst.mkpart(1, 2861587))
865+ self.assertEqual(inst.calls, [
866+ ('check_call', ['parted', '-s', dev, 'unit', 'MiB', 'mkpart', 'primary', 'ext2', '1', '2861587']),
867+ ])
868+
869+ def test_remaining(self):
870+ dev = random_drive_dev()
871+ inst = drives.Drive(dev, mocking=True)
872+ inst.start = 1
873+ inst.stop = 2861587
874+ self.assertEqual(inst.remaining, 2861586)
875+ inst.start = 12345
876+ self.assertEqual(inst.remaining, 2849242)
877+ inst.stop = 1861587
878+ self.assertEqual(inst.remaining, 1849242)
879+ self.assertEqual(inst.calls, [])
880+
881+ def test_add_partition(self):
882+ dev = random_drive_dev()
883+ inst = drives.Drive(dev, mocking=True)
884+ inst.index = 0
885+ inst.start = 1
886+ inst.stop = 2861587
887+
888+ part = inst.add_partition(123456)
889+ self.assertIsInstance(part, drives.Partition)
890+ self.assertIs(part.mocking, True)
891+ self.assertEqual(part.dev, '{}{:d}'.format(dev, 1))
892+ self.assertEqual(part.calls, [])
893+ self.assertEqual(inst.index, 1)
894+ self.assertEqual(inst.calls, [
895+ ('check_call', ['parted', '-s', dev, 'unit', 'MiB', 'mkpart', 'primary', 'ext2', '1', '123457']),
896+ ])
897+ self.assertEqual(inst.remaining, 2738130)
898+
899+ part = inst.add_partition(2738130)
900+ self.assertIsInstance(part, drives.Partition)
901+ self.assertIs(part.mocking, True)
902+ self.assertEqual(part.dev, '{}{:d}'.format(dev, 2))
903+ self.assertEqual(part.calls, [])
904+ self.assertEqual(inst.index, 2)
905+ self.assertEqual(inst.calls, [
906+ ('check_call', ['parted', '-s', dev, 'unit', 'MiB', 'mkpart', 'primary', 'ext2', '1', '123457']),
907+ ('check_call', ['parted', '-s', dev, 'unit', 'MiB', 'mkpart', 'primary', 'ext2', '123457', '2861587']),
908+ ])
909+ self.assertEqual(inst.remaining, 0)
910
911
912 class TestPartition(TestCase):
913 def test_init(self):
914- for dev in ('/dev/sda1', '/dev/sda9', '/dev/sdz1', '/dev/sdz9'):
915- part = drives.Partition(dev)
916- self.assertIs(part.dev, dev)
917- for dev in ('/dev/vda1', '/dev/vda9', '/dev/vdz1', '/dev/vdz9'):
918- part = drives.Partition(dev)
919- self.assertIs(part.dev, dev)
920+ for base in ('/dev/sd', '/dev/vd'):
921+ for letter in string.ascii_lowercase:
922+ for number in range(1, 10):
923+ dev = '{}{}{:d}'.format(base, letter, number)
924+ inst = drives.Partition(dev)
925+ self.assertIs(inst.dev, dev)
926+ self.assertIs(inst.mocking, False)
927+ inst = drives.Partition(dev, mocking=True)
928+ self.assertIs(inst.dev, dev)
929+ self.assertIs(inst.mocking, True)
930
931 with self.assertRaises(ValueError) as cm:
932 drives.Partition('/dev/sda11')
933@@ -130,3 +467,148 @@
934 "Invalid partition device file: '/dev/sda0'"
935 )
936
937+ def test_mkfs_ext4(self):
938+ dev = random_partition_dev()
939+ inst = drives.Partition(dev, mocking=True)
940+ label = random_id(5)
941+ store_id = random_id()
942+ ext4_uuid = drives.db32_to_uuid(store_id)
943+ self.assertIsNone(inst.mkfs_ext4(label, store_id))
944+ self.assertEqual(inst.calls, [
945+ ('check_call', ['mkfs.ext4', dev, '-L', label, '-U', ext4_uuid, '-m', '0']),
946+ ])
947+
948+ def test_create_filestore(self):
949+ dev = random_partition_dev()
950+ inst = drives.Partition(dev, mocking=True)
951+ tmp = TempDir()
952+ store_id = random_id()
953+ ext4_uuid = drives.db32_to_uuid(store_id)
954+ label = random_id(5)
955+ serial = random_id(10)
956+ kw = {
957+ 'drive_serial': serial,
958+ 'filesystem_type': 'ext4',
959+ 'filesystem_uuid': ext4_uuid,
960+ 'filesystem_label': label,
961+ }
962+ doc = inst.create_filestore(tmp.dir, store_id, 1, **kw)
963+ self.assertIsInstance(doc, dict)
964+ self.assertEqual(set(doc), set([
965+ '_id',
966+ 'time',
967+ 'type',
968+ 'plugin',
969+ 'copies',
970+ 'drive_serial',
971+ 'filesystem_type',
972+ 'filesystem_uuid',
973+ 'filesystem_label',
974+ ]))
975+ self.assertEqual(doc, {
976+ '_id': store_id,
977+ 'time': doc['time'],
978+ 'type': 'dmedia/store',
979+ 'plugin': 'filestore',
980+ 'copies': 1,
981+ 'drive_serial': serial,
982+ 'filesystem_type': 'ext4',
983+ 'filesystem_uuid': ext4_uuid,
984+ 'filesystem_label': label,
985+ })
986+ fs = FileStore(tmp.dir, store_id)
987+ self.assertEqual(fs.doc, doc)
988+ del fs
989+ self.assertEqual(inst.calls, [
990+ ('check_call', ['mount', dev, tmp.dir]),
991+ ('check_call', ['chmod', '0777', tmp.dir]),
992+ ('check_call', ['umount', dev]),
993+ ])
994+
995+
996+class TestDeviceNotFound(TestCase):
997+ def test_init(self):
998+ dev = '/dev/{}'.format(random_id())
999+ inst = drives.DeviceNotFound(dev)
1000+ self.assertIs(inst.dev, dev)
1001+ self.assertEqual(str(inst), 'No such device: {!r}'.format(dev))
1002+
1003+
1004+class TestDevices(TestCase):
1005+ def test_init(self):
1006+ inst = drives.Devices()
1007+ self.assertIsInstance(inst.udev_client, GUdev.Client)
1008+
1009+ marker = random_id()
1010+
1011+ class Subclass(drives.Devices):
1012+ def get_udev_client(self):
1013+ return marker
1014+
1015+ inst = Subclass()
1016+ self.assertIs(inst.udev_client, marker)
1017+
1018+ def test_get_device(self):
1019+ inst = drives.Devices()
1020+ dev = '/dev/nopenopenope'
1021+ with self.assertRaises(drives.DeviceNotFound) as cm:
1022+ inst.get_device(dev)
1023+ self.assertIs(cm.exception.dev, dev)
1024+ self.assertEqual(str(cm.exception),
1025+ "No such device: '/dev/nopenopenope'"
1026+ )
1027+
1028+ def test_get_drive_info(self):
1029+ inst = drives.Devices()
1030+ for device in inst.iter_drives():
1031+ dev = device.get_device_file()
1032+ self.assertEqual(
1033+ inst.get_drive_info(dev),
1034+ drives.get_drive_info(device),
1035+ )
1036+ with self.assertRaises(ValueError) as cm:
1037+ inst.get_drive_info('/dev/sdaa')
1038+ self.assertEqual(str(cm.exception),
1039+ "Invalid drive device file: '/dev/sdaa'"
1040+ )
1041+
1042+ def test_get_partition_info(self):
1043+ inst = drives.Devices()
1044+ for device in inst.iter_partitions():
1045+ dev = device.get_device_file()
1046+ self.assertEqual(
1047+ inst.get_partition_info(dev),
1048+ drives.get_partition_info(device),
1049+ )
1050+ with self.assertRaises(ValueError) as cm:
1051+ inst.get_partition_info('/dev/sda11')
1052+ self.assertEqual(str(cm.exception),
1053+ "Invalid partition device file: '/dev/sda11'"
1054+ )
1055+
1056+ def test_iter_drives(self):
1057+ inst = drives.Devices()
1058+ for drive in inst.iter_drives():
1059+ self.assertIsInstance(drive, GUdev.Device)
1060+ self.assertEqual(drive.get_devtype(), 'disk')
1061+ self.assertTrue(drives.VALID_DRIVE.match(drive.get_device_file()))
1062+
1063+ def test_iter_partitions(self):
1064+ inst = drives.Devices()
1065+ for drive in inst.iter_partitions():
1066+ self.assertIsInstance(drive, GUdev.Device)
1067+ self.assertEqual(drive.get_devtype(), 'partition')
1068+ self.assertTrue(drives.VALID_PARTITION.match(drive.get_device_file()))
1069+
1070+ def test_get_parentdir_info(self):
1071+ inst = drives.Devices()
1072+ info = inst.get_parentdir_info('/')
1073+ self.assertIsInstance(info, dict)
1074+ if info != {}:
1075+ self.assertEqual(set(info), set(EXPECTED_PARTITION_KEYS))
1076+
1077+ def test_get_info(self):
1078+ inst = drives.Devices()
1079+ info = inst.get_info()
1080+ self.assertIsInstance(info, dict)
1081+ self.assertEqual(set(info), set(['drives', 'partitions']))

Subscribers

People subscribed via source and target branches