blob: a2a9ed4ce43835cae22f63307251f823cfb20430 [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):
424 """"Reconnect the CrosDisks DBus client.
425
426 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)
432
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):
481 logging.debug('Failed to verify filesystem test object at "%s"',
482 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| \
497 stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH):
498 super(FilesystemTestDirectory, self).__init__(path, content, mode)
499
500 def _create(self, base_dir):
501 path = os.path.join(base_dir, self._path) if self._path else base_dir
502
503 if self._path:
504 with ExceptionSuppressor(OSError):
505 os.makedirs(path)
506 os.chmod(path, self._mode)
507
508 if not os.path.isdir(path):
509 return False
510
511 for content in self._content:
512 if not content.create(path):
513 return False
514 return True
515
516 def _verify(self, base_dir):
517 path = os.path.join(base_dir, self._path) if self._path else base_dir
518 if not os.path.isdir(path):
519 return False
520
521 for content in self._content:
522 if not content.verify(path):
523 return False
524 return True
525
526
527class FilesystemTestFile(FilesystemTestObject):
528 """A filesystem test object that represents a file."""
529
530 def __init__(self, path, content, mode=stat.S_IRUSR|stat.S_IWUSR| \
531 stat.S_IRGRP|stat.S_IROTH):
532 super(FilesystemTestFile, self).__init__(path, content, mode)
533
534 def _create(self, base_dir):
535 path = os.path.join(base_dir, self._path)
536 with ExceptionSuppressor(IOError):
537 with open(path, 'wb+') as f:
538 f.write(self._content)
Ben Chanf6a74c52013-02-19 14:06:17 -0800539 with ExceptionSuppressor(OSError):
540 os.chmod(path, self._mode)
Ben Chan0499e532011-08-29 10:53:18 -0700541 return True
542 return False
543
544 def _verify(self, base_dir):
545 path = os.path.join(base_dir, self._path)
546 with ExceptionSuppressor(IOError):
547 with open(path, 'rb') as f:
548 return f.read() == self._content
549 return False
550
551
552class DefaultFilesystemTestContent(FilesystemTestDirectory):
553 def __init__(self):
554 super(DefaultFilesystemTestContent, self).__init__('', [
555 FilesystemTestFile('file1', '0123456789'),
556 FilesystemTestDirectory('dir1', [
557 FilesystemTestFile('file1', ''),
558 FilesystemTestFile('file2', 'abcdefg'),
559 FilesystemTestDirectory('dir2', [
560 FilesystemTestFile('file3', 'abcdefg'),
Anand K Mistrye00f8a42018-08-21 11:09:54 +1000561 FilesystemTestFile('file4', 'a' * 65536),
Ben Chan0499e532011-08-29 10:53:18 -0700562 ]),
563 ]),
564 ], stat.S_IRWXU|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH)
565
566
567class VirtualFilesystemImage(object):
568 def __init__(self, block_size, block_count, filesystem_type,
569 *args, **kwargs):
570 """Initializes the instance.
571
572 Args:
573 block_size: The number of bytes of each block in the image.
574 block_count: The number of blocks in the image.
575 filesystem_type: The filesystem type to be given to the mkfs
576 program for formatting the image.
577
578 Keyword Args:
579 mount_filesystem_type: The filesystem type to be given to the
580 mount program for mounting the image.
581 mkfs_options: A list of options to be given to the mkfs program.
582 """
583 self._block_size = block_size
584 self._block_count = block_count
585 self._filesystem_type = filesystem_type
586 self._mount_filesystem_type = kwargs.get('mount_filesystem_type')
587 if self._mount_filesystem_type is None:
588 self._mount_filesystem_type = filesystem_type
589 self._mkfs_options = kwargs.get('mkfs_options')
590 if self._mkfs_options is None:
591 self._mkfs_options = []
592 self._image_file = None
593 self._loop_device = None
Ben Chan33b5f042017-08-21 13:45:30 -0700594 self._loop_device_stat = None
Ben Chan0499e532011-08-29 10:53:18 -0700595 self._mount_dir = None
596
597 def __del__(self):
598 with ExceptionSuppressor(Exception):
599 self.clean()
600
601 def __enter__(self):
602 self.create()
603 return self
604
605 def __exit__(self, exc_type, exc_value, traceback):
606 self.clean()
607 return False
608
609 def _remove_temp_path(self, temp_path):
610 """Removes a temporary file or directory created using autotemp."""
611 if temp_path:
612 with ExceptionSuppressor(Exception):
613 path = temp_path.name
614 temp_path.clean()
615 logging.debug('Removed "%s"', path)
616
617 def _remove_image_file(self):
618 """Removes the image file if one has been created."""
619 self._remove_temp_path(self._image_file)
620 self._image_file = None
621
622 def _remove_mount_dir(self):
623 """Removes the mount directory if one has been created."""
624 self._remove_temp_path(self._mount_dir)
625 self._mount_dir = None
626
627 @property
628 def image_file(self):
629 """Gets the path of the image file.
630
631 Returns:
632 The path of the image file or None if no image file has been
633 created.
634 """
635 return self._image_file.name if self._image_file else None
636
637 @property
638 def loop_device(self):
639 """Gets the loop device where the image file is attached to.
640
641 Returns:
642 The path of the loop device where the image file is attached to or
643 None if no loop device is attaching the image file.
644 """
645 return self._loop_device
646
647 @property
648 def mount_dir(self):
649 """Gets the directory where the image file is mounted to.
650
651 Returns:
652 The directory where the image file is mounted to or None if no
653 mount directory has been created.
654 """
655 return self._mount_dir.name if self._mount_dir else None
656
657 def create(self):
658 """Creates a zero-filled image file with the specified size.
659
660 The created image file is temporary and removed when clean()
661 is called.
662 """
663 self.clean()
664 self._image_file = autotemp.tempfile(unique_id='fsImage')
665 try:
666 logging.debug('Creating zero-filled image file at "%s"',
667 self._image_file.name)
668 utils.run('dd if=/dev/zero of=%s bs=%s count=%s' %
669 (self._image_file.name, self._block_size,
670 self._block_count))
671 except error.CmdError as exc:
672 self._remove_image_file()
673 message = 'Failed to create filesystem image: %s' % exc
674 raise RuntimeError(message)
675
676 def clean(self):
677 """Removes the image file if one has been created.
678
679 Before removal, the image file is detached from the loop device that
680 it is attached to.
681 """
682 self.detach_from_loop_device()
683 self._remove_image_file()
684
685 def attach_to_loop_device(self):
686 """Attaches the created image file to a loop device.
687
688 Creates the image file, if one has not been created, by calling
689 create().
690
691 Returns:
692 The path of the loop device where the image file is attached to.
693 """
694 if self._loop_device:
695 return self._loop_device
696
697 if not self._image_file:
698 self.create()
699
700 logging.debug('Attaching image file "%s" to loop device',
701 self._image_file.name)
702 utils.run('losetup -f %s' % self._image_file.name)
703 output = utils.system_output('losetup -j %s' % self._image_file.name)
704 # output should look like: "/dev/loop0: [000d]:6329 (/tmp/test.img)"
705 self._loop_device = output.split(':')[0]
706 logging.debug('Attached image file "%s" to loop device "%s"',
707 self._image_file.name, self._loop_device)
Ben Chan33b5f042017-08-21 13:45:30 -0700708
709 self._loop_device_stat = os.stat(self._loop_device)
710 logging.debug('Loop device "%s" (uid=%d, gid=%d, permissions=%04o)',
711 self._loop_device,
712 self._loop_device_stat.st_uid,
713 self._loop_device_stat.st_gid,
714 stat.S_IMODE(self._loop_device_stat.st_mode))
Ben Chan0499e532011-08-29 10:53:18 -0700715 return self._loop_device
716
717 def detach_from_loop_device(self):
718 """Detaches the image file from the loop device."""
719 if not self._loop_device:
720 return
721
722 self.unmount()
723
724 logging.debug('Cleaning up remaining mount points of loop device "%s"',
725 self._loop_device)
726 utils.run('umount -f %s' % self._loop_device, ignore_status=True)
727
Ben Chan33b5f042017-08-21 13:45:30 -0700728 logging.debug('Restore ownership/permissions of loop device "%s"',
729 self._loop_device)
730 os.chmod(self._loop_device,
731 stat.S_IMODE(self._loop_device_stat.st_mode))
732 os.chown(self._loop_device,
733 self._loop_device_stat.st_uid, self._loop_device_stat.st_gid)
734
Ben Chan0499e532011-08-29 10:53:18 -0700735 logging.debug('Detaching image file "%s" from loop device "%s"',
736 self._image_file.name, self._loop_device)
737 utils.run('losetup -d %s' % self._loop_device)
738 self._loop_device = None
739
740 def format(self):
741 """Formats the image file as the specified filesystem."""
742 self.attach_to_loop_device()
743 try:
744 logging.debug('Formatting image file at "%s" as "%s" filesystem',
745 self._image_file.name, self._filesystem_type)
746 utils.run('yes | mkfs -t %s %s %s' %
747 (self._filesystem_type, ' '.join(self._mkfs_options),
748 self._loop_device))
749 logging.debug('blkid: %s', utils.system_output(
750 'blkid -c /dev/null %s' % self._loop_device,
751 ignore_status=True))
752 except error.CmdError as exc:
753 message = 'Failed to format filesystem image: %s' % exc
754 raise RuntimeError(message)
755
756 def mount(self, options=None):
757 """Mounts the image file to a directory.
758
759 Args:
760 options: An optional list of mount options.
761 """
762 if self._mount_dir:
763 return self._mount_dir.name
764
765 if options is None:
766 options = []
767
768 options_arg = ','.join(options)
769 if options_arg:
770 options_arg = '-o ' + options_arg
771
772 self.attach_to_loop_device()
773 self._mount_dir = autotemp.tempdir(unique_id='fsImage')
774 try:
775 logging.debug('Mounting image file "%s" (%s) to directory "%s"',
776 self._image_file.name, self._loop_device,
777 self._mount_dir.name)
778 utils.run('mount -t %s %s %s %s' %
779 (self._mount_filesystem_type, options_arg,
780 self._loop_device, self._mount_dir.name))
781 except error.CmdError as exc:
782 self._remove_mount_dir()
783 message = ('Failed to mount virtual filesystem image "%s": %s' %
784 (self._image_file.name, exc))
785 raise RuntimeError(message)
786 return self._mount_dir.name
787
788 def unmount(self):
789 """Unmounts the image file from the mounted directory."""
790 if not self._mount_dir:
791 return
792
793 try:
794 logging.debug('Unmounting image file "%s" (%s) from directory "%s"',
795 self._image_file.name, self._loop_device,
796 self._mount_dir.name)
797 utils.run('umount %s' % self._mount_dir.name)
798 except error.CmdError as exc:
799 message = ('Failed to unmount virtual filesystem image "%s": %s' %
800 (self._image_file.name, exc))
801 raise RuntimeError(message)
802 finally:
803 self._remove_mount_dir()
Klemen Kozjek7740bfd2017-08-15 16:40:30 +0900804
805 def get_volume_label(self):
806 """Gets volume name information of |self._loop_device|
807
808 @return a string with volume name if it exists.
809 """
810 # This script is run as root in a normal autotest run,
811 # so this works: It doesn't have access to the necessary info
812 # when run as a non-privileged user
813 cmd = "blkid -c /dev/null -o udev %s" % self._loop_device
814 output = utils.system_output(cmd, ignore_status=True)
815
816 for line in output.splitlines():
817 udev_key, udev_val = line.split('=')
818
819 if udev_key == 'ID_FS_LABEL':
820 return udev_val
821
822 return None