blob: 90fe80a6c83a9c47d15f27d88299996660ca8246 [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
Sergei Datsenkof97bc2a2020-09-21 23:50:59 +1000388 def add_loopback_to_allowlist(self, path):
389 """Adds a device by its path to the allowlist for testing.
390
391 Args:
392 path: path to the /dev/loopX device.
393 """
394 sys_path = '/sys/devices/virtual/block/' + os.path.basename(path)
395 self.interface.AddDeviceToAllowlist(sys_path)
396
397 def remove_loopback_from_allowlist(self, path):
398 """Removes a device by its sys path from the allowlist for testing.
399
400 Args:
401 path: path to the /dev/loopX device.
402 """
403 sys_path = '/sys/devices/virtual/block/' + os.path.basename(path)
404 self.interface.RemoveDeviceFromAllowlist(sys_path)
405
Ben Chan0499e532011-08-29 10:53:18 -0700406
407class CrosDisksTester(GenericTesterMainLoop):
408 """A base tester class for testing the CrosDisks server.
409
410 A derived class should override the get_tests method to return a list of
411 test methods. The perform_one_test method invokes each test method in the
412 list to verify some functionalities of CrosDisks server.
413 """
414 def __init__(self, test):
415 bus_loop = DBusGMainLoop(set_as_default=True)
Anand K Mistry33812262019-01-08 15:38:21 +1100416 self.bus = dbus.SystemBus(mainloop=bus_loop)
Ben Chan0499e532011-08-29 10:53:18 -0700417 self.main_loop = gobject.MainLoop()
418 super(CrosDisksTester, self).__init__(test, self.main_loop)
Anand K Mistry33812262019-01-08 15:38:21 +1100419 self.cros_disks = CrosDisksClient(self.main_loop, self.bus)
Ben Chan0499e532011-08-29 10:53:18 -0700420
421 def get_tests(self):
422 """Returns a list of test methods to be invoked by perform_one_test.
423
424 A derived class should override this method.
425
426 Returns:
427 A list of test methods.
428 """
429 return []
430
431 @ExceptionForward
432 def perform_one_test(self):
433 """Exercises each test method in the list returned by get_tests.
434 """
435 tests = self.get_tests()
436 self.remaining_requirements = set([test.func_name for test in tests])
437 for test in tests:
438 test()
439 self.requirement_completed(test.func_name)
440
Anand K Mistry33812262019-01-08 15:38:21 +1100441 def reconnect_client(self, timeout_seconds=None):
François Degros2cf82be2020-09-07 17:29:32 +1000442 """"Reconnect the CrosDisks DBus client.
Anand K Mistry33812262019-01-08 15:38:21 +1100443
François Degros2cf82be2020-09-07 17:29:32 +1000444 Args:
445 timeout_seconds: Maximum time in seconds to wait for the DBus
446 connection.
447 """
448 self.cros_disks = CrosDisksClient(self.main_loop, self.bus,
449 timeout_seconds)
Anand K Mistry33812262019-01-08 15:38:21 +1100450
Ben Chan0499e532011-08-29 10:53:18 -0700451
452class FilesystemTestObject(object):
453 """A base class to represent a filesystem test object.
454
455 A filesystem test object can be a file, directory or symbolic link.
456 A derived class should override the _create and _verify method to implement
457 how the test object should be created and verified, respectively, on a
458 filesystem.
459 """
460 def __init__(self, path, content, mode):
461 """Initializes the instance.
462
463 Args:
464 path: The relative path of the test object.
465 content: The content of the test object.
466 mode: The file permissions given to the test object.
467 """
468 self._path = path
469 self._content = content
470 self._mode = mode
471
472 def create(self, base_dir):
473 """Creates the test object in a base directory.
474
475 Args:
476 base_dir: The base directory where the test object is created.
477
478 Returns:
479 True if the test object is created successfully or False otherwise.
480 """
481 if not self._create(base_dir):
482 logging.debug('Failed to create filesystem test object at "%s"',
483 os.path.join(base_dir, self._path))
484 return False
485 return True
486
487 def verify(self, base_dir):
488 """Verifies the test object in a base directory.
489
490 Args:
491 base_dir: The base directory where the test object is expected to be
492 found.
493
494 Returns:
495 True if the test object is found in the base directory and matches
496 the expected content, or False otherwise.
497 """
498 if not self._verify(base_dir):
François Degros2cf82be2020-09-07 17:29:32 +1000499 logging.error('Mismatched filesystem object at "%s"',
Ben Chan0499e532011-08-29 10:53:18 -0700500 os.path.join(base_dir, self._path))
501 return False
502 return True
503
504 def _create(self, base_dir):
505 return False
506
507 def _verify(self, base_dir):
508 return False
509
510
511class FilesystemTestDirectory(FilesystemTestObject):
512 """A filesystem test object that represents a directory."""
513
514 def __init__(self, path, content, mode=stat.S_IRWXU|stat.S_IRGRP| \
François Degros2cf82be2020-09-07 17:29:32 +1000515 stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH, strict=False):
516 """Initializes the directory.
517
518 Args:
519 path: The name of this directory.
520 content: The list of items in this directory.
521 mode: The file permissions given to this directory.
522 strict: Whether verify() strictly compares directory contents for
523 equality. This flag only applies to this directory, and not
524 to any child directories.
525 """
Ben Chan0499e532011-08-29 10:53:18 -0700526 super(FilesystemTestDirectory, self).__init__(path, content, mode)
François Degros2cf82be2020-09-07 17:29:32 +1000527 self._strict = strict
Ben Chan0499e532011-08-29 10:53:18 -0700528
529 def _create(self, base_dir):
530 path = os.path.join(base_dir, self._path) if self._path else base_dir
531
532 if self._path:
533 with ExceptionSuppressor(OSError):
534 os.makedirs(path)
535 os.chmod(path, self._mode)
536
537 if not os.path.isdir(path):
538 return False
539
540 for content in self._content:
541 if not content.create(path):
542 return False
François Degros2cf82be2020-09-07 17:29:32 +1000543
Ben Chan0499e532011-08-29 10:53:18 -0700544 return True
545
546 def _verify(self, base_dir):
547 path = os.path.join(base_dir, self._path) if self._path else base_dir
548 if not os.path.isdir(path):
549 return False
550
François Degros2cf82be2020-09-07 17:29:32 +1000551 seen = set()
Ben Chan0499e532011-08-29 10:53:18 -0700552 for content in self._content:
553 if not content.verify(path):
554 return False
François Degros2cf82be2020-09-07 17:29:32 +1000555 seen.add(content._path)
556
557 if self._strict:
558 for child in os.listdir(path):
559 if child not in seen:
560 logging.error('Unexpected filesystem entry "%s"',
561 os.path.join(path, child))
562 return False
563
Ben Chan0499e532011-08-29 10:53:18 -0700564 return True
565
566
567class FilesystemTestFile(FilesystemTestObject):
568 """A filesystem test object that represents a file."""
569
570 def __init__(self, path, content, mode=stat.S_IRUSR|stat.S_IWUSR| \
571 stat.S_IRGRP|stat.S_IROTH):
572 super(FilesystemTestFile, self).__init__(path, content, mode)
573
574 def _create(self, base_dir):
575 path = os.path.join(base_dir, self._path)
576 with ExceptionSuppressor(IOError):
577 with open(path, 'wb+') as f:
578 f.write(self._content)
Ben Chanf6a74c52013-02-19 14:06:17 -0800579 with ExceptionSuppressor(OSError):
580 os.chmod(path, self._mode)
Ben Chan0499e532011-08-29 10:53:18 -0700581 return True
582 return False
583
584 def _verify(self, base_dir):
585 path = os.path.join(base_dir, self._path)
586 with ExceptionSuppressor(IOError):
587 with open(path, 'rb') as f:
588 return f.read() == self._content
589 return False
590
591
592class DefaultFilesystemTestContent(FilesystemTestDirectory):
593 def __init__(self):
594 super(DefaultFilesystemTestContent, self).__init__('', [
595 FilesystemTestFile('file1', '0123456789'),
596 FilesystemTestDirectory('dir1', [
597 FilesystemTestFile('file1', ''),
598 FilesystemTestFile('file2', 'abcdefg'),
599 FilesystemTestDirectory('dir2', [
600 FilesystemTestFile('file3', 'abcdefg'),
Anand K Mistrye00f8a42018-08-21 11:09:54 +1000601 FilesystemTestFile('file4', 'a' * 65536),
Ben Chan0499e532011-08-29 10:53:18 -0700602 ]),
603 ]),
604 ], stat.S_IRWXU|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH)
605
606
607class VirtualFilesystemImage(object):
608 def __init__(self, block_size, block_count, filesystem_type,
609 *args, **kwargs):
610 """Initializes the instance.
611
612 Args:
613 block_size: The number of bytes of each block in the image.
614 block_count: The number of blocks in the image.
615 filesystem_type: The filesystem type to be given to the mkfs
616 program for formatting the image.
617
618 Keyword Args:
619 mount_filesystem_type: The filesystem type to be given to the
620 mount program for mounting the image.
621 mkfs_options: A list of options to be given to the mkfs program.
622 """
623 self._block_size = block_size
624 self._block_count = block_count
625 self._filesystem_type = filesystem_type
626 self._mount_filesystem_type = kwargs.get('mount_filesystem_type')
627 if self._mount_filesystem_type is None:
628 self._mount_filesystem_type = filesystem_type
629 self._mkfs_options = kwargs.get('mkfs_options')
630 if self._mkfs_options is None:
631 self._mkfs_options = []
632 self._image_file = None
633 self._loop_device = None
Ben Chan33b5f042017-08-21 13:45:30 -0700634 self._loop_device_stat = None
Ben Chan0499e532011-08-29 10:53:18 -0700635 self._mount_dir = None
636
637 def __del__(self):
638 with ExceptionSuppressor(Exception):
639 self.clean()
640
641 def __enter__(self):
642 self.create()
643 return self
644
645 def __exit__(self, exc_type, exc_value, traceback):
646 self.clean()
647 return False
648
649 def _remove_temp_path(self, temp_path):
650 """Removes a temporary file or directory created using autotemp."""
651 if temp_path:
652 with ExceptionSuppressor(Exception):
653 path = temp_path.name
654 temp_path.clean()
655 logging.debug('Removed "%s"', path)
656
657 def _remove_image_file(self):
658 """Removes the image file if one has been created."""
659 self._remove_temp_path(self._image_file)
660 self._image_file = None
661
662 def _remove_mount_dir(self):
663 """Removes the mount directory if one has been created."""
664 self._remove_temp_path(self._mount_dir)
665 self._mount_dir = None
666
667 @property
668 def image_file(self):
669 """Gets the path of the image file.
670
671 Returns:
672 The path of the image file or None if no image file has been
673 created.
674 """
675 return self._image_file.name if self._image_file else None
676
677 @property
678 def loop_device(self):
679 """Gets the loop device where the image file is attached to.
680
681 Returns:
682 The path of the loop device where the image file is attached to or
683 None if no loop device is attaching the image file.
684 """
685 return self._loop_device
686
687 @property
688 def mount_dir(self):
689 """Gets the directory where the image file is mounted to.
690
691 Returns:
692 The directory where the image file is mounted to or None if no
693 mount directory has been created.
694 """
695 return self._mount_dir.name if self._mount_dir else None
696
697 def create(self):
698 """Creates a zero-filled image file with the specified size.
699
700 The created image file is temporary and removed when clean()
701 is called.
702 """
703 self.clean()
704 self._image_file = autotemp.tempfile(unique_id='fsImage')
705 try:
706 logging.debug('Creating zero-filled image file at "%s"',
707 self._image_file.name)
708 utils.run('dd if=/dev/zero of=%s bs=%s count=%s' %
709 (self._image_file.name, self._block_size,
710 self._block_count))
711 except error.CmdError as exc:
712 self._remove_image_file()
713 message = 'Failed to create filesystem image: %s' % exc
714 raise RuntimeError(message)
715
716 def clean(self):
717 """Removes the image file if one has been created.
718
719 Before removal, the image file is detached from the loop device that
720 it is attached to.
721 """
722 self.detach_from_loop_device()
723 self._remove_image_file()
724
725 def attach_to_loop_device(self):
726 """Attaches the created image file to a loop device.
727
728 Creates the image file, if one has not been created, by calling
729 create().
730
731 Returns:
732 The path of the loop device where the image file is attached to.
733 """
734 if self._loop_device:
735 return self._loop_device
736
737 if not self._image_file:
738 self.create()
739
740 logging.debug('Attaching image file "%s" to loop device',
741 self._image_file.name)
742 utils.run('losetup -f %s' % self._image_file.name)
743 output = utils.system_output('losetup -j %s' % self._image_file.name)
744 # output should look like: "/dev/loop0: [000d]:6329 (/tmp/test.img)"
745 self._loop_device = output.split(':')[0]
746 logging.debug('Attached image file "%s" to loop device "%s"',
747 self._image_file.name, self._loop_device)
Ben Chan33b5f042017-08-21 13:45:30 -0700748
749 self._loop_device_stat = os.stat(self._loop_device)
750 logging.debug('Loop device "%s" (uid=%d, gid=%d, permissions=%04o)',
751 self._loop_device,
752 self._loop_device_stat.st_uid,
753 self._loop_device_stat.st_gid,
754 stat.S_IMODE(self._loop_device_stat.st_mode))
Ben Chan0499e532011-08-29 10:53:18 -0700755 return self._loop_device
756
757 def detach_from_loop_device(self):
758 """Detaches the image file from the loop device."""
759 if not self._loop_device:
760 return
761
762 self.unmount()
763
764 logging.debug('Cleaning up remaining mount points of loop device "%s"',
765 self._loop_device)
766 utils.run('umount -f %s' % self._loop_device, ignore_status=True)
767
Ben Chan33b5f042017-08-21 13:45:30 -0700768 logging.debug('Restore ownership/permissions of loop device "%s"',
769 self._loop_device)
770 os.chmod(self._loop_device,
771 stat.S_IMODE(self._loop_device_stat.st_mode))
772 os.chown(self._loop_device,
773 self._loop_device_stat.st_uid, self._loop_device_stat.st_gid)
774
Ben Chan0499e532011-08-29 10:53:18 -0700775 logging.debug('Detaching image file "%s" from loop device "%s"',
776 self._image_file.name, self._loop_device)
777 utils.run('losetup -d %s' % self._loop_device)
778 self._loop_device = None
779
780 def format(self):
781 """Formats the image file as the specified filesystem."""
782 self.attach_to_loop_device()
783 try:
784 logging.debug('Formatting image file at "%s" as "%s" filesystem',
785 self._image_file.name, self._filesystem_type)
786 utils.run('yes | mkfs -t %s %s %s' %
787 (self._filesystem_type, ' '.join(self._mkfs_options),
788 self._loop_device))
789 logging.debug('blkid: %s', utils.system_output(
790 'blkid -c /dev/null %s' % self._loop_device,
791 ignore_status=True))
792 except error.CmdError as exc:
793 message = 'Failed to format filesystem image: %s' % exc
794 raise RuntimeError(message)
795
796 def mount(self, options=None):
797 """Mounts the image file to a directory.
798
799 Args:
800 options: An optional list of mount options.
801 """
802 if self._mount_dir:
803 return self._mount_dir.name
804
805 if options is None:
806 options = []
807
808 options_arg = ','.join(options)
809 if options_arg:
810 options_arg = '-o ' + options_arg
811
812 self.attach_to_loop_device()
813 self._mount_dir = autotemp.tempdir(unique_id='fsImage')
814 try:
815 logging.debug('Mounting image file "%s" (%s) to directory "%s"',
816 self._image_file.name, self._loop_device,
817 self._mount_dir.name)
818 utils.run('mount -t %s %s %s %s' %
819 (self._mount_filesystem_type, options_arg,
820 self._loop_device, self._mount_dir.name))
821 except error.CmdError as exc:
822 self._remove_mount_dir()
823 message = ('Failed to mount virtual filesystem image "%s": %s' %
824 (self._image_file.name, exc))
825 raise RuntimeError(message)
826 return self._mount_dir.name
827
828 def unmount(self):
829 """Unmounts the image file from the mounted directory."""
830 if not self._mount_dir:
831 return
832
833 try:
834 logging.debug('Unmounting image file "%s" (%s) from directory "%s"',
835 self._image_file.name, self._loop_device,
836 self._mount_dir.name)
837 utils.run('umount %s' % self._mount_dir.name)
838 except error.CmdError as exc:
839 message = ('Failed to unmount virtual filesystem image "%s": %s' %
840 (self._image_file.name, exc))
841 raise RuntimeError(message)
842 finally:
843 self._remove_mount_dir()
Klemen Kozjek7740bfd2017-08-15 16:40:30 +0900844
845 def get_volume_label(self):
846 """Gets volume name information of |self._loop_device|
847
848 @return a string with volume name if it exists.
849 """
850 # This script is run as root in a normal autotest run,
851 # so this works: It doesn't have access to the necessary info
852 # when run as a non-privileged user
853 cmd = "blkid -c /dev/null -o udev %s" % self._loop_device
854 output = utils.system_output(cmd, ignore_status=True)
855
856 for line in output.splitlines():
857 udev_key, udev_val = line.split('=')
858
859 if udev_key == 'ID_FS_LABEL':
860 return udev_val
861
862 return None