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