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
=== modified file 'dmedia-provision-drive'
--- dmedia-provision-drive 2013-08-04 14:03:47 +0000
+++ dmedia-provision-drive 2013-11-04 01:28:24 +0000
@@ -44,11 +44,12 @@
4444
45import argparse45import argparse
46import os46import os
47import tempfile
4748
48from filestore import _dumps49from filestore import _dumps
49from dbase32 import random_id, isdb3250from dbase32 import random_id, isdb32
5051
51from dmedia.drives import Drive, VALID_DRIVE52from dmedia.drives import Drive, Devices, VALID_DRIVE
5253
5354
54parser = argparse.ArgumentParser()55parser = argparse.ArgumentParser()
@@ -90,7 +91,14 @@
9091
91if os.getuid() != 0:92if os.getuid() != 0:
92 raise SystemExit('Error: must be run as root')93 raise SystemExit('Error: must be run as root')
94
93drive = Drive(args.dev)95drive = Drive(args.dev)
94doc = drive.provision(args.label, args.id)96partition = drive.provision(args.label, args.id)
95print(_dumps(doc))97devices = Devices()
9698info = devices.get_partition_info(partition.dev)
99tmpdir = tempfile.mkdtemp(prefix='dmedia.')
100try:
101 doc = partition.create_filestore(tmpdir, args.id, 1, **info)
102 print(_dumps(doc))
103finally:
104 os.rmdir(tmpdir)
97105
=== modified file 'dmedia-service'
--- dmedia-service 2013-10-29 03:31:24 +0000
+++ dmedia-service 2013-11-04 01:28:24 +0000
@@ -51,7 +51,7 @@
51from dmedia.service.background import Snapshots, LazyAccess, Downloads51from dmedia.service.background import Snapshots, LazyAccess, Downloads
52from dmedia.service.avahi import Avahi52from dmedia.service.avahi import Avahi
53from dmedia.service.peers import Browser, Publisher53from dmedia.service.peers import Browser, Publisher
54from dmedia.drives import get_parentdir_info54from dmedia.drives import Devices
5555
5656
57BUS = dmedia.BUS57BUS = dmedia.BUS
@@ -60,6 +60,7 @@
60session = dbus.SessionBus()60session = dbus.SessionBus()
61mainloop = GLib.MainLoop()61mainloop = GLib.MainLoop()
62VolumeMonitor = Gio.VolumeMonitor.get()62VolumeMonitor = Gio.VolumeMonitor.get()
63devices = Devices()
6364
6465
65def on_sighup(signum, frame):66def on_sighup(signum, frame):
@@ -206,7 +207,7 @@
206 if isfilestore(self.couch.basedir):207 if isfilestore(self.couch.basedir):
207 self.core.connect_filestore(self.couch.basedir)208 self.core.connect_filestore(self.couch.basedir)
208 else:209 else:
209 info = get_parentdir_info(self.couch.basedir)210 info = devices.get_parentdir_info(self.couch.basedir)
210 self.core.create_filestore(self.couch.basedir, **info)211 self.core.create_filestore(self.couch.basedir, **info)
211 VolumeMonitor.connect('mount-added', self.on_mount_added)212 VolumeMonitor.connect('mount-added', self.on_mount_added)
212 VolumeMonitor.connect('mount-pre-unmount', self.on_mount_pre_unmount)213 VolumeMonitor.connect('mount-pre-unmount', self.on_mount_pre_unmount)
213214
=== modified file 'dmedia/drives.py'
--- dmedia/drives.py 2013-11-02 19:06:33 +0000
+++ dmedia/drives.py 2013-11-04 01:28:24 +0000
@@ -24,10 +24,9 @@
24"""24"""
2525
26from uuid import UUID26from uuid import UUID
27from subprocess import check_call, check_output27import subprocess
28import re28import re
29import time29import time
30import tempfile
31import os30import os
32from os import path31from os import path
3332
@@ -38,9 +37,20 @@
38from .units import bytes1037from .units import bytes10
3938
4039
41udev_client = GUdev.Client.new(['block'])
42VALID_DRIVE = re.compile('^/dev/[sv]d[a-z]$')40VALID_DRIVE = re.compile('^/dev/[sv]d[a-z]$')
43VALID_PARTITION = re.compile('^/dev/[sv]d[a-z][1-9]$')41VALID_PARTITION = re.compile('^(/dev/[sv]d[a-z])([1-9])$')
42
43
44def check_drive_dev(dev):
45 if not VALID_DRIVE.match(dev):
46 raise ValueError('Invalid drive device file: {!r}'.format(dev))
47 return dev
48
49
50def check_partition_dev(dev):
51 if not VALID_PARTITION.match(dev):
52 raise ValueError('Invalid partition device file: {!r}'.format(dev))
53 return dev
4454
4555
46def db32_to_uuid(store_id):56def db32_to_uuid(store_id):
@@ -91,17 +101,6 @@
91 return string.replace('\\x20', ' ').strip()101 return string.replace('\\x20', ' ').strip()
92102
93103
94class NoSuchDevice(Exception):
95 pass
96
97
98def get_device(dev):
99 device = udev_client.query_by_device_file(dev)
100 if device is None:
101 raise NoSuchDevice('No such device: {!r}'.format(dev))
102 return device
103
104
105def get_drive_info(device):104def get_drive_info(device):
106 physical = device.get_sysfs_attr_as_uint64('queue/physical_block_size')105 physical = device.get_sysfs_attr_as_uint64('queue/physical_block_size')
107 logical = device.get_sysfs_attr_as_uint64('queue/logical_block_size')106 logical = device.get_sysfs_attr_as_uint64('queue/logical_block_size')
@@ -110,15 +109,20 @@
110 return {109 return {
111 'drive_block_physical': physical,110 'drive_block_physical': physical,
112 'drive_block_logical': logical,111 'drive_block_logical': logical,
112 'drive_alignment_offset': device.get_sysfs_attr_as_int('alignment_offset'),
113 'drive_discard_alignment': device.get_sysfs_attr_as_int('discard_alignment'),
113 'drive_bytes': drive_bytes,114 'drive_bytes': drive_bytes,
114 'drive_size': bytes10(drive_bytes),115 'drive_size': bytes10(drive_bytes),
115 'drive_model': device.get_property('ID_MODEL'),116 'drive_model': unfuck(device.get_property('ID_MODEL_ENC')),
116 'drive_model_id': device.get_property('ID_MODEL_ID'),117 'drive_model_id': device.get_property('ID_MODEL_ID'),
117 'drive_revision': device.get_property('ID_REVISION'),118 'drive_revision': device.get_property('ID_REVISION'),
118 'drive_serial': device.get_property('ID_SERIAL_SHORT'),119 'drive_serial': device.get_property('ID_SERIAL_SHORT'),
119 'drive_wwn': device.get_property('ID_WWN_WITH_EXTENSION'),120 'drive_wwn': device.get_property('ID_WWN_WITH_EXTENSION'),
120 'drive_vendor': device.get_property('ID_VENDOR'),121 'drive_vendor': unfuck(device.get_property('ID_VENDOR_ENC')),
122 'drive_vendor_id': device.get_property('ID_VENDOR_ID'),
121 'drive_removable': bool(device.get_sysfs_attr_as_int('removable')),123 'drive_removable': bool(device.get_sysfs_attr_as_int('removable')),
124 'drive_bus': device.get_property('ID_BUS'),
125 'drive_rpm': device.get_property('ID_ATA_ROTATION_RATE_RPM'),
122 }126 }
123127
124128
@@ -151,8 +155,8 @@
151 'partition_scheme': device.get_property('ID_PART_ENTRY_SCHEME'),155 'partition_scheme': device.get_property('ID_PART_ENTRY_SCHEME'),
152 'partition_number': device.get_property_as_int('ID_PART_ENTRY_NUMBER'),156 'partition_number': device.get_property_as_int('ID_PART_ENTRY_NUMBER'),
153 'partition_bytes': part_bytes,157 'partition_bytes': part_bytes,
158 'partition_start_bytes': part_start_sector * logical,
154 'partition_size': bytes10(part_bytes),159 'partition_size': bytes10(part_bytes),
155 'partition_start_bytes': part_start_sector * logical,
156160
157 'filesystem_type': device.get_property('ID_FS_TYPE'),161 'filesystem_type': device.get_property('ID_FS_TYPE'),
158 'filesystem_uuid': device.get_property('ID_FS_UUID'),162 'filesystem_uuid': device.get_property('ID_FS_UUID'),
@@ -169,22 +173,68 @@
169 raise ValueError('Could not find disk size with unit=MiB')173 raise ValueError('Could not find disk size with unit=MiB')
170174
171175
172class Drive:176def parse_mounts(procdir='/proc'):
173 def __init__(self, dev):177 text = open(path.join(procdir, 'mounts'), 'r').read()
174 if not VALID_DRIVE.match(dev):178 mounts = {}
175 raise ValueError('Invalid drive device file: {!r}'.format(dev))179 for line in text.splitlines():
176 self.dev = dev180 (dev, mount, type_, options, dump, pass_) = line.split()
181 mounts[mount.replace('\\040', ' ')] = dev
182 return mounts
183
184
185class Mockable:
186 """
187 Mock calls to `subprocess.check_call()`, `subprocess.check_output()`.
188 """
189
190 def __init__(self, mocking=False):
191 assert isinstance(mocking, bool)
192 self.mocking = mocking
193 self.calls = []
194 self.outputs = []
195
196 def reset(self, mocking=False, outputs=None):
197 assert isinstance(mocking, bool)
198 self.mocking = mocking
199 self.calls.clear()
200 self.outputs.clear()
201 if outputs:
202 assert mocking is True
203 for value in outputs:
204 assert isinstance(value, bytes)
205 self.outputs.append(value)
206
207 def check_call(self, cmd):
208 assert isinstance(cmd, list)
209 if self.mocking:
210 self.calls.append(('check_call', cmd))
211 else:
212 subprocess.check_call(cmd)
213
214 def check_output(self, cmd):
215 assert isinstance(cmd, list)
216 if self.mocking:
217 self.calls.append(('check_output', cmd))
218 return self.outputs.pop(0)
219 else:
220 return subprocess.check_output(cmd)
221
222
223class Drive(Mockable):
224 def __init__(self, dev, mocking=False):
225 super().__init__(mocking)
226 self.dev = check_drive_dev(dev)
177227
178 def get_partition(self, index):228 def get_partition(self, index):
179 assert isinstance(index, int)229 assert isinstance(index, int)
180 assert index >= 1230 assert index >= 1
181 return Partition('{}{}'.format(self.dev, index))231 return Partition('{}{}'.format(self.dev, index), mocking=self.mocking)
182232
183 def rereadpt(self):233 def rereadpt(self):
184 check_call(['blockdev', '--rereadpt', self.dev])234 self.check_call(['blockdev', '--rereadpt', self.dev])
185235
186 def zero(self):236 def zero(self):
187 check_call(['dd',237 self.check_call(['dd',
188 'if=/dev/zero',238 'if=/dev/zero',
189 'of={}'.format(self.dev),239 'of={}'.format(self.dev),
190 'bs=4M',240 'bs=4M',
@@ -201,37 +251,34 @@
201 return cmd251 return cmd
202252
203 def mklabel(self):253 def mklabel(self):
204 check_call(self.parted('mklabel', 'gpt'))254 self.check_call(self.parted('mklabel', 'gpt'))
205255
206 def print(self):256 def print(self):
207 cmd = self.parted('print')257 cmd = self.parted('print')
208 return check_output(cmd).decode('utf-8')258 return self.check_output(cmd).decode('utf-8')
209259
210 def init_partition_table(self):260 def init_partition_table(self):
211 self.rereadpt() # Make sure existing partitions aren't mounted261 self.rereadpt() # Make sure existing partitions aren't mounted
212 self.zero()262 self.zero()
213 time.sleep(2)263 time.sleep(1)
214 self.rereadpt()264 self.rereadpt()
215 self.mklabel()265 self.mklabel()
216266 self.size = parse_drive_size(self.print())
217 text = self.print()
218 print(text)
219 self.size = parse_drive_size(text)
220 self.index = 0267 self.index = 0
221 self.start = 1268 self.start = 1
222 self.stop = self.size - 1269 self.stop = self.size - 1
223 assert self.start < self.stop270 assert self.start < self.stop
224271
225 @property
226 def remaining(self):
227 return self.stop - self.start
228
229 def mkpart(self, start, stop):272 def mkpart(self, start, stop):
230 assert isinstance(start, int)273 assert isinstance(start, int)
231 assert isinstance(stop, int)274 assert isinstance(stop, int)
232 assert 1 <= start < stop <= self.stop275 assert 1 <= start < stop <= self.stop
233 cmd = self.parted('mkpart', 'primary', 'ext2', str(start), str(stop))276 cmd = self.parted('mkpart', 'primary', 'ext2', str(start), str(stop))
234 check_call(cmd)277 self.check_call(cmd)
278
279 @property
280 def remaining(self):
281 return self.stop - self.start
235282
236 def add_partition(self, size):283 def add_partition(self, size):
237 assert isinstance(size, int)284 assert isinstance(size, int)
@@ -246,19 +293,14 @@
246 self.init_partition_table()293 self.init_partition_table()
247 partition = self.add_partition(self.remaining)294 partition = self.add_partition(self.remaining)
248 partition.mkfs_ext4(label, store_id)295 partition.mkfs_ext4(label, store_id)
249 time.sleep(2)296 time.sleep(1)
250 doc = partition.create_filestore(store_id)297 return partition
251 return doc298
252299
253300class Partition(Mockable):
254class Partition:301 def __init__(self, dev, mocking=False):
255 def __init__(self, dev):302 super().__init__(mocking)
256 if not VALID_PARTITION.match(dev):303 self.dev = check_partition_dev(dev)
257 raise ValueError('Invalid partition device file: {!r}'.format(dev))
258 self.dev = dev
259
260 def get_info(self):
261 return get_partition_info(get_device(self.dev))
262304
263 def mkfs_ext4(self, label, store_id):305 def mkfs_ext4(self, label, store_id):
264 cmd = ['mkfs.ext4', self.dev,306 cmd = ['mkfs.ext4', self.dev,
@@ -266,97 +308,101 @@
266 '-U', db32_to_uuid(store_id),308 '-U', db32_to_uuid(store_id),
267 '-m', '0', # 0% reserved blocks309 '-m', '0', # 0% reserved blocks
268 ]310 ]
269 check_call(cmd)311 self.check_call(cmd)
270312
271 def create_filestore(self, store_id, copies=1):313 def create_filestore(self, mount, store_id=None, copies=1, **kw):
272 kw = self.get_info()
273 tmpdir = tempfile.mkdtemp(prefix='dmedia.')
274 fs = None314 fs = None
275 check_call(['mount', self.dev, tmpdir])315 self.check_call(['mount', self.dev, mount])
276 try:316 try:
277 fs = FileStore.create(tmpdir, store_id, 1, **kw)317 fs = FileStore.create(mount, store_id, copies, **kw)
278 check_call(['chmod', '0777', tmpdir])318 self.check_call(['chmod', '0777', mount])
279 return fs.doc319 return fs.doc
280 finally:320 finally:
281 del fs321 del fs
282 check_call(['umount', tmpdir])322 self.check_call(['umount', self.dev])
283 os.rmdir(tmpdir)323
324
325class DeviceNotFound(Exception):
326 def __init__(self, dev):
327 self.dev = dev
328 super().__init__('No such device: {!r}'.format(dev))
284329
285330
286class Devices:331class Devices:
332 """
333 Gather disk and partition info using udev.
334 """
335
287 def __init__(self):336 def __init__(self):
288 self.client = GUdev.Client.new(['block'])337 self.udev_client = self.get_udev_client()
289 self.partitions = {}338
290339 def get_udev_client(self):
291 def run(self):340 """
292 self.client.connect('uevent', self.on_uevent)341 Making this easy to override for mocking purposes.
293 for device in self.client.query_by_subsystem('block'):342 """
294 self.on_uevent(None, 'add', device)343 return GUdev.Client.new(['block'])
295344
296 def on_uevent(self, client, action, device):345 def get_device(self, dev):
297 _type = device.get_devtype()346 """
298 dev = device.get_device_file()347 Get a device object by its dev path (eg, ``'/dev/sda'``).
299 if _type == 'partition':348 """
300 print(action, _type, dev)349 device = self.udev_client.query_by_device_file(dev)
301 if action == 'add':350 if device is None:
302 self.add_partition(device)351 raise DeviceNotFound(dev)
303 elif action == 'remove':352 return device
304 self.remove_partition(device)353
305354 def get_drive_info(self, dev):
306 def add_partition(self, device):355 device = self.get_device(check_drive_dev(dev))
307 dev = device.get_device_file()356 return get_drive_info(device)
308 print('add_partition({!r})'.format(dev))357
309358 def get_partition_info(self, dev):
310 def remove_partition(self, device):359 device = self.get_device(check_partition_dev(dev))
311 dev = device.get_device_file()360 return get_partition_info(device)
312 print('add_partition({!r})'.format(dev))361
313362 def iter_drives(self):
314363 for device in self.udev_client.query_by_subsystem('block'):
315def parse_mounts(procdir='/proc'):364 if device.get_devtype() != 'disk':
316 text = open(path.join(procdir, 'mounts'), 'r').read()365 continue
317 mounts = {}366 if VALID_DRIVE.match(device.get_device_file()):
318 for line in text.splitlines():367 yield device
319 (dev, mount, type_, options, dump, pass_) = line.split()368
320 mounts[mount.replace('\\040', ' ')] = dev369 def iter_partitions(self):
321 return mounts370 for device in self.udev_client.query_by_subsystem('block'):
322371 if device.get_devtype() != 'partition':
323372 continue
324def get_homedir_info(homedir):373 if VALID_PARTITION.match(device.get_device_file()):
325 mounts = parse_mounts()374 yield device
326 mountdir = homedir375
327 while True:376 def get_parentdir_info(self, parentdir):
328 if mountdir in mounts:377 assert path.abspath(parentdir) == parentdir
329 try:378 mounts = parse_mounts()
330 device = get_device(mounts[mountdir])379 mountdir = parentdir
331 return get_partition_info(device)380 while True:
332 except NoSuchDevice:381 if mountdir in mounts:
333 pass382 try:
334 if mountdir == '/':383 device = self.get_device(mounts[mountdir])
335 return {}384 return get_partition_info(device)
336 mountdir = path.dirname(mountdir)385 except DeviceNotFound:
337386 pass
338387 if mountdir == '/':
339def get_parentdir_info(parentdir):388 return {}
340 mounts = parse_mounts()389 mountdir = path.dirname(mountdir)
341 mountdir = parentdir390
342 while True:391 def get_info(self):
343 if mountdir in mounts:392 return {
344 try:393 'drives': dict(
345 device = get_device(mounts[mountdir])394 (drive.get_device_file(), get_drive_info(drive))
346 return get_partition_info(device)395 for drive in self.iter_drives()
347 except NoSuchDevice:396 ),
348 pass397 'partitions': dict(
349 if mountdir == '/':398 (partition.get_device_file(), get_drive_info(partition))
350 return {}399 for partition in self.iter_partitions()
351 mountdir = path.dirname(mountdir)400 ),
352401 }
353402
354def get_mountdir_info(mountdir):403
355 mounts = parse_mounts()404if __name__ == '__main__':
356 if mountdir in mounts:405 d = Devices()
357 try:406 print(_dumps(d.get_info()))
358 device = get_device(mounts[mountdir])407 print(_dumps(parse_mounts()))
359 return get_partition_info(device)408 print(_dumps(d.get_parentdir_info('/')))
360 except NoSuchDevice:
361 pass
362 return {}
363409
=== modified file 'dmedia/tests/test_drives.py'
--- dmedia/tests/test_drives.py 2013-07-21 20:04:46 +0000
+++ dmedia/tests/test_drives.py 2013-11-04 01:28:24 +0000
@@ -26,12 +26,21 @@
26from unittest import TestCase26from unittest import TestCase
27import uuid27import uuid
28import os28import os
2929import string
30from dbase32 import db32enc30import re
3131import subprocess
32from random import SystemRandom
33
34from filestore import FileStore
35from dbase32 import db32enc, random_id
36from gi.repository import GUdev
37
38from .base import TempDir
32from dmedia import drives39from dmedia import drives
3340
3441
42random = SystemRandom()
43
35PARTED_PRINT = """44PARTED_PRINT = """
36Model: ATA WDC WD30EZRX-00D (scsi)45Model: ATA WDC WD30EZRX-00D (scsi)
37Disk /dev/sdd: 2861588MiB46Disk /dev/sdd: 2861588MiB
@@ -42,6 +51,76 @@
42 1 1.00MiB 2861587MiB 2861586MiB ext4 primary51 1 1.00MiB 2861587MiB 2861586MiB ext4 primary
43"""52"""
4453
54EXPECTED_DRIVE_KEYS = (
55 'drive_block_physical',
56 'drive_block_logical',
57 'drive_alignment_offset',
58 'drive_discard_alignment',
59 'drive_bytes',
60 'drive_size',
61 'drive_model',
62 'drive_model_id',
63 'drive_revision',
64 'drive_serial',
65 'drive_wwn',
66 'drive_vendor',
67 'drive_vendor_id',
68 'drive_removable',
69 'drive_bus',
70 'drive_rpm',
71)
72
73EXPECTED_PARTITION_KEYS = EXPECTED_DRIVE_KEYS + (
74 'partition_scheme',
75 'partition_number',
76 'partition_bytes',
77 'partition_start_bytes',
78 'partition_size',
79 'partition_size',
80 'filesystem_type',
81 'filesystem_uuid',
82 'filesystem_label',
83)
84
85
86def random_drive_dev():
87 base = random.choice(['/dev/sd', '/dev/vd'])
88 letter = random.choice(string.ascii_lowercase)
89 dev = '{}{}'.format(base, letter)
90 assert drives.VALID_DRIVE.match(dev)
91 return dev
92
93
94def random_partition_dev():
95 drive_dev = random_drive_dev()
96 number = random.randint(1, 9)
97 dev = '{}{:d}'.format(drive_dev, number)
98 assert drives.VALID_PARTITION.match(dev)
99 return dev
100
101
102class TestConstants(TestCase):
103 def test_VALID_DRIVE(self):
104 self.assertIsInstance(drives.VALID_DRIVE, re._pattern_type)
105 for base in ('/dev/sd', '/dev/vd'):
106 for letter in string.ascii_lowercase:
107 dev = base + letter
108 m = drives.VALID_DRIVE.match(dev)
109 self.assertIsNotNone(m)
110 self.assertEqual(m.group(0), dev)
111
112 def test_VALID_PARTITION(self):
113 self.assertIsInstance(drives.VALID_PARTITION, re._pattern_type)
114 for base in ('/dev/sd', '/dev/vd'):
115 for letter in string.ascii_lowercase:
116 for number in range(1, 10):
117 dev = '{}{}{:d}'.format(base, letter, number)
118 m = drives.VALID_PARTITION.match(dev)
119 self.assertIsNotNone(m)
120 self.assertEqual(m.group(0), dev)
121 self.assertEqual(m.group(1), base + letter)
122 self.assertEqual(m.group(2), str(number))
123
45124
46class TestFunctions(TestCase):125class TestFunctions(TestCase):
47 def test_db32_to_uuid(self):126 def test_db32_to_uuid(self):
@@ -60,7 +139,7 @@
60 drives.uuid_to_db32(str(uuid.UUID(bytes=data))),139 drives.uuid_to_db32(str(uuid.UUID(bytes=data))),
61 db32enc(data[:15])140 db32enc(data[:15])
62 )141 )
63 142
64 def test_unfuck(self):143 def test_unfuck(self):
65 self.assertIsNone(drives.unfuck(None))144 self.assertIsNone(drives.unfuck(None))
66 fucked = 'WDC\\x20WD30EZRX-00DC0B0\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20'145 fucked = 'WDC\\x20WD30EZRX-00DC0B0\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20'
@@ -69,12 +148,144 @@
69 def test_parse_drive_size(self):148 def test_parse_drive_size(self):
70 self.assertEqual(drives.parse_drive_size(PARTED_PRINT), 2861588)149 self.assertEqual(drives.parse_drive_size(PARTED_PRINT), 2861588)
71150
151 def test_get_drive_info(self):
152 d = drives.Devices()
153 for device in d.iter_drives():
154 info = drives.get_drive_info(device)
155 self.assertEqual(set(info), set(EXPECTED_DRIVE_KEYS))
156
157 def test_get_partition_info(self):
158 d = drives.Devices()
159 for device in d.iter_partitions():
160 info = drives.get_partition_info(device)
161 self.assertEqual(set(info), set(EXPECTED_PARTITION_KEYS))
162 m = drives.VALID_PARTITION.match(device.get_device_file())
163 self.assertIsNotNone(m)
164 drive_device = d.get_device(m.group(1))
165 sub = dict(
166 (key, info[key]) for key in EXPECTED_DRIVE_KEYS
167 )
168 self.assertEqual(sub, drives.get_drive_info(drive_device))
169
170 def test_parse_mounts(self):
171 mounts = drives.parse_mounts()
172 self.assertIsInstance(mounts, dict)
173 for (key, value) in mounts.items():
174 self.assertIsInstance(key, str)
175 self.assertIsInstance(value, str)
176
177
178class TestMockable(TestCase):
179 def test_init(self):
180 inst = drives.Mockable()
181 self.assertIs(inst.mocking, False)
182 self.assertEqual(inst.calls, [])
183 self.assertEqual(inst.outputs, [])
184
185 inst = drives.Mockable(mocking=True)
186 self.assertIs(inst.mocking, True)
187 self.assertEqual(inst.calls, [])
188 self.assertEqual(inst.outputs, [])
189
190 def test_reset(self):
191 inst = drives.Mockable()
192 calls = inst.calls
193 outputs = inst.outputs
194 self.assertIsNone(inst.reset())
195 self.assertIs(inst.mocking, False)
196 self.assertIs(inst.calls, calls)
197 self.assertEqual(inst.calls, [])
198 self.assertIs(inst.outputs, outputs)
199 self.assertEqual(inst.outputs, [])
200
201 inst = drives.Mockable(mocking=True)
202 calls = inst.calls
203 outputs = inst.outputs
204 inst.calls.extend(
205 [('check_call', ['stuff']), ('check_output', ['junk'])]
206 )
207 inst.outputs.extend([b'foo', b'bar'])
208 self.assertIsNone(inst.reset())
209 self.assertIs(inst.mocking, False)
210 self.assertIs(inst.calls, calls)
211 self.assertEqual(inst.calls, [])
212 self.assertIs(inst.outputs, outputs)
213 self.assertEqual(inst.outputs, [])
214
215 inst = drives.Mockable(mocking=True)
216 calls = inst.calls
217 outputs = inst.outputs
218 inst.calls.extend(
219 [('check_call', ['stuff']), ('check_output', ['junk'])]
220 )
221 inst.outputs.extend([b'foo', b'bar'])
222 self.assertIsNone(inst.reset(mocking=True, outputs=[b'aye', b'bee']))
223 self.assertIs(inst.mocking, True)
224 self.assertIs(inst.calls, calls)
225 self.assertEqual(inst.calls, [])
226 self.assertIs(inst.outputs, outputs)
227 self.assertEqual(inst.outputs, [b'aye', b'bee'])
228
229 def test_check_call(self):
230 inst = drives.Mockable()
231 self.assertIsNone(inst.check_call(['/bin/true']))
232 self.assertEqual(inst.calls, [])
233 self.assertEqual(inst.outputs, [])
234 with self.assertRaises(subprocess.CalledProcessError) as cm:
235 inst.check_call(['/bin/false'])
236 self.assertEqual(cm.exception.cmd, ['/bin/false'])
237 self.assertEqual(cm.exception.returncode, 1)
238 self.assertEqual(inst.calls, [])
239 self.assertEqual(inst.outputs, [])
240
241 inst = drives.Mockable(mocking=True)
242 self.assertIsNone(inst.check_call(['/bin/true']))
243 self.assertEqual(inst.calls, [
244 ('check_call', ['/bin/true']),
245 ])
246 self.assertEqual(inst.outputs, [])
247 self.assertIsNone(inst.check_call(['/bin/false']))
248 self.assertEqual(inst.calls, [
249 ('check_call', ['/bin/true']),
250 ('check_call', ['/bin/false']),
251 ])
252 self.assertEqual(inst.outputs, [])
253
254 def test_check_output(self):
255 inst = drives.Mockable()
256 self.assertEqual(inst.check_output(['/bin/echo', 'foobar']), b'foobar\n')
257 self.assertEqual(inst.calls, [])
258 self.assertEqual(inst.outputs, [])
259 with self.assertRaises(subprocess.CalledProcessError) as cm:
260 inst.check_output(['/bin/false', 'stuff'])
261 self.assertEqual(cm.exception.cmd, ['/bin/false', 'stuff'])
262 self.assertEqual(cm.exception.returncode, 1)
263 self.assertEqual(inst.calls, [])
264 self.assertEqual(inst.outputs, [])
265
266 inst.reset(mocking=True, outputs=[b'foo', b'bar'])
267 self.assertEqual(inst.check_output(['/bin/echo', 'stuff']), b'foo')
268 self.assertEqual(inst.calls, [
269 ('check_output', ['/bin/echo', 'stuff']),
270 ])
271 self.assertEqual(inst.outputs, [b'bar'])
272 self.assertEqual(inst.check_output(['/bin/false', 'stuff']), b'bar')
273 self.assertEqual(inst.calls, [
274 ('check_output', ['/bin/echo', 'stuff']),
275 ('check_output', ['/bin/false', 'stuff']),
276 ])
277 self.assertEqual(inst.outputs, [])
278
72279
73class TestDrive(TestCase):280class TestDrive(TestCase):
74 def test_init(self):281 def test_init(self):
75 for dev in ('/dev/sda', '/dev/sdz', '/dev/vda', '/dev/vdz'):282 for dev in ('/dev/sda', '/dev/sdz', '/dev/vda', '/dev/vdz'):
76 inst = drives.Drive(dev)283 inst = drives.Drive(dev)
77 self.assertIs(inst.dev, dev)284 self.assertIs(inst.dev, dev)
285 self.assertIs(inst.mocking, False)
286 inst = drives.Drive(dev, mocking=True)
287 self.assertIs(inst.dev, dev)
288 self.assertIs(inst.mocking, True)
78 with self.assertRaises(ValueError) as cm:289 with self.assertRaises(ValueError) as cm:
79 drives.Drive('/dev/sda1')290 drives.Drive('/dev/sda1')
80 self.assertEqual(str(cm.exception),291 self.assertEqual(str(cm.exception),
@@ -92,31 +303,157 @@
92 )303 )
93304
94 def test_get_partition(self):305 def test_get_partition(self):
95 inst = drives.Drive('/dev/sdb')306 for base in ('/dev/sd', '/dev/vd'):
96 part = inst.get_partition(1)307 for letter in string.ascii_lowercase:
97 self.assertIsInstance(part, drives.Partition)308 dev = base + letter
98 self.assertEqual(part.dev, '/dev/sdb1')309 for number in range(1, 10):
99 part = inst.get_partition(2)310 # mocking=True
100 self.assertIsInstance(part, drives.Partition)311 inst = drives.Drive(dev)
101 self.assertEqual(part.dev, '/dev/sdb2')312 part = inst.get_partition(number)
102313 self.assertIsInstance(part, drives.Partition)
103 inst = drives.Drive('/dev/vdz')314 self.assertEqual(part.dev, '{}{:d}'.format(dev, number))
104 part = inst.get_partition(8)315 self.assertIs(part.mocking, False)
105 self.assertIsInstance(part, drives.Partition)316 # mocking=False
106 self.assertEqual(part.dev, '/dev/vdz8')317 inst = drives.Drive(dev, mocking=True)
107 part = inst.get_partition(9)318 part = inst.get_partition(number)
108 self.assertIsInstance(part, drives.Partition)319 self.assertIsInstance(part, drives.Partition)
109 self.assertEqual(part.dev, '/dev/vdz9')320 self.assertEqual(part.dev, '{}{:d}'.format(dev, number))
321 self.assertIs(part.mocking, True)
322
323 def test_rereadpt(self):
324 dev = random_drive_dev()
325 inst = drives.Drive(dev, mocking=True)
326 self.assertIsNone(inst.rereadpt())
327 self.assertEqual(inst.calls, [
328 ('check_call', ['blockdev', '--rereadpt', dev]),
329 ])
330
331 def test_zero(self):
332 dev = random_drive_dev()
333 inst = drives.Drive(dev, mocking=True)
334 self.assertIsNone(inst.zero())
335 self.assertEqual(inst.calls, [
336 ('check_call', ['dd', 'if=/dev/zero', 'of={}'.format(dev), 'bs=4M', 'count=1', 'oflag=sync']),
337 ])
338
339 def test_parted(self):
340 dev = random_drive_dev()
341 inst = drives.Drive(dev, mocking=True)
342 self.assertEqual(inst.parted(),
343 ['parted', '-s', dev, 'unit', 'MiB']
344 )
345 self.assertEqual(inst.calls, [])
346 self.assertEqual(inst.parted('print'),
347 ['parted', '-s', dev, 'unit', 'MiB', 'print']
348 )
349 self.assertEqual(inst.calls, [])
350 self.assertEqual(inst.parted('mklabel', 'gpt'),
351 ['parted', '-s', dev, 'unit', 'MiB', 'mklabel', 'gpt']
352 )
353 self.assertEqual(inst.calls, [])
354
355 def test_mklabel(self):
356 dev = random_drive_dev()
357 inst = drives.Drive(dev, mocking=True)
358 self.assertIsNone(inst.mklabel())
359 self.assertEqual(inst.calls, [
360 ('check_call', ['parted', '-s', dev, 'unit', 'MiB', 'mklabel', 'gpt']),
361 ])
362
363 def test_print(self):
364 dev = random_drive_dev()
365 inst = drives.Drive(dev)
366 marker = random_id()
367 inst.reset(mocking=True, outputs=[marker.encode('utf-8')])
368 self.assertEqual(inst.print(), marker)
369 self.assertEqual(inst.calls, [
370 ('check_output', ['parted', '-s', dev, 'unit', 'MiB', 'print']),
371 ])
372 self.assertEqual(inst.outputs, [])
373
374 def test_init_partition_table(self):
375 dev = random_drive_dev()
376 inst = drives.Drive(dev)
377 inst.reset(mocking=True, outputs=[PARTED_PRINT.encode('utf-8')])
378 self.assertIsNone(inst.init_partition_table())
379 self.assertEqual(inst.calls, [
380 ('check_call', ['blockdev', '--rereadpt', dev]),
381 ('check_call', ['dd', 'if=/dev/zero', 'of={}'.format(dev), 'bs=4M', 'count=1', 'oflag=sync']),
382 ('check_call', ['blockdev', '--rereadpt', dev]),
383 ('check_call', ['parted', '-s', dev, 'unit', 'MiB', 'mklabel', 'gpt']),
384 ('check_output', ['parted', '-s', dev, 'unit', 'MiB', 'print']),
385 ])
386 self.assertEqual(inst.outputs, [])
387 self.assertEqual(inst.size, 2861588)
388 self.assertEqual(inst.index, 0)
389 self.assertEqual(inst.start, 1)
390 self.assertEqual(inst.stop, 2861587)
391
392 def test_mkpart(self):
393 dev = random_drive_dev()
394 inst = drives.Drive(dev, mocking=True)
395 inst.start = 1
396 inst.stop = 2861587
397 self.assertIsNone(inst.mkpart(1, 2861587))
398 self.assertEqual(inst.calls, [
399 ('check_call', ['parted', '-s', dev, 'unit', 'MiB', 'mkpart', 'primary', 'ext2', '1', '2861587']),
400 ])
401
402 def test_remaining(self):
403 dev = random_drive_dev()
404 inst = drives.Drive(dev, mocking=True)
405 inst.start = 1
406 inst.stop = 2861587
407 self.assertEqual(inst.remaining, 2861586)
408 inst.start = 12345
409 self.assertEqual(inst.remaining, 2849242)
410 inst.stop = 1861587
411 self.assertEqual(inst.remaining, 1849242)
412 self.assertEqual(inst.calls, [])
413
414 def test_add_partition(self):
415 dev = random_drive_dev()
416 inst = drives.Drive(dev, mocking=True)
417 inst.index = 0
418 inst.start = 1
419 inst.stop = 2861587
420
421 part = inst.add_partition(123456)
422 self.assertIsInstance(part, drives.Partition)
423 self.assertIs(part.mocking, True)
424 self.assertEqual(part.dev, '{}{:d}'.format(dev, 1))
425 self.assertEqual(part.calls, [])
426 self.assertEqual(inst.index, 1)
427 self.assertEqual(inst.calls, [
428 ('check_call', ['parted', '-s', dev, 'unit', 'MiB', 'mkpart', 'primary', 'ext2', '1', '123457']),
429 ])
430 self.assertEqual(inst.remaining, 2738130)
431
432 part = inst.add_partition(2738130)
433 self.assertIsInstance(part, drives.Partition)
434 self.assertIs(part.mocking, True)
435 self.assertEqual(part.dev, '{}{:d}'.format(dev, 2))
436 self.assertEqual(part.calls, [])
437 self.assertEqual(inst.index, 2)
438 self.assertEqual(inst.calls, [
439 ('check_call', ['parted', '-s', dev, 'unit', 'MiB', 'mkpart', 'primary', 'ext2', '1', '123457']),
440 ('check_call', ['parted', '-s', dev, 'unit', 'MiB', 'mkpart', 'primary', 'ext2', '123457', '2861587']),
441 ])
442 self.assertEqual(inst.remaining, 0)
110443
111444
112class TestPartition(TestCase):445class TestPartition(TestCase):
113 def test_init(self):446 def test_init(self):
114 for dev in ('/dev/sda1', '/dev/sda9', '/dev/sdz1', '/dev/sdz9'):447 for base in ('/dev/sd', '/dev/vd'):
115 part = drives.Partition(dev)448 for letter in string.ascii_lowercase:
116 self.assertIs(part.dev, dev)449 for number in range(1, 10):
117 for dev in ('/dev/vda1', '/dev/vda9', '/dev/vdz1', '/dev/vdz9'):450 dev = '{}{}{:d}'.format(base, letter, number)
118 part = drives.Partition(dev)451 inst = drives.Partition(dev)
119 self.assertIs(part.dev, dev)452 self.assertIs(inst.dev, dev)
453 self.assertIs(inst.mocking, False)
454 inst = drives.Partition(dev, mocking=True)
455 self.assertIs(inst.dev, dev)
456 self.assertIs(inst.mocking, True)
120457
121 with self.assertRaises(ValueError) as cm:458 with self.assertRaises(ValueError) as cm:
122 drives.Partition('/dev/sda11')459 drives.Partition('/dev/sda11')
@@ -130,3 +467,148 @@
130 "Invalid partition device file: '/dev/sda0'"467 "Invalid partition device file: '/dev/sda0'"
131 )468 )
132469
470 def test_mkfs_ext4(self):
471 dev = random_partition_dev()
472 inst = drives.Partition(dev, mocking=True)
473 label = random_id(5)
474 store_id = random_id()
475 ext4_uuid = drives.db32_to_uuid(store_id)
476 self.assertIsNone(inst.mkfs_ext4(label, store_id))
477 self.assertEqual(inst.calls, [
478 ('check_call', ['mkfs.ext4', dev, '-L', label, '-U', ext4_uuid, '-m', '0']),
479 ])
480
481 def test_create_filestore(self):
482 dev = random_partition_dev()
483 inst = drives.Partition(dev, mocking=True)
484 tmp = TempDir()
485 store_id = random_id()
486 ext4_uuid = drives.db32_to_uuid(store_id)
487 label = random_id(5)
488 serial = random_id(10)
489 kw = {
490 'drive_serial': serial,
491 'filesystem_type': 'ext4',
492 'filesystem_uuid': ext4_uuid,
493 'filesystem_label': label,
494 }
495 doc = inst.create_filestore(tmp.dir, store_id, 1, **kw)
496 self.assertIsInstance(doc, dict)
497 self.assertEqual(set(doc), set([
498 '_id',
499 'time',
500 'type',
501 'plugin',
502 'copies',
503 'drive_serial',
504 'filesystem_type',
505 'filesystem_uuid',
506 'filesystem_label',
507 ]))
508 self.assertEqual(doc, {
509 '_id': store_id,
510 'time': doc['time'],
511 'type': 'dmedia/store',
512 'plugin': 'filestore',
513 'copies': 1,
514 'drive_serial': serial,
515 'filesystem_type': 'ext4',
516 'filesystem_uuid': ext4_uuid,
517 'filesystem_label': label,
518 })
519 fs = FileStore(tmp.dir, store_id)
520 self.assertEqual(fs.doc, doc)
521 del fs
522 self.assertEqual(inst.calls, [
523 ('check_call', ['mount', dev, tmp.dir]),
524 ('check_call', ['chmod', '0777', tmp.dir]),
525 ('check_call', ['umount', dev]),
526 ])
527
528
529class TestDeviceNotFound(TestCase):
530 def test_init(self):
531 dev = '/dev/{}'.format(random_id())
532 inst = drives.DeviceNotFound(dev)
533 self.assertIs(inst.dev, dev)
534 self.assertEqual(str(inst), 'No such device: {!r}'.format(dev))
535
536
537class TestDevices(TestCase):
538 def test_init(self):
539 inst = drives.Devices()
540 self.assertIsInstance(inst.udev_client, GUdev.Client)
541
542 marker = random_id()
543
544 class Subclass(drives.Devices):
545 def get_udev_client(self):
546 return marker
547
548 inst = Subclass()
549 self.assertIs(inst.udev_client, marker)
550
551 def test_get_device(self):
552 inst = drives.Devices()
553 dev = '/dev/nopenopenope'
554 with self.assertRaises(drives.DeviceNotFound) as cm:
555 inst.get_device(dev)
556 self.assertIs(cm.exception.dev, dev)
557 self.assertEqual(str(cm.exception),
558 "No such device: '/dev/nopenopenope'"
559 )
560
561 def test_get_drive_info(self):
562 inst = drives.Devices()
563 for device in inst.iter_drives():
564 dev = device.get_device_file()
565 self.assertEqual(
566 inst.get_drive_info(dev),
567 drives.get_drive_info(device),
568 )
569 with self.assertRaises(ValueError) as cm:
570 inst.get_drive_info('/dev/sdaa')
571 self.assertEqual(str(cm.exception),
572 "Invalid drive device file: '/dev/sdaa'"
573 )
574
575 def test_get_partition_info(self):
576 inst = drives.Devices()
577 for device in inst.iter_partitions():
578 dev = device.get_device_file()
579 self.assertEqual(
580 inst.get_partition_info(dev),
581 drives.get_partition_info(device),
582 )
583 with self.assertRaises(ValueError) as cm:
584 inst.get_partition_info('/dev/sda11')
585 self.assertEqual(str(cm.exception),
586 "Invalid partition device file: '/dev/sda11'"
587 )
588
589 def test_iter_drives(self):
590 inst = drives.Devices()
591 for drive in inst.iter_drives():
592 self.assertIsInstance(drive, GUdev.Device)
593 self.assertEqual(drive.get_devtype(), 'disk')
594 self.assertTrue(drives.VALID_DRIVE.match(drive.get_device_file()))
595
596 def test_iter_partitions(self):
597 inst = drives.Devices()
598 for drive in inst.iter_partitions():
599 self.assertIsInstance(drive, GUdev.Device)
600 self.assertEqual(drive.get_devtype(), 'partition')
601 self.assertTrue(drives.VALID_PARTITION.match(drive.get_device_file()))
602
603 def test_get_parentdir_info(self):
604 inst = drives.Devices()
605 info = inst.get_parentdir_info('/')
606 self.assertIsInstance(info, dict)
607 if info != {}:
608 self.assertEqual(set(info), set(EXPECTED_PARTITION_KEYS))
609
610 def test_get_info(self):
611 inst = drives.Devices()
612 info = inst.get_info()
613 self.assertIsInstance(info, dict)
614 self.assertEqual(set(info), set(['drives', 'partitions']))

Subscribers

People subscribed via source and target branches