blob: 1f376698b268ff15de7f8e20fcd7242e5747138d [file] [log] [blame]
barfab@chromium.orgb6d29932012-04-11 09:46:43 +02001# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Ben Chan0499e532011-08-29 10:53:18 -07002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
barfab@chromium.orgb6d29932012-04-11 09:46:43 +02005import dbus, gobject, logging, os, stat
6from dbus.mainloop.glib import DBusGMainLoop
Ben Chan0499e532011-08-29 10:53:18 -07007
barfab@chromium.orgb6d29932012-04-11 09:46:43 +02008import common
Ben Chan0499e532011-08-29 10:53:18 -07009from autotest_lib.client.bin import utils
10from autotest_lib.client.common_lib import autotemp, error
Lutz Justen1c6be452018-05-29 13:37:00 +020011from autotest_lib.client.cros import dbus_util
barfab@chromium.orgb6d29932012-04-11 09:46:43 +020012from mainloop import ExceptionForward
13from mainloop import GenericTesterMainLoop
Ben Chan0499e532011-08-29 10:53:18 -070014
15
16"""This module contains several helper classes for writing tests to verify the
17CrosDisks DBus interface. In particular, the CrosDisksTester class can be used
18to derive functional tests that interact with the CrosDisks server over DBus.
19"""
20
21
22class ExceptionSuppressor(object):
23 """A context manager class for suppressing certain types of exception.
24
25 An instance of this class is expected to be used with the with statement
26 and takes a set of exception classes at instantiation, which are types of
27 exception to be suppressed (and logged) in the code block under the with
28 statement.
29
30 Example:
31
32 with ExceptionSuppressor(OSError, IOError):
33 # An exception, which is a sub-class of OSError or IOError, is
34 # suppressed in the block code under the with statement.
35 """
36 def __init__(self, *args):
37 self.__suppressed_exc_types = (args)
38
39 def __enter__(self):
40 return self
41
42 def __exit__(self, exc_type, exc_value, traceback):
43 if exc_type and issubclass(exc_type, self.__suppressed_exc_types):
44 try:
45 logging.exception('Suppressed exception: %s(%s)',
46 exc_type, exc_value)
47 except Exception:
48 pass
49 return True
50 return False
51
52
53class DBusClient(object):
54 """ A base class of a DBus proxy client to test a DBus server.
55
56 This class is expected to be used along with a GLib main loop and provides
57 some convenient functions for testing the DBus API exposed by a DBus server.
58 """
Lutz Justen1c6be452018-05-29 13:37:00 +020059
60 def __init__(self, main_loop, bus, bus_name, object_path, timeout=None):
Ben Chan0499e532011-08-29 10:53:18 -070061 """Initializes the instance.
62
63 Args:
64 main_loop: The GLib main loop.
65 bus: The bus where the DBus server is connected to.
66 bus_name: The bus name owned by the DBus server.
67 object_path: The object path of the DBus server.
Lutz Justen1c6be452018-05-29 13:37:00 +020068 timeout: Maximum time in seconds to wait for the DBus connection.
Ben Chan0499e532011-08-29 10:53:18 -070069 """
70 self.__signal_content = {}
71 self.main_loop = main_loop
72 self.signal_timeout_in_seconds = 10
73 logging.debug('Getting D-Bus proxy object on bus "%s" and path "%s"',
74 bus_name, object_path)
Lutz Justen1c6be452018-05-29 13:37:00 +020075 self.proxy_object = dbus_util.get_dbus_object(bus, bus_name,
76 object_path, timeout)
Ben Chan0499e532011-08-29 10:53:18 -070077
78 def clear_signal_content(self, signal_name):
79 """Clears the content of the signal.
80
81 Args:
82 signal_name: The name of the signal.
83 """
84 if signal_name in self.__signal_content:
85 self.__signal_content[signal_name] = None
86
87 def get_signal_content(self, signal_name):
88 """Gets the content of a signal.
89
90 Args:
91 signal_name: The name of the signal.
92
93 Returns:
94 The content of a signal or None if the signal is not being handled.
95 """
96 return self.__signal_content.get(signal_name)
97
98 def handle_signal(self, interface, signal_name, argument_names=()):
99 """Registers a signal handler to handle a given signal.
100
101 Args:
102 interface: The DBus interface of the signal.
103 signal_name: The name of the signal.
104 argument_names: A list of argument names that the signal contains.
105 """
106 if signal_name in self.__signal_content:
107 return
108
109 self.__signal_content[signal_name] = None
110
111 def signal_handler(*args):
112 self.__signal_content[signal_name] = dict(zip(argument_names, args))
113
114 logging.debug('Handling D-Bus signal "%s(%s)" on interface "%s"',
115 signal_name, ', '.join(argument_names), interface)
116 self.proxy_object.connect_to_signal(signal_name, signal_handler,
117 interface)
118
119 def wait_for_signal(self, signal_name):
Ben Chan81904f12011-11-21 17:20:18 -0800120 """Waits for the reception of a signal.
121
122 Args:
123 signal_name: The name of the signal to wait for.
Ben Chan0499e532011-08-29 10:53:18 -0700124
125 Returns:
126 The content of the signal.
127 """
128 if signal_name not in self.__signal_content:
129 return None
130
131 def check_signal_content():
132 context = self.main_loop.get_context()
133 while context.iteration(False):
134 pass
135 return self.__signal_content[signal_name] is not None
136
137 logging.debug('Waiting for D-Bus signal "%s"', signal_name)
138 utils.poll_for_condition(condition=check_signal_content,
139 desc='%s signal' % signal_name,
140 timeout=self.signal_timeout_in_seconds)
141 content = self.__signal_content[signal_name]
142 logging.debug('Received D-Bus signal "%s(%s)"', signal_name, content)
143 self.__signal_content[signal_name] = None
144 return content
145
Ben Chan81904f12011-11-21 17:20:18 -0800146 def expect_signal(self, signal_name, expected_content):
147 """Waits the the reception of a signal and verifies its content.
148
149 Args:
150 signal_name: The name of the signal to wait for.
151 expected_content: The expected content of the signal, which can be
152 partially specified. Only specified fields are
153 compared between the actual and expected content.
154
155 Returns:
156 The actual content of the signal.
157
158 Raises:
159 error.TestFail: A test failure when there is a mismatch between the
160 actual and expected content of the signal.
161 """
162 actual_content = self.wait_for_signal(signal_name)
163 logging.debug("%s signal: expected=%s actual=%s",
164 signal_name, expected_content, actual_content)
165 for argument, expected_value in expected_content.iteritems():
166 if argument not in actual_content:
167 raise error.TestFail(
168 ('%s signal missing "%s": expected=%s, actual=%s') %
169 (signal_name, argument, expected_content, actual_content))
170
171 if actual_content[argument] != expected_value:
172 raise error.TestFail(
173 ('%s signal not matched on "%s": expected=%s, actual=%s') %
174 (signal_name, argument, expected_content, actual_content))
175 return actual_content
176
Ben Chan0499e532011-08-29 10:53:18 -0700177
178class CrosDisksClient(DBusClient):
179 """A DBus proxy client for testing the CrosDisks DBus server.
180 """
181
182 CROS_DISKS_BUS_NAME = 'org.chromium.CrosDisks'
183 CROS_DISKS_INTERFACE = 'org.chromium.CrosDisks'
184 CROS_DISKS_OBJECT_PATH = '/org/chromium/CrosDisks'
185 DBUS_PROPERTIES_INTERFACE = 'org.freedesktop.DBus.Properties'
Ben Chan81904f12011-11-21 17:20:18 -0800186 FORMAT_COMPLETED_SIGNAL = 'FormatCompleted'
187 FORMAT_COMPLETED_SIGNAL_ARGUMENTS = (
188 'status', 'path'
189 )
Ben Chan0499e532011-08-29 10:53:18 -0700190 MOUNT_COMPLETED_SIGNAL = 'MountCompleted'
191 MOUNT_COMPLETED_SIGNAL_ARGUMENTS = (
192 'status', 'source_path', 'source_type', 'mount_path'
193 )
Klemen Kozjek7740bfd2017-08-15 16:40:30 +0900194 RENAME_COMPLETED_SIGNAL = 'RenameCompleted'
195 RENAME_COMPLETED_SIGNAL_ARGUMENTS = (
196 'status', 'path'
197 )
Ben Chan0499e532011-08-29 10:53:18 -0700198
Anand K Mistry33812262019-01-08 15:38:21 +1100199 def __init__(self, main_loop, bus, timeout_seconds=None):
Ben Chan0499e532011-08-29 10:53:18 -0700200 """Initializes the instance.
201
202 Args:
203 main_loop: The GLib main loop.
204 bus: The bus where the DBus server is connected to.
Anand K Mistry33812262019-01-08 15:38:21 +1100205 timeout_seconds: Maximum time in seconds to wait for the DBus
206 connection.
Ben Chan0499e532011-08-29 10:53:18 -0700207 """
208 super(CrosDisksClient, self).__init__(main_loop, bus,
209 self.CROS_DISKS_BUS_NAME,
Anand K Mistry33812262019-01-08 15:38:21 +1100210 self.CROS_DISKS_OBJECT_PATH,
211 timeout_seconds)
Ben Chan0499e532011-08-29 10:53:18 -0700212 self.interface = dbus.Interface(self.proxy_object,
213 self.CROS_DISKS_INTERFACE)
214 self.properties = dbus.Interface(self.proxy_object,
215 self.DBUS_PROPERTIES_INTERFACE)
216 self.handle_signal(self.CROS_DISKS_INTERFACE,
Ben Chan81904f12011-11-21 17:20:18 -0800217 self.FORMAT_COMPLETED_SIGNAL,
218 self.FORMAT_COMPLETED_SIGNAL_ARGUMENTS)
219 self.handle_signal(self.CROS_DISKS_INTERFACE,
Ben Chan0499e532011-08-29 10:53:18 -0700220 self.MOUNT_COMPLETED_SIGNAL,
221 self.MOUNT_COMPLETED_SIGNAL_ARGUMENTS)
Klemen Kozjek7740bfd2017-08-15 16:40:30 +0900222 self.handle_signal(self.CROS_DISKS_INTERFACE,
223 self.RENAME_COMPLETED_SIGNAL,
224 self.RENAME_COMPLETED_SIGNAL_ARGUMENTS)
Ben Chan0499e532011-08-29 10:53:18 -0700225
Ben Chan0499e532011-08-29 10:53:18 -0700226 def enumerate_devices(self):
227 """Invokes the CrosDisks EnumerateMountableDevices method.
228
229 Returns:
230 A list of sysfs paths of devices that are recognized by
231 CrosDisks.
232 """
233 return self.interface.EnumerateDevices()
234
235 def get_device_properties(self, path):
236 """Invokes the CrosDisks GetDeviceProperties method.
237
238 Args:
239 path: The device path.
240
241 Returns:
242 The properties of the device in a dictionary.
243 """
244 return self.interface.GetDeviceProperties(path)
245
Ben Chan81904f12011-11-21 17:20:18 -0800246 def format(self, path, filesystem_type=None, options=None):
247 """Invokes the CrosDisks Format method.
248
249 Args:
250 path: The device path to format.
251 filesystem_type: The filesystem type used for formatting the device.
252 options: A list of options used for formatting the device.
253 """
254 if filesystem_type is None:
255 filesystem_type = ''
256 if options is None:
257 options = []
258 self.clear_signal_content(self.FORMAT_COMPLETED_SIGNAL)
Ben Chan577e66b2017-10-12 19:18:03 -0700259 self.interface.Format(path, filesystem_type,
260 dbus.Array(options, signature='s'))
Ben Chan81904f12011-11-21 17:20:18 -0800261
262 def wait_for_format_completion(self):
263 """Waits for the CrosDisks FormatCompleted signal.
264
265 Returns:
266 The content of the FormatCompleted signal.
267 """
268 return self.wait_for_signal(self.FORMAT_COMPLETED_SIGNAL)
269
270 def expect_format_completion(self, expected_content):
271 """Waits and verifies for the CrosDisks FormatCompleted signal.
272
273 Args:
274 expected_content: The expected content of the FormatCompleted
275 signal, which can be partially specified.
276 Only specified fields are compared between the
277 actual and expected content.
278
279 Returns:
280 The actual content of the FormatCompleted signal.
281
282 Raises:
283 error.TestFail: A test failure when there is a mismatch between the
284 actual and expected content of the FormatCompleted
285 signal.
286 """
287 return self.expect_signal(self.FORMAT_COMPLETED_SIGNAL,
288 expected_content)
289
Klemen Kozjek7740bfd2017-08-15 16:40:30 +0900290 def rename(self, path, volume_name=None):
291 """Invokes the CrosDisks Rename method.
292
293 Args:
294 path: The device path to rename.
295 volume_name: The new name used for renaming.
296 """
297 if volume_name is None:
298 volume_name = ''
299 self.clear_signal_content(self.RENAME_COMPLETED_SIGNAL)
300 self.interface.Rename(path, volume_name)
301
302 def wait_for_rename_completion(self):
303 """Waits for the CrosDisks RenameCompleted signal.
304
305 Returns:
306 The content of the RenameCompleted signal.
307 """
308 return self.wait_for_signal(self.RENAME_COMPLETED_SIGNAL)
309
310 def expect_rename_completion(self, expected_content):
311 """Waits and verifies for the CrosDisks RenameCompleted signal.
312
313 Args:
314 expected_content: The expected content of the RenameCompleted
315 signal, which can be partially specified.
316 Only specified fields are compared between the
317 actual and expected content.
318
319 Returns:
320 The actual content of the RenameCompleted signal.
321
322 Raises:
323 error.TestFail: A test failure when there is a mismatch between the
324 actual and expected content of the RenameCompleted
325 signal.
326 """
327 return self.expect_signal(self.RENAME_COMPLETED_SIGNAL,
328 expected_content)
329
Ben Chan0499e532011-08-29 10:53:18 -0700330 def mount(self, path, filesystem_type=None, options=None):
331 """Invokes the CrosDisks Mount method.
332
333 Args:
334 path: The device path to mount.
335 filesystem_type: The filesystem type used for mounting the device.
336 options: A list of options used for mounting the device.
337 """
338 if filesystem_type is None:
339 filesystem_type = ''
340 if options is None:
341 options = []
342 self.clear_signal_content(self.MOUNT_COMPLETED_SIGNAL)
Ben Chan577e66b2017-10-12 19:18:03 -0700343 self.interface.Mount(path, filesystem_type,
344 dbus.Array(options, signature='s'))
Ben Chan0499e532011-08-29 10:53:18 -0700345
346 def unmount(self, path, options=None):
347 """Invokes the CrosDisks Unmount method.
348
349 Args:
350 path: The device or mount path to unmount.
351 options: A list of options used for unmounting the path.
Anand K Mistry966caf72018-08-15 15:02:41 +1000352
353 Returns:
354 The mount error code.
Ben Chan0499e532011-08-29 10:53:18 -0700355 """
356 if options is None:
357 options = []
Anand K Mistry966caf72018-08-15 15:02:41 +1000358 return self.interface.Unmount(path, dbus.Array(options, signature='s'))
Ben Chan0499e532011-08-29 10:53:18 -0700359
360 def wait_for_mount_completion(self):
361 """Waits for the CrosDisks MountCompleted signal.
362
363 Returns:
364 The content of the MountCompleted signal.
365 """
366 return self.wait_for_signal(self.MOUNT_COMPLETED_SIGNAL)
367
368 def expect_mount_completion(self, expected_content):
369 """Waits and verifies for the CrosDisks MountCompleted signal.
370
371 Args:
372 expected_content: The expected content of the MountCompleted
373 signal, which can be partially specified.
374 Only specified fields are compared between the
375 actual and expected content.
376
377 Returns:
378 The actual content of the MountCompleted signal.
379
Ben Chan0499e532011-08-29 10:53:18 -0700380 Raises:
381 error.TestFail: A test failure when there is a mismatch between the
382 actual and expected content of the MountCompleted
383 signal.
384 """
Ben Chan81904f12011-11-21 17:20:18 -0800385 return self.expect_signal(self.MOUNT_COMPLETED_SIGNAL,
386 expected_content)
Ben Chan0499e532011-08-29 10:53:18 -0700387
388
389class CrosDisksTester(GenericTesterMainLoop):
390 """A base tester class for testing the CrosDisks server.
391
392 A derived class should override the get_tests method to return a list of
393 test methods. The perform_one_test method invokes each test method in the
394 list to verify some functionalities of CrosDisks server.
395 """
396 def __init__(self, test):
397 bus_loop = DBusGMainLoop(set_as_default=True)
Anand K Mistry33812262019-01-08 15:38:21 +1100398 self.bus = dbus.SystemBus(mainloop=bus_loop)
Ben Chan0499e532011-08-29 10:53:18 -0700399 self.main_loop = gobject.MainLoop()
400 super(CrosDisksTester, self).__init__(test, self.main_loop)
Anand K Mistry33812262019-01-08 15:38:21 +1100401 self.cros_disks = CrosDisksClient(self.main_loop, self.bus)
Ben Chan0499e532011-08-29 10:53:18 -0700402
403 def get_tests(self):
404 """Returns a list of test methods to be invoked by perform_one_test.
405
406 A derived class should override this method.
407
408 Returns:
409 A list of test methods.
410 """
411 return []
412
413 @ExceptionForward
414 def perform_one_test(self):
415 """Exercises each test method in the list returned by get_tests.
416 """
417 tests = self.get_tests()
418 self.remaining_requirements = set([test.func_name for test in tests])
419 for test in tests:
420 test()
421 self.requirement_completed(test.func_name)
422
Anand K Mistry33812262019-01-08 15:38:21 +1100423 def reconnect_client(self, timeout_seconds=None):
François Degros2cf82be2020-09-07 17:29:32 +1000424 """"Reconnect the CrosDisks DBus client.
Anand K Mistry33812262019-01-08 15:38:21 +1100425
François Degros2cf82be2020-09-07 17:29:32 +1000426 Args:
427 timeout_seconds: Maximum time in seconds to wait for the DBus
428 connection.
429 """
430 self.cros_disks = CrosDisksClient(self.main_loop, self.bus,
431 timeout_seconds)
Anand K Mistry33812262019-01-08 15:38:21 +1100432
Ben Chan0499e532011-08-29 10:53:18 -0700433
434class FilesystemTestObject(object):
435 """A base class to represent a filesystem test object.
436
437 A filesystem test object can be a file, directory or symbolic link.
438 A derived class should override the _create and _verify method to implement
439 how the test object should be created and verified, respectively, on a
440 filesystem.
441 """
442 def __init__(self, path, content, mode):
443 """Initializes the instance.
444
445 Args:
446 path: The relative path of the test object.
447 content: The content of the test object.
448 mode: The file permissions given to the test object.
449 """
450 self._path = path
451 self._content = content
452 self._mode = mode
453
454 def create(self, base_dir):
455 """Creates the test object in a base directory.
456
457 Args:
458 base_dir: The base directory where the test object is created.
459
460 Returns:
461 True if the test object is created successfully or False otherwise.
462 """
463 if not self._create(base_dir):
464 logging.debug('Failed to create filesystem test object at "%s"',
465 os.path.join(base_dir, self._path))
466 return False
467 return True
468
469 def verify(self, base_dir):
470 """Verifies the test object in a base directory.
471
472 Args:
473 base_dir: The base directory where the test object is expected to be
474 found.
475
476 Returns:
477 True if the test object is found in the base directory and matches
478 the expected content, or False otherwise.
479 """
480 if not self._verify(base_dir):
François Degros2cf82be2020-09-07 17:29:32 +1000481 logging.error('Mismatched filesystem object at "%s"',
Ben Chan0499e532011-08-29 10:53:18 -0700482 os.path.join(base_dir, self._path))
483 return False
484 return True
485
486 def _create(self, base_dir):
487 return False
488
489 def _verify(self, base_dir):
490 return False
491
492
493class FilesystemTestDirectory(FilesystemTestObject):
494 """A filesystem test object that represents a directory."""
495
496 def __init__(self, path, content, mode=stat.S_IRWXU|stat.S_IRGRP| \
François Degros2cf82be2020-09-07 17:29:32 +1000497 stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH, strict=False):
498 """Initializes the directory.
499
500 Args:
501 path: The name of this directory.
502 content: The list of items in this directory.
503 mode: The file permissions given to this directory.
504 strict: Whether verify() strictly compares directory contents for
505 equality. This flag only applies to this directory, and not
506 to any child directories.
507 """
Ben Chan0499e532011-08-29 10:53:18 -0700508 super(FilesystemTestDirectory, self).__init__(path, content, mode)
François Degros2cf82be2020-09-07 17:29:32 +1000509 self._strict = strict
Ben Chan0499e532011-08-29 10:53:18 -0700510
511 def _create(self, base_dir):
512 path = os.path.join(base_dir, self._path) if self._path else base_dir
513
514 if self._path:
515 with ExceptionSuppressor(OSError):
516 os.makedirs(path)
517 os.chmod(path, self._mode)
518
519 if not os.path.isdir(path):
520 return False
521
522 for content in self._content:
523 if not content.create(path):
524 return False
François Degros2cf82be2020-09-07 17:29:32 +1000525
Ben Chan0499e532011-08-29 10:53:18 -0700526 return True
527
528 def _verify(self, base_dir):
529 path = os.path.join(base_dir, self._path) if self._path else base_dir
530 if not os.path.isdir(path):
531 return False
532
François Degros2cf82be2020-09-07 17:29:32 +1000533 seen = set()
Ben Chan0499e532011-08-29 10:53:18 -0700534 for content in self._content:
535 if not content.verify(path):
536 return False
François Degros2cf82be2020-09-07 17:29:32 +1000537 seen.add(content._path)
538
539 if self._strict:
540 for child in os.listdir(path):
541 if child not in seen:
542 logging.error('Unexpected filesystem entry "%s"',
543 os.path.join(path, child))
544 return False
545
Ben Chan0499e532011-08-29 10:53:18 -0700546 return True
547
548
549class FilesystemTestFile(FilesystemTestObject):
550 """A filesystem test object that represents a file."""
551
552 def __init__(self, path, content, mode=stat.S_IRUSR|stat.S_IWUSR| \
553 stat.S_IRGRP|stat.S_IROTH):
554 super(FilesystemTestFile, self).__init__(path, content, mode)
555
556 def _create(self, base_dir):
557 path = os.path.join(base_dir, self._path)
558 with ExceptionSuppressor(IOError):
559 with open(path, 'wb+') as f:
560 f.write(self._content)
Ben Chanf6a74c52013-02-19 14:06:17 -0800561 with ExceptionSuppressor(OSError):
562 os.chmod(path, self._mode)
Ben Chan0499e532011-08-29 10:53:18 -0700563 return True
564 return False
565
566 def _verify(self, base_dir):
567 path = os.path.join(base_dir, self._path)
568 with ExceptionSuppressor(IOError):
569 with open(path, 'rb') as f:
570 return f.read() == self._content
571 return False
572
573
574class DefaultFilesystemTestContent(FilesystemTestDirectory):
575 def __init__(self):
576 super(DefaultFilesystemTestContent, self).__init__('', [
577 FilesystemTestFile('file1', '0123456789'),
578 FilesystemTestDirectory('dir1', [
579 FilesystemTestFile('file1', ''),
580 FilesystemTestFile('file2', 'abcdefg'),
581 FilesystemTestDirectory('dir2', [
582 FilesystemTestFile('file3', 'abcdefg'),
Anand K Mistrye00f8a42018-08-21 11:09:54 +1000583 FilesystemTestFile('file4', 'a' * 65536),
Ben Chan0499e532011-08-29 10:53:18 -0700584 ]),
585 ]),
586 ], stat.S_IRWXU|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH)
587
588
589class VirtualFilesystemImage(object):
590 def __init__(self, block_size, block_count, filesystem_type,
591 *args, **kwargs):
592 """Initializes the instance.
593
594 Args:
595 block_size: The number of bytes of each block in the image.
596 block_count: The number of blocks in the image.
597 filesystem_type: The filesystem type to be given to the mkfs
598 program for formatting the image.
599
600 Keyword Args:
601 mount_filesystem_type: The filesystem type to be given to the
602 mount program for mounting the image.
603 mkfs_options: A list of options to be given to the mkfs program.
604 """
605 self._block_size = block_size
606 self._block_count = block_count
607 self._filesystem_type = filesystem_type
608 self._mount_filesystem_type = kwargs.get('mount_filesystem_type')
609 if self._mount_filesystem_type is None:
610 self._mount_filesystem_type = filesystem_type
611 self._mkfs_options = kwargs.get('mkfs_options')
612 if self._mkfs_options is None:
613 self._mkfs_options = []
614 self._image_file = None
615 self._loop_device = None
Ben Chan33b5f042017-08-21 13:45:30 -0700616 self._loop_device_stat = None
Ben Chan0499e532011-08-29 10:53:18 -0700617 self._mount_dir = None
618
619 def __del__(self):
620 with ExceptionSuppressor(Exception):
621 self.clean()
622
623 def __enter__(self):
624 self.create()
625 return self
626
627 def __exit__(self, exc_type, exc_value, traceback):
628 self.clean()
629 return False
630
631 def _remove_temp_path(self, temp_path):
632 """Removes a temporary file or directory created using autotemp."""
633 if temp_path:
634 with ExceptionSuppressor(Exception):
635 path = temp_path.name
636 temp_path.clean()
637 logging.debug('Removed "%s"', path)
638
639 def _remove_image_file(self):
640 """Removes the image file if one has been created."""
641 self._remove_temp_path(self._image_file)
642 self._image_file = None
643
644 def _remove_mount_dir(self):
645 """Removes the mount directory if one has been created."""
646 self._remove_temp_path(self._mount_dir)
647 self._mount_dir = None
648
649 @property
650 def image_file(self):
651 """Gets the path of the image file.
652
653 Returns:
654 The path of the image file or None if no image file has been
655 created.
656 """
657 return self._image_file.name if self._image_file else None
658
659 @property
660 def loop_device(self):
661 """Gets the loop device where the image file is attached to.
662
663 Returns:
664 The path of the loop device where the image file is attached to or
665 None if no loop device is attaching the image file.
666 """
667 return self._loop_device
668
669 @property
670 def mount_dir(self):
671 """Gets the directory where the image file is mounted to.
672
673 Returns:
674 The directory where the image file is mounted to or None if no
675 mount directory has been created.
676 """
677 return self._mount_dir.name if self._mount_dir else None
678
679 def create(self):
680 """Creates a zero-filled image file with the specified size.
681
682 The created image file is temporary and removed when clean()
683 is called.
684 """
685 self.clean()
686 self._image_file = autotemp.tempfile(unique_id='fsImage')
687 try:
688 logging.debug('Creating zero-filled image file at "%s"',
689 self._image_file.name)
690 utils.run('dd if=/dev/zero of=%s bs=%s count=%s' %
691 (self._image_file.name, self._block_size,
692 self._block_count))
693 except error.CmdError as exc:
694 self._remove_image_file()
695 message = 'Failed to create filesystem image: %s' % exc
696 raise RuntimeError(message)
697
698 def clean(self):
699 """Removes the image file if one has been created.
700
701 Before removal, the image file is detached from the loop device that
702 it is attached to.
703 """
704 self.detach_from_loop_device()
705 self._remove_image_file()
706
707 def attach_to_loop_device(self):
708 """Attaches the created image file to a loop device.
709
710 Creates the image file, if one has not been created, by calling
711 create().
712
713 Returns:
714 The path of the loop device where the image file is attached to.
715 """
716 if self._loop_device:
717 return self._loop_device
718
719 if not self._image_file:
720 self.create()
721
722 logging.debug('Attaching image file "%s" to loop device',
723 self._image_file.name)
724 utils.run('losetup -f %s' % self._image_file.name)
725 output = utils.system_output('losetup -j %s' % self._image_file.name)
726 # output should look like: "/dev/loop0: [000d]:6329 (/tmp/test.img)"
727 self._loop_device = output.split(':')[0]
728 logging.debug('Attached image file "%s" to loop device "%s"',
729 self._image_file.name, self._loop_device)
Ben Chan33b5f042017-08-21 13:45:30 -0700730
731 self._loop_device_stat = os.stat(self._loop_device)
732 logging.debug('Loop device "%s" (uid=%d, gid=%d, permissions=%04o)',
733 self._loop_device,
734 self._loop_device_stat.st_uid,
735 self._loop_device_stat.st_gid,
736 stat.S_IMODE(self._loop_device_stat.st_mode))
Ben Chan0499e532011-08-29 10:53:18 -0700737 return self._loop_device
738
739 def detach_from_loop_device(self):
740 """Detaches the image file from the loop device."""
741 if not self._loop_device:
742 return
743
744 self.unmount()
745
746 logging.debug('Cleaning up remaining mount points of loop device "%s"',
747 self._loop_device)
748 utils.run('umount -f %s' % self._loop_device, ignore_status=True)
749
Ben Chan33b5f042017-08-21 13:45:30 -0700750 logging.debug('Restore ownership/permissions of loop device "%s"',
751 self._loop_device)
752 os.chmod(self._loop_device,
753 stat.S_IMODE(self._loop_device_stat.st_mode))
754 os.chown(self._loop_device,
755 self._loop_device_stat.st_uid, self._loop_device_stat.st_gid)
756
Ben Chan0499e532011-08-29 10:53:18 -0700757 logging.debug('Detaching image file "%s" from loop device "%s"',
758 self._image_file.name, self._loop_device)
759 utils.run('losetup -d %s' % self._loop_device)
760 self._loop_device = None
761
762 def format(self):
763 """Formats the image file as the specified filesystem."""
764 self.attach_to_loop_device()
765 try:
766 logging.debug('Formatting image file at "%s" as "%s" filesystem',
767 self._image_file.name, self._filesystem_type)
768 utils.run('yes | mkfs -t %s %s %s' %
769 (self._filesystem_type, ' '.join(self._mkfs_options),
770 self._loop_device))
771 logging.debug('blkid: %s', utils.system_output(
772 'blkid -c /dev/null %s' % self._loop_device,
773 ignore_status=True))
774 except error.CmdError as exc:
775 message = 'Failed to format filesystem image: %s' % exc
776 raise RuntimeError(message)
777
778 def mount(self, options=None):
779 """Mounts the image file to a directory.
780
781 Args:
782 options: An optional list of mount options.
783 """
784 if self._mount_dir:
785 return self._mount_dir.name
786
787 if options is None:
788 options = []
789
790 options_arg = ','.join(options)
791 if options_arg:
792 options_arg = '-o ' + options_arg
793
794 self.attach_to_loop_device()
795 self._mount_dir = autotemp.tempdir(unique_id='fsImage')
796 try:
797 logging.debug('Mounting image file "%s" (%s) to directory "%s"',
798 self._image_file.name, self._loop_device,
799 self._mount_dir.name)
800 utils.run('mount -t %s %s %s %s' %
801 (self._mount_filesystem_type, options_arg,
802 self._loop_device, self._mount_dir.name))
803 except error.CmdError as exc:
804 self._remove_mount_dir()
805 message = ('Failed to mount virtual filesystem image "%s": %s' %
806 (self._image_file.name, exc))
807 raise RuntimeError(message)
808 return self._mount_dir.name
809
810 def unmount(self):
811 """Unmounts the image file from the mounted directory."""
812 if not self._mount_dir:
813 return
814
815 try:
816 logging.debug('Unmounting image file "%s" (%s) from directory "%s"',
817 self._image_file.name, self._loop_device,
818 self._mount_dir.name)
819 utils.run('umount %s' % self._mount_dir.name)
820 except error.CmdError as exc:
821 message = ('Failed to unmount virtual filesystem image "%s": %s' %
822 (self._image_file.name, exc))
823 raise RuntimeError(message)
824 finally:
825 self._remove_mount_dir()
Klemen Kozjek7740bfd2017-08-15 16:40:30 +0900826
827 def get_volume_label(self):
828 """Gets volume name information of |self._loop_device|
829
830 @return a string with volume name if it exists.
831 """
832 # This script is run as root in a normal autotest run,
833 # so this works: It doesn't have access to the necessary info
834 # when run as a non-privileged user
835 cmd = "blkid -c /dev/null -o udev %s" % self._loop_device
836 output = utils.system_output(cmd, ignore_status=True)
837
838 for line in output.splitlines():
839 udev_key, udev_val = line.split('=')
840
841 if udev_key == 'ID_FS_LABEL':
842 return udev_val
843
844 return None