blob: abdad779eb79a2579df036562220733ad8754027 [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
199 def __init__(self, main_loop, bus):
200 """Initializes the instance.
201
202 Args:
203 main_loop: The GLib main loop.
204 bus: The bus where the DBus server is connected to.
205 """
206 super(CrosDisksClient, self).__init__(main_loop, bus,
207 self.CROS_DISKS_BUS_NAME,
208 self.CROS_DISKS_OBJECT_PATH)
209 self.interface = dbus.Interface(self.proxy_object,
210 self.CROS_DISKS_INTERFACE)
211 self.properties = dbus.Interface(self.proxy_object,
212 self.DBUS_PROPERTIES_INTERFACE)
213 self.handle_signal(self.CROS_DISKS_INTERFACE,
Ben Chan81904f12011-11-21 17:20:18 -0800214 self.FORMAT_COMPLETED_SIGNAL,
215 self.FORMAT_COMPLETED_SIGNAL_ARGUMENTS)
216 self.handle_signal(self.CROS_DISKS_INTERFACE,
Ben Chan0499e532011-08-29 10:53:18 -0700217 self.MOUNT_COMPLETED_SIGNAL,
218 self.MOUNT_COMPLETED_SIGNAL_ARGUMENTS)
Klemen Kozjek7740bfd2017-08-15 16:40:30 +0900219 self.handle_signal(self.CROS_DISKS_INTERFACE,
220 self.RENAME_COMPLETED_SIGNAL,
221 self.RENAME_COMPLETED_SIGNAL_ARGUMENTS)
Ben Chan0499e532011-08-29 10:53:18 -0700222
Ben Chan0499e532011-08-29 10:53:18 -0700223 def enumerate_devices(self):
224 """Invokes the CrosDisks EnumerateMountableDevices method.
225
226 Returns:
227 A list of sysfs paths of devices that are recognized by
228 CrosDisks.
229 """
230 return self.interface.EnumerateDevices()
231
232 def get_device_properties(self, path):
233 """Invokes the CrosDisks GetDeviceProperties method.
234
235 Args:
236 path: The device path.
237
238 Returns:
239 The properties of the device in a dictionary.
240 """
241 return self.interface.GetDeviceProperties(path)
242
Ben Chan81904f12011-11-21 17:20:18 -0800243 def format(self, path, filesystem_type=None, options=None):
244 """Invokes the CrosDisks Format method.
245
246 Args:
247 path: The device path to format.
248 filesystem_type: The filesystem type used for formatting the device.
249 options: A list of options used for formatting the device.
250 """
251 if filesystem_type is None:
252 filesystem_type = ''
253 if options is None:
254 options = []
255 self.clear_signal_content(self.FORMAT_COMPLETED_SIGNAL)
Ben Chan577e66b2017-10-12 19:18:03 -0700256 self.interface.Format(path, filesystem_type,
257 dbus.Array(options, signature='s'))
Ben Chan81904f12011-11-21 17:20:18 -0800258
259 def wait_for_format_completion(self):
260 """Waits for the CrosDisks FormatCompleted signal.
261
262 Returns:
263 The content of the FormatCompleted signal.
264 """
265 return self.wait_for_signal(self.FORMAT_COMPLETED_SIGNAL)
266
267 def expect_format_completion(self, expected_content):
268 """Waits and verifies for the CrosDisks FormatCompleted signal.
269
270 Args:
271 expected_content: The expected content of the FormatCompleted
272 signal, which can be partially specified.
273 Only specified fields are compared between the
274 actual and expected content.
275
276 Returns:
277 The actual content of the FormatCompleted signal.
278
279 Raises:
280 error.TestFail: A test failure when there is a mismatch between the
281 actual and expected content of the FormatCompleted
282 signal.
283 """
284 return self.expect_signal(self.FORMAT_COMPLETED_SIGNAL,
285 expected_content)
286
Klemen Kozjek7740bfd2017-08-15 16:40:30 +0900287 def rename(self, path, volume_name=None):
288 """Invokes the CrosDisks Rename method.
289
290 Args:
291 path: The device path to rename.
292 volume_name: The new name used for renaming.
293 """
294 if volume_name is None:
295 volume_name = ''
296 self.clear_signal_content(self.RENAME_COMPLETED_SIGNAL)
297 self.interface.Rename(path, volume_name)
298
299 def wait_for_rename_completion(self):
300 """Waits for the CrosDisks RenameCompleted signal.
301
302 Returns:
303 The content of the RenameCompleted signal.
304 """
305 return self.wait_for_signal(self.RENAME_COMPLETED_SIGNAL)
306
307 def expect_rename_completion(self, expected_content):
308 """Waits and verifies for the CrosDisks RenameCompleted signal.
309
310 Args:
311 expected_content: The expected content of the RenameCompleted
312 signal, which can be partially specified.
313 Only specified fields are compared between the
314 actual and expected content.
315
316 Returns:
317 The actual content of the RenameCompleted signal.
318
319 Raises:
320 error.TestFail: A test failure when there is a mismatch between the
321 actual and expected content of the RenameCompleted
322 signal.
323 """
324 return self.expect_signal(self.RENAME_COMPLETED_SIGNAL,
325 expected_content)
326
Ben Chan0499e532011-08-29 10:53:18 -0700327 def mount(self, path, filesystem_type=None, options=None):
328 """Invokes the CrosDisks Mount method.
329
330 Args:
331 path: The device path to mount.
332 filesystem_type: The filesystem type used for mounting the device.
333 options: A list of options used for mounting the device.
334 """
335 if filesystem_type is None:
336 filesystem_type = ''
337 if options is None:
338 options = []
339 self.clear_signal_content(self.MOUNT_COMPLETED_SIGNAL)
Ben Chan577e66b2017-10-12 19:18:03 -0700340 self.interface.Mount(path, filesystem_type,
341 dbus.Array(options, signature='s'))
Ben Chan0499e532011-08-29 10:53:18 -0700342
343 def unmount(self, path, options=None):
344 """Invokes the CrosDisks Unmount method.
345
346 Args:
347 path: The device or mount path to unmount.
348 options: A list of options used for unmounting the path.
349 """
350 if options is None:
351 options = []
Ben Chan577e66b2017-10-12 19:18:03 -0700352 self.interface.Unmount(path, dbus.Array(options, signature='s'))
Ben Chan0499e532011-08-29 10:53:18 -0700353
354 def wait_for_mount_completion(self):
355 """Waits for the CrosDisks MountCompleted signal.
356
357 Returns:
358 The content of the MountCompleted signal.
359 """
360 return self.wait_for_signal(self.MOUNT_COMPLETED_SIGNAL)
361
362 def expect_mount_completion(self, expected_content):
363 """Waits and verifies for the CrosDisks MountCompleted signal.
364
365 Args:
366 expected_content: The expected content of the MountCompleted
367 signal, which can be partially specified.
368 Only specified fields are compared between the
369 actual and expected content.
370
371 Returns:
372 The actual content of the MountCompleted signal.
373
Ben Chan0499e532011-08-29 10:53:18 -0700374 Raises:
375 error.TestFail: A test failure when there is a mismatch between the
376 actual and expected content of the MountCompleted
377 signal.
378 """
Ben Chan81904f12011-11-21 17:20:18 -0800379 return self.expect_signal(self.MOUNT_COMPLETED_SIGNAL,
380 expected_content)
Ben Chan0499e532011-08-29 10:53:18 -0700381
382
383class CrosDisksTester(GenericTesterMainLoop):
384 """A base tester class for testing the CrosDisks server.
385
386 A derived class should override the get_tests method to return a list of
387 test methods. The perform_one_test method invokes each test method in the
388 list to verify some functionalities of CrosDisks server.
389 """
390 def __init__(self, test):
391 bus_loop = DBusGMainLoop(set_as_default=True)
392 bus = dbus.SystemBus(mainloop=bus_loop)
393 self.main_loop = gobject.MainLoop()
394 super(CrosDisksTester, self).__init__(test, self.main_loop)
395 self.cros_disks = CrosDisksClient(self.main_loop, bus)
396
397 def get_tests(self):
398 """Returns a list of test methods to be invoked by perform_one_test.
399
400 A derived class should override this method.
401
402 Returns:
403 A list of test methods.
404 """
405 return []
406
407 @ExceptionForward
408 def perform_one_test(self):
409 """Exercises each test method in the list returned by get_tests.
410 """
411 tests = self.get_tests()
412 self.remaining_requirements = set([test.func_name for test in tests])
413 for test in tests:
414 test()
415 self.requirement_completed(test.func_name)
416
417
418class FilesystemTestObject(object):
419 """A base class to represent a filesystem test object.
420
421 A filesystem test object can be a file, directory or symbolic link.
422 A derived class should override the _create and _verify method to implement
423 how the test object should be created and verified, respectively, on a
424 filesystem.
425 """
426 def __init__(self, path, content, mode):
427 """Initializes the instance.
428
429 Args:
430 path: The relative path of the test object.
431 content: The content of the test object.
432 mode: The file permissions given to the test object.
433 """
434 self._path = path
435 self._content = content
436 self._mode = mode
437
438 def create(self, base_dir):
439 """Creates the test object in a base directory.
440
441 Args:
442 base_dir: The base directory where the test object is created.
443
444 Returns:
445 True if the test object is created successfully or False otherwise.
446 """
447 if not self._create(base_dir):
448 logging.debug('Failed to create filesystem test object at "%s"',
449 os.path.join(base_dir, self._path))
450 return False
451 return True
452
453 def verify(self, base_dir):
454 """Verifies the test object in a base directory.
455
456 Args:
457 base_dir: The base directory where the test object is expected to be
458 found.
459
460 Returns:
461 True if the test object is found in the base directory and matches
462 the expected content, or False otherwise.
463 """
464 if not self._verify(base_dir):
465 logging.debug('Failed to verify filesystem test object at "%s"',
466 os.path.join(base_dir, self._path))
467 return False
468 return True
469
470 def _create(self, base_dir):
471 return False
472
473 def _verify(self, base_dir):
474 return False
475
476
477class FilesystemTestDirectory(FilesystemTestObject):
478 """A filesystem test object that represents a directory."""
479
480 def __init__(self, path, content, mode=stat.S_IRWXU|stat.S_IRGRP| \
481 stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH):
482 super(FilesystemTestDirectory, self).__init__(path, content, mode)
483
484 def _create(self, base_dir):
485 path = os.path.join(base_dir, self._path) if self._path else base_dir
486
487 if self._path:
488 with ExceptionSuppressor(OSError):
489 os.makedirs(path)
490 os.chmod(path, self._mode)
491
492 if not os.path.isdir(path):
493 return False
494
495 for content in self._content:
496 if not content.create(path):
497 return False
498 return True
499
500 def _verify(self, base_dir):
501 path = os.path.join(base_dir, self._path) if self._path else base_dir
502 if not os.path.isdir(path):
503 return False
504
505 for content in self._content:
506 if not content.verify(path):
507 return False
508 return True
509
510
511class FilesystemTestFile(FilesystemTestObject):
512 """A filesystem test object that represents a file."""
513
514 def __init__(self, path, content, mode=stat.S_IRUSR|stat.S_IWUSR| \
515 stat.S_IRGRP|stat.S_IROTH):
516 super(FilesystemTestFile, self).__init__(path, content, mode)
517
518 def _create(self, base_dir):
519 path = os.path.join(base_dir, self._path)
520 with ExceptionSuppressor(IOError):
521 with open(path, 'wb+') as f:
522 f.write(self._content)
Ben Chanf6a74c52013-02-19 14:06:17 -0800523 with ExceptionSuppressor(OSError):
524 os.chmod(path, self._mode)
Ben Chan0499e532011-08-29 10:53:18 -0700525 return True
526 return False
527
528 def _verify(self, base_dir):
529 path = os.path.join(base_dir, self._path)
530 with ExceptionSuppressor(IOError):
531 with open(path, 'rb') as f:
532 return f.read() == self._content
533 return False
534
535
536class DefaultFilesystemTestContent(FilesystemTestDirectory):
537 def __init__(self):
538 super(DefaultFilesystemTestContent, self).__init__('', [
539 FilesystemTestFile('file1', '0123456789'),
540 FilesystemTestDirectory('dir1', [
541 FilesystemTestFile('file1', ''),
542 FilesystemTestFile('file2', 'abcdefg'),
543 FilesystemTestDirectory('dir2', [
544 FilesystemTestFile('file3', 'abcdefg'),
545 ]),
546 ]),
547 ], stat.S_IRWXU|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH)
548
549
550class VirtualFilesystemImage(object):
551 def __init__(self, block_size, block_count, filesystem_type,
552 *args, **kwargs):
553 """Initializes the instance.
554
555 Args:
556 block_size: The number of bytes of each block in the image.
557 block_count: The number of blocks in the image.
558 filesystem_type: The filesystem type to be given to the mkfs
559 program for formatting the image.
560
561 Keyword Args:
562 mount_filesystem_type: The filesystem type to be given to the
563 mount program for mounting the image.
564 mkfs_options: A list of options to be given to the mkfs program.
565 """
566 self._block_size = block_size
567 self._block_count = block_count
568 self._filesystem_type = filesystem_type
569 self._mount_filesystem_type = kwargs.get('mount_filesystem_type')
570 if self._mount_filesystem_type is None:
571 self._mount_filesystem_type = filesystem_type
572 self._mkfs_options = kwargs.get('mkfs_options')
573 if self._mkfs_options is None:
574 self._mkfs_options = []
575 self._image_file = None
576 self._loop_device = None
Ben Chan33b5f042017-08-21 13:45:30 -0700577 self._loop_device_stat = None
Ben Chan0499e532011-08-29 10:53:18 -0700578 self._mount_dir = None
579
580 def __del__(self):
581 with ExceptionSuppressor(Exception):
582 self.clean()
583
584 def __enter__(self):
585 self.create()
586 return self
587
588 def __exit__(self, exc_type, exc_value, traceback):
589 self.clean()
590 return False
591
592 def _remove_temp_path(self, temp_path):
593 """Removes a temporary file or directory created using autotemp."""
594 if temp_path:
595 with ExceptionSuppressor(Exception):
596 path = temp_path.name
597 temp_path.clean()
598 logging.debug('Removed "%s"', path)
599
600 def _remove_image_file(self):
601 """Removes the image file if one has been created."""
602 self._remove_temp_path(self._image_file)
603 self._image_file = None
604
605 def _remove_mount_dir(self):
606 """Removes the mount directory if one has been created."""
607 self._remove_temp_path(self._mount_dir)
608 self._mount_dir = None
609
610 @property
611 def image_file(self):
612 """Gets the path of the image file.
613
614 Returns:
615 The path of the image file or None if no image file has been
616 created.
617 """
618 return self._image_file.name if self._image_file else None
619
620 @property
621 def loop_device(self):
622 """Gets the loop device where the image file is attached to.
623
624 Returns:
625 The path of the loop device where the image file is attached to or
626 None if no loop device is attaching the image file.
627 """
628 return self._loop_device
629
630 @property
631 def mount_dir(self):
632 """Gets the directory where the image file is mounted to.
633
634 Returns:
635 The directory where the image file is mounted to or None if no
636 mount directory has been created.
637 """
638 return self._mount_dir.name if self._mount_dir else None
639
640 def create(self):
641 """Creates a zero-filled image file with the specified size.
642
643 The created image file is temporary and removed when clean()
644 is called.
645 """
646 self.clean()
647 self._image_file = autotemp.tempfile(unique_id='fsImage')
648 try:
649 logging.debug('Creating zero-filled image file at "%s"',
650 self._image_file.name)
651 utils.run('dd if=/dev/zero of=%s bs=%s count=%s' %
652 (self._image_file.name, self._block_size,
653 self._block_count))
654 except error.CmdError as exc:
655 self._remove_image_file()
656 message = 'Failed to create filesystem image: %s' % exc
657 raise RuntimeError(message)
658
659 def clean(self):
660 """Removes the image file if one has been created.
661
662 Before removal, the image file is detached from the loop device that
663 it is attached to.
664 """
665 self.detach_from_loop_device()
666 self._remove_image_file()
667
668 def attach_to_loop_device(self):
669 """Attaches the created image file to a loop device.
670
671 Creates the image file, if one has not been created, by calling
672 create().
673
674 Returns:
675 The path of the loop device where the image file is attached to.
676 """
677 if self._loop_device:
678 return self._loop_device
679
680 if not self._image_file:
681 self.create()
682
683 logging.debug('Attaching image file "%s" to loop device',
684 self._image_file.name)
685 utils.run('losetup -f %s' % self._image_file.name)
686 output = utils.system_output('losetup -j %s' % self._image_file.name)
687 # output should look like: "/dev/loop0: [000d]:6329 (/tmp/test.img)"
688 self._loop_device = output.split(':')[0]
689 logging.debug('Attached image file "%s" to loop device "%s"',
690 self._image_file.name, self._loop_device)
Ben Chan33b5f042017-08-21 13:45:30 -0700691
692 self._loop_device_stat = os.stat(self._loop_device)
693 logging.debug('Loop device "%s" (uid=%d, gid=%d, permissions=%04o)',
694 self._loop_device,
695 self._loop_device_stat.st_uid,
696 self._loop_device_stat.st_gid,
697 stat.S_IMODE(self._loop_device_stat.st_mode))
Ben Chan0499e532011-08-29 10:53:18 -0700698 return self._loop_device
699
700 def detach_from_loop_device(self):
701 """Detaches the image file from the loop device."""
702 if not self._loop_device:
703 return
704
705 self.unmount()
706
707 logging.debug('Cleaning up remaining mount points of loop device "%s"',
708 self._loop_device)
709 utils.run('umount -f %s' % self._loop_device, ignore_status=True)
710
Ben Chan33b5f042017-08-21 13:45:30 -0700711 logging.debug('Restore ownership/permissions of loop device "%s"',
712 self._loop_device)
713 os.chmod(self._loop_device,
714 stat.S_IMODE(self._loop_device_stat.st_mode))
715 os.chown(self._loop_device,
716 self._loop_device_stat.st_uid, self._loop_device_stat.st_gid)
717
Ben Chan0499e532011-08-29 10:53:18 -0700718 logging.debug('Detaching image file "%s" from loop device "%s"',
719 self._image_file.name, self._loop_device)
720 utils.run('losetup -d %s' % self._loop_device)
721 self._loop_device = None
722
723 def format(self):
724 """Formats the image file as the specified filesystem."""
725 self.attach_to_loop_device()
726 try:
727 logging.debug('Formatting image file at "%s" as "%s" filesystem',
728 self._image_file.name, self._filesystem_type)
729 utils.run('yes | mkfs -t %s %s %s' %
730 (self._filesystem_type, ' '.join(self._mkfs_options),
731 self._loop_device))
732 logging.debug('blkid: %s', utils.system_output(
733 'blkid -c /dev/null %s' % self._loop_device,
734 ignore_status=True))
735 except error.CmdError as exc:
736 message = 'Failed to format filesystem image: %s' % exc
737 raise RuntimeError(message)
738
739 def mount(self, options=None):
740 """Mounts the image file to a directory.
741
742 Args:
743 options: An optional list of mount options.
744 """
745 if self._mount_dir:
746 return self._mount_dir.name
747
748 if options is None:
749 options = []
750
751 options_arg = ','.join(options)
752 if options_arg:
753 options_arg = '-o ' + options_arg
754
755 self.attach_to_loop_device()
756 self._mount_dir = autotemp.tempdir(unique_id='fsImage')
757 try:
758 logging.debug('Mounting image file "%s" (%s) to directory "%s"',
759 self._image_file.name, self._loop_device,
760 self._mount_dir.name)
761 utils.run('mount -t %s %s %s %s' %
762 (self._mount_filesystem_type, options_arg,
763 self._loop_device, self._mount_dir.name))
764 except error.CmdError as exc:
765 self._remove_mount_dir()
766 message = ('Failed to mount virtual filesystem image "%s": %s' %
767 (self._image_file.name, exc))
768 raise RuntimeError(message)
769 return self._mount_dir.name
770
771 def unmount(self):
772 """Unmounts the image file from the mounted directory."""
773 if not self._mount_dir:
774 return
775
776 try:
777 logging.debug('Unmounting image file "%s" (%s) from directory "%s"',
778 self._image_file.name, self._loop_device,
779 self._mount_dir.name)
780 utils.run('umount %s' % self._mount_dir.name)
781 except error.CmdError as exc:
782 message = ('Failed to unmount virtual filesystem image "%s": %s' %
783 (self._image_file.name, exc))
784 raise RuntimeError(message)
785 finally:
786 self._remove_mount_dir()
Klemen Kozjek7740bfd2017-08-15 16:40:30 +0900787
788 def get_volume_label(self):
789 """Gets volume name information of |self._loop_device|
790
791 @return a string with volume name if it exists.
792 """
793 # This script is run as root in a normal autotest run,
794 # so this works: It doesn't have access to the necessary info
795 # when run as a non-privileged user
796 cmd = "blkid -c /dev/null -o udev %s" % self._loop_device
797 output = utils.system_output(cmd, ignore_status=True)
798
799 for line in output.splitlines():
800 udev_key, udev_val = line.split('=')
801
802 if udev_key == 'ID_FS_LABEL':
803 return udev_val
804
805 return None