blob: 6daaa84b1ef2741058a005cab692d994753d6ad3 [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
barfab@chromium.orgb6d29932012-04-11 09:46:43 +020011from mainloop import ExceptionForward
12from mainloop import GenericTesterMainLoop
Ben Chan0499e532011-08-29 10:53:18 -070013
14
15"""This module contains several helper classes for writing tests to verify the
16CrosDisks DBus interface. In particular, the CrosDisksTester class can be used
17to derive functional tests that interact with the CrosDisks server over DBus.
18"""
19
20
21class ExceptionSuppressor(object):
22 """A context manager class for suppressing certain types of exception.
23
24 An instance of this class is expected to be used with the with statement
25 and takes a set of exception classes at instantiation, which are types of
26 exception to be suppressed (and logged) in the code block under the with
27 statement.
28
29 Example:
30
31 with ExceptionSuppressor(OSError, IOError):
32 # An exception, which is a sub-class of OSError or IOError, is
33 # suppressed in the block code under the with statement.
34 """
35 def __init__(self, *args):
36 self.__suppressed_exc_types = (args)
37
38 def __enter__(self):
39 return self
40
41 def __exit__(self, exc_type, exc_value, traceback):
42 if exc_type and issubclass(exc_type, self.__suppressed_exc_types):
43 try:
44 logging.exception('Suppressed exception: %s(%s)',
45 exc_type, exc_value)
46 except Exception:
47 pass
48 return True
49 return False
50
51
52class DBusClient(object):
53 """ A base class of a DBus proxy client to test a DBus server.
54
55 This class is expected to be used along with a GLib main loop and provides
56 some convenient functions for testing the DBus API exposed by a DBus server.
57 """
58 def __init__(self, main_loop, bus, bus_name, object_path):
59 """Initializes the instance.
60
61 Args:
62 main_loop: The GLib main loop.
63 bus: The bus where the DBus server is connected to.
64 bus_name: The bus name owned by the DBus server.
65 object_path: The object path of the DBus server.
66 """
67 self.__signal_content = {}
68 self.main_loop = main_loop
69 self.signal_timeout_in_seconds = 10
70 logging.debug('Getting D-Bus proxy object on bus "%s" and path "%s"',
71 bus_name, object_path)
72 self.proxy_object = bus.get_object(bus_name, object_path)
73
74 def clear_signal_content(self, signal_name):
75 """Clears the content of the signal.
76
77 Args:
78 signal_name: The name of the signal.
79 """
80 if signal_name in self.__signal_content:
81 self.__signal_content[signal_name] = None
82
83 def get_signal_content(self, signal_name):
84 """Gets the content of a signal.
85
86 Args:
87 signal_name: The name of the signal.
88
89 Returns:
90 The content of a signal or None if the signal is not being handled.
91 """
92 return self.__signal_content.get(signal_name)
93
94 def handle_signal(self, interface, signal_name, argument_names=()):
95 """Registers a signal handler to handle a given signal.
96
97 Args:
98 interface: The DBus interface of the signal.
99 signal_name: The name of the signal.
100 argument_names: A list of argument names that the signal contains.
101 """
102 if signal_name in self.__signal_content:
103 return
104
105 self.__signal_content[signal_name] = None
106
107 def signal_handler(*args):
108 self.__signal_content[signal_name] = dict(zip(argument_names, args))
109
110 logging.debug('Handling D-Bus signal "%s(%s)" on interface "%s"',
111 signal_name, ', '.join(argument_names), interface)
112 self.proxy_object.connect_to_signal(signal_name, signal_handler,
113 interface)
114
115 def wait_for_signal(self, signal_name):
Ben Chan81904f12011-11-21 17:20:18 -0800116 """Waits for the reception of a signal.
117
118 Args:
119 signal_name: The name of the signal to wait for.
Ben Chan0499e532011-08-29 10:53:18 -0700120
121 Returns:
122 The content of the signal.
123 """
124 if signal_name not in self.__signal_content:
125 return None
126
127 def check_signal_content():
128 context = self.main_loop.get_context()
129 while context.iteration(False):
130 pass
131 return self.__signal_content[signal_name] is not None
132
133 logging.debug('Waiting for D-Bus signal "%s"', signal_name)
134 utils.poll_for_condition(condition=check_signal_content,
135 desc='%s signal' % signal_name,
136 timeout=self.signal_timeout_in_seconds)
137 content = self.__signal_content[signal_name]
138 logging.debug('Received D-Bus signal "%s(%s)"', signal_name, content)
139 self.__signal_content[signal_name] = None
140 return content
141
Ben Chan81904f12011-11-21 17:20:18 -0800142 def expect_signal(self, signal_name, expected_content):
143 """Waits the the reception of a signal and verifies its content.
144
145 Args:
146 signal_name: The name of the signal to wait for.
147 expected_content: The expected content of the signal, which can be
148 partially specified. Only specified fields are
149 compared between the actual and expected content.
150
151 Returns:
152 The actual content of the signal.
153
154 Raises:
155 error.TestFail: A test failure when there is a mismatch between the
156 actual and expected content of the signal.
157 """
158 actual_content = self.wait_for_signal(signal_name)
159 logging.debug("%s signal: expected=%s actual=%s",
160 signal_name, expected_content, actual_content)
161 for argument, expected_value in expected_content.iteritems():
162 if argument not in actual_content:
163 raise error.TestFail(
164 ('%s signal missing "%s": expected=%s, actual=%s') %
165 (signal_name, argument, expected_content, actual_content))
166
167 if actual_content[argument] != expected_value:
168 raise error.TestFail(
169 ('%s signal not matched on "%s": expected=%s, actual=%s') %
170 (signal_name, argument, expected_content, actual_content))
171 return actual_content
172
Ben Chan0499e532011-08-29 10:53:18 -0700173
174class CrosDisksClient(DBusClient):
175 """A DBus proxy client for testing the CrosDisks DBus server.
176 """
177
178 CROS_DISKS_BUS_NAME = 'org.chromium.CrosDisks'
179 CROS_DISKS_INTERFACE = 'org.chromium.CrosDisks'
180 CROS_DISKS_OBJECT_PATH = '/org/chromium/CrosDisks'
181 DBUS_PROPERTIES_INTERFACE = 'org.freedesktop.DBus.Properties'
Ben Chan81904f12011-11-21 17:20:18 -0800182 FORMAT_COMPLETED_SIGNAL = 'FormatCompleted'
183 FORMAT_COMPLETED_SIGNAL_ARGUMENTS = (
184 'status', 'path'
185 )
Ben Chan0499e532011-08-29 10:53:18 -0700186 MOUNT_COMPLETED_SIGNAL = 'MountCompleted'
187 MOUNT_COMPLETED_SIGNAL_ARGUMENTS = (
188 'status', 'source_path', 'source_type', 'mount_path'
189 )
Klemen Kozjek7740bfd2017-08-15 16:40:30 +0900190 RENAME_COMPLETED_SIGNAL = 'RenameCompleted'
191 RENAME_COMPLETED_SIGNAL_ARGUMENTS = (
192 'status', 'path'
193 )
Ben Chan0499e532011-08-29 10:53:18 -0700194
195 def __init__(self, main_loop, bus):
196 """Initializes the instance.
197
198 Args:
199 main_loop: The GLib main loop.
200 bus: The bus where the DBus server is connected to.
201 """
202 super(CrosDisksClient, self).__init__(main_loop, bus,
203 self.CROS_DISKS_BUS_NAME,
204 self.CROS_DISKS_OBJECT_PATH)
205 self.interface = dbus.Interface(self.proxy_object,
206 self.CROS_DISKS_INTERFACE)
207 self.properties = dbus.Interface(self.proxy_object,
208 self.DBUS_PROPERTIES_INTERFACE)
209 self.handle_signal(self.CROS_DISKS_INTERFACE,
Ben Chan81904f12011-11-21 17:20:18 -0800210 self.FORMAT_COMPLETED_SIGNAL,
211 self.FORMAT_COMPLETED_SIGNAL_ARGUMENTS)
212 self.handle_signal(self.CROS_DISKS_INTERFACE,
Ben Chan0499e532011-08-29 10:53:18 -0700213 self.MOUNT_COMPLETED_SIGNAL,
214 self.MOUNT_COMPLETED_SIGNAL_ARGUMENTS)
Klemen Kozjek7740bfd2017-08-15 16:40:30 +0900215 self.handle_signal(self.CROS_DISKS_INTERFACE,
216 self.RENAME_COMPLETED_SIGNAL,
217 self.RENAME_COMPLETED_SIGNAL_ARGUMENTS)
Ben Chan0499e532011-08-29 10:53:18 -0700218
Ben Chan0499e532011-08-29 10:53:18 -0700219 def is_alive(self):
220 """Invokes the CrosDisks IsAlive method.
221
222 Returns:
223 True if the CrosDisks server is alive or False otherwise.
224 """
225 return self.interface.IsAlive()
226
227 def enumerate_auto_mountable_devices(self):
228 """Invokes the CrosDisks EnumerateAutoMountableDevices method.
229
230 Returns:
231 A list of sysfs paths of devices that are auto-mountable by
232 CrosDisks.
233 """
234 return self.interface.EnumerateAutoMountableDevices()
235
236 def enumerate_devices(self):
237 """Invokes the CrosDisks EnumerateMountableDevices method.
238
239 Returns:
240 A list of sysfs paths of devices that are recognized by
241 CrosDisks.
242 """
243 return self.interface.EnumerateDevices()
244
245 def get_device_properties(self, path):
246 """Invokes the CrosDisks GetDeviceProperties method.
247
248 Args:
249 path: The device path.
250
251 Returns:
252 The properties of the device in a dictionary.
253 """
254 return self.interface.GetDeviceProperties(path)
255
Ben Chan81904f12011-11-21 17:20:18 -0800256 def format(self, path, filesystem_type=None, options=None):
257 """Invokes the CrosDisks Format method.
258
259 Args:
260 path: The device path to format.
261 filesystem_type: The filesystem type used for formatting the device.
262 options: A list of options used for formatting the device.
263 """
264 if filesystem_type is None:
265 filesystem_type = ''
266 if options is None:
267 options = []
268 self.clear_signal_content(self.FORMAT_COMPLETED_SIGNAL)
Ben Chan577e66b2017-10-12 19:18:03 -0700269 self.interface.Format(path, filesystem_type,
270 dbus.Array(options, signature='s'))
Ben Chan81904f12011-11-21 17:20:18 -0800271
272 def wait_for_format_completion(self):
273 """Waits for the CrosDisks FormatCompleted signal.
274
275 Returns:
276 The content of the FormatCompleted signal.
277 """
278 return self.wait_for_signal(self.FORMAT_COMPLETED_SIGNAL)
279
280 def expect_format_completion(self, expected_content):
281 """Waits and verifies for the CrosDisks FormatCompleted signal.
282
283 Args:
284 expected_content: The expected content of the FormatCompleted
285 signal, which can be partially specified.
286 Only specified fields are compared between the
287 actual and expected content.
288
289 Returns:
290 The actual content of the FormatCompleted signal.
291
292 Raises:
293 error.TestFail: A test failure when there is a mismatch between the
294 actual and expected content of the FormatCompleted
295 signal.
296 """
297 return self.expect_signal(self.FORMAT_COMPLETED_SIGNAL,
298 expected_content)
299
Klemen Kozjek7740bfd2017-08-15 16:40:30 +0900300 def rename(self, path, volume_name=None):
301 """Invokes the CrosDisks Rename method.
302
303 Args:
304 path: The device path to rename.
305 volume_name: The new name used for renaming.
306 """
307 if volume_name is None:
308 volume_name = ''
309 self.clear_signal_content(self.RENAME_COMPLETED_SIGNAL)
310 self.interface.Rename(path, volume_name)
311
312 def wait_for_rename_completion(self):
313 """Waits for the CrosDisks RenameCompleted signal.
314
315 Returns:
316 The content of the RenameCompleted signal.
317 """
318 return self.wait_for_signal(self.RENAME_COMPLETED_SIGNAL)
319
320 def expect_rename_completion(self, expected_content):
321 """Waits and verifies for the CrosDisks RenameCompleted signal.
322
323 Args:
324 expected_content: The expected content of the RenameCompleted
325 signal, which can be partially specified.
326 Only specified fields are compared between the
327 actual and expected content.
328
329 Returns:
330 The actual content of the RenameCompleted signal.
331
332 Raises:
333 error.TestFail: A test failure when there is a mismatch between the
334 actual and expected content of the RenameCompleted
335 signal.
336 """
337 return self.expect_signal(self.RENAME_COMPLETED_SIGNAL,
338 expected_content)
339
Ben Chan0499e532011-08-29 10:53:18 -0700340 def mount(self, path, filesystem_type=None, options=None):
341 """Invokes the CrosDisks Mount method.
342
343 Args:
344 path: The device path to mount.
345 filesystem_type: The filesystem type used for mounting the device.
346 options: A list of options used for mounting the device.
347 """
348 if filesystem_type is None:
349 filesystem_type = ''
350 if options is None:
351 options = []
352 self.clear_signal_content(self.MOUNT_COMPLETED_SIGNAL)
Ben Chan577e66b2017-10-12 19:18:03 -0700353 self.interface.Mount(path, filesystem_type,
354 dbus.Array(options, signature='s'))
Ben Chan0499e532011-08-29 10:53:18 -0700355
356 def unmount(self, path, options=None):
357 """Invokes the CrosDisks Unmount method.
358
359 Args:
360 path: The device or mount path to unmount.
361 options: A list of options used for unmounting the path.
362 """
363 if options is None:
364 options = []
Ben Chan577e66b2017-10-12 19:18:03 -0700365 self.interface.Unmount(path, dbus.Array(options, signature='s'))
Ben Chan0499e532011-08-29 10:53:18 -0700366
367 def wait_for_mount_completion(self):
368 """Waits for the CrosDisks MountCompleted signal.
369
370 Returns:
371 The content of the MountCompleted signal.
372 """
373 return self.wait_for_signal(self.MOUNT_COMPLETED_SIGNAL)
374
375 def expect_mount_completion(self, expected_content):
376 """Waits and verifies for the CrosDisks MountCompleted signal.
377
378 Args:
379 expected_content: The expected content of the MountCompleted
380 signal, which can be partially specified.
381 Only specified fields are compared between the
382 actual and expected content.
383
384 Returns:
385 The actual content of the MountCompleted signal.
386
Ben Chan0499e532011-08-29 10:53:18 -0700387 Raises:
388 error.TestFail: A test failure when there is a mismatch between the
389 actual and expected content of the MountCompleted
390 signal.
391 """
Ben Chan81904f12011-11-21 17:20:18 -0800392 return self.expect_signal(self.MOUNT_COMPLETED_SIGNAL,
393 expected_content)
Ben Chan0499e532011-08-29 10:53:18 -0700394
395
396class CrosDisksTester(GenericTesterMainLoop):
397 """A base tester class for testing the CrosDisks server.
398
399 A derived class should override the get_tests method to return a list of
400 test methods. The perform_one_test method invokes each test method in the
401 list to verify some functionalities of CrosDisks server.
402 """
403 def __init__(self, test):
404 bus_loop = DBusGMainLoop(set_as_default=True)
405 bus = dbus.SystemBus(mainloop=bus_loop)
406 self.main_loop = gobject.MainLoop()
407 super(CrosDisksTester, self).__init__(test, self.main_loop)
408 self.cros_disks = CrosDisksClient(self.main_loop, bus)
409
410 def get_tests(self):
411 """Returns a list of test methods to be invoked by perform_one_test.
412
413 A derived class should override this method.
414
415 Returns:
416 A list of test methods.
417 """
418 return []
419
420 @ExceptionForward
421 def perform_one_test(self):
422 """Exercises each test method in the list returned by get_tests.
423 """
424 tests = self.get_tests()
425 self.remaining_requirements = set([test.func_name for test in tests])
426 for test in tests:
427 test()
428 self.requirement_completed(test.func_name)
429
430
431class FilesystemTestObject(object):
432 """A base class to represent a filesystem test object.
433
434 A filesystem test object can be a file, directory or symbolic link.
435 A derived class should override the _create and _verify method to implement
436 how the test object should be created and verified, respectively, on a
437 filesystem.
438 """
439 def __init__(self, path, content, mode):
440 """Initializes the instance.
441
442 Args:
443 path: The relative path of the test object.
444 content: The content of the test object.
445 mode: The file permissions given to the test object.
446 """
447 self._path = path
448 self._content = content
449 self._mode = mode
450
451 def create(self, base_dir):
452 """Creates the test object in a base directory.
453
454 Args:
455 base_dir: The base directory where the test object is created.
456
457 Returns:
458 True if the test object is created successfully or False otherwise.
459 """
460 if not self._create(base_dir):
461 logging.debug('Failed to create filesystem test object at "%s"',
462 os.path.join(base_dir, self._path))
463 return False
464 return True
465
466 def verify(self, base_dir):
467 """Verifies the test object in a base directory.
468
469 Args:
470 base_dir: The base directory where the test object is expected to be
471 found.
472
473 Returns:
474 True if the test object is found in the base directory and matches
475 the expected content, or False otherwise.
476 """
477 if not self._verify(base_dir):
478 logging.debug('Failed to verify filesystem test object at "%s"',
479 os.path.join(base_dir, self._path))
480 return False
481 return True
482
483 def _create(self, base_dir):
484 return False
485
486 def _verify(self, base_dir):
487 return False
488
489
490class FilesystemTestDirectory(FilesystemTestObject):
491 """A filesystem test object that represents a directory."""
492
493 def __init__(self, path, content, mode=stat.S_IRWXU|stat.S_IRGRP| \
494 stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH):
495 super(FilesystemTestDirectory, self).__init__(path, content, mode)
496
497 def _create(self, base_dir):
498 path = os.path.join(base_dir, self._path) if self._path else base_dir
499
500 if self._path:
501 with ExceptionSuppressor(OSError):
502 os.makedirs(path)
503 os.chmod(path, self._mode)
504
505 if not os.path.isdir(path):
506 return False
507
508 for content in self._content:
509 if not content.create(path):
510 return False
511 return True
512
513 def _verify(self, base_dir):
514 path = os.path.join(base_dir, self._path) if self._path else base_dir
515 if not os.path.isdir(path):
516 return False
517
518 for content in self._content:
519 if not content.verify(path):
520 return False
521 return True
522
523
524class FilesystemTestFile(FilesystemTestObject):
525 """A filesystem test object that represents a file."""
526
527 def __init__(self, path, content, mode=stat.S_IRUSR|stat.S_IWUSR| \
528 stat.S_IRGRP|stat.S_IROTH):
529 super(FilesystemTestFile, self).__init__(path, content, mode)
530
531 def _create(self, base_dir):
532 path = os.path.join(base_dir, self._path)
533 with ExceptionSuppressor(IOError):
534 with open(path, 'wb+') as f:
535 f.write(self._content)
Ben Chanf6a74c52013-02-19 14:06:17 -0800536 with ExceptionSuppressor(OSError):
537 os.chmod(path, self._mode)
Ben Chan0499e532011-08-29 10:53:18 -0700538 return True
539 return False
540
541 def _verify(self, base_dir):
542 path = os.path.join(base_dir, self._path)
543 with ExceptionSuppressor(IOError):
544 with open(path, 'rb') as f:
545 return f.read() == self._content
546 return False
547
548
549class DefaultFilesystemTestContent(FilesystemTestDirectory):
550 def __init__(self):
551 super(DefaultFilesystemTestContent, self).__init__('', [
552 FilesystemTestFile('file1', '0123456789'),
553 FilesystemTestDirectory('dir1', [
554 FilesystemTestFile('file1', ''),
555 FilesystemTestFile('file2', 'abcdefg'),
556 FilesystemTestDirectory('dir2', [
557 FilesystemTestFile('file3', 'abcdefg'),
558 ]),
559 ]),
560 ], stat.S_IRWXU|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH)
561
562
563class VirtualFilesystemImage(object):
564 def __init__(self, block_size, block_count, filesystem_type,
565 *args, **kwargs):
566 """Initializes the instance.
567
568 Args:
569 block_size: The number of bytes of each block in the image.
570 block_count: The number of blocks in the image.
571 filesystem_type: The filesystem type to be given to the mkfs
572 program for formatting the image.
573
574 Keyword Args:
575 mount_filesystem_type: The filesystem type to be given to the
576 mount program for mounting the image.
577 mkfs_options: A list of options to be given to the mkfs program.
578 """
579 self._block_size = block_size
580 self._block_count = block_count
581 self._filesystem_type = filesystem_type
582 self._mount_filesystem_type = kwargs.get('mount_filesystem_type')
583 if self._mount_filesystem_type is None:
584 self._mount_filesystem_type = filesystem_type
585 self._mkfs_options = kwargs.get('mkfs_options')
586 if self._mkfs_options is None:
587 self._mkfs_options = []
588 self._image_file = None
589 self._loop_device = None
Ben Chan33b5f042017-08-21 13:45:30 -0700590 self._loop_device_stat = None
Ben Chan0499e532011-08-29 10:53:18 -0700591 self._mount_dir = None
592
593 def __del__(self):
594 with ExceptionSuppressor(Exception):
595 self.clean()
596
597 def __enter__(self):
598 self.create()
599 return self
600
601 def __exit__(self, exc_type, exc_value, traceback):
602 self.clean()
603 return False
604
605 def _remove_temp_path(self, temp_path):
606 """Removes a temporary file or directory created using autotemp."""
607 if temp_path:
608 with ExceptionSuppressor(Exception):
609 path = temp_path.name
610 temp_path.clean()
611 logging.debug('Removed "%s"', path)
612
613 def _remove_image_file(self):
614 """Removes the image file if one has been created."""
615 self._remove_temp_path(self._image_file)
616 self._image_file = None
617
618 def _remove_mount_dir(self):
619 """Removes the mount directory if one has been created."""
620 self._remove_temp_path(self._mount_dir)
621 self._mount_dir = None
622
623 @property
624 def image_file(self):
625 """Gets the path of the image file.
626
627 Returns:
628 The path of the image file or None if no image file has been
629 created.
630 """
631 return self._image_file.name if self._image_file else None
632
633 @property
634 def loop_device(self):
635 """Gets the loop device where the image file is attached to.
636
637 Returns:
638 The path of the loop device where the image file is attached to or
639 None if no loop device is attaching the image file.
640 """
641 return self._loop_device
642
643 @property
644 def mount_dir(self):
645 """Gets the directory where the image file is mounted to.
646
647 Returns:
648 The directory where the image file is mounted to or None if no
649 mount directory has been created.
650 """
651 return self._mount_dir.name if self._mount_dir else None
652
653 def create(self):
654 """Creates a zero-filled image file with the specified size.
655
656 The created image file is temporary and removed when clean()
657 is called.
658 """
659 self.clean()
660 self._image_file = autotemp.tempfile(unique_id='fsImage')
661 try:
662 logging.debug('Creating zero-filled image file at "%s"',
663 self._image_file.name)
664 utils.run('dd if=/dev/zero of=%s bs=%s count=%s' %
665 (self._image_file.name, self._block_size,
666 self._block_count))
667 except error.CmdError as exc:
668 self._remove_image_file()
669 message = 'Failed to create filesystem image: %s' % exc
670 raise RuntimeError(message)
671
672 def clean(self):
673 """Removes the image file if one has been created.
674
675 Before removal, the image file is detached from the loop device that
676 it is attached to.
677 """
678 self.detach_from_loop_device()
679 self._remove_image_file()
680
681 def attach_to_loop_device(self):
682 """Attaches the created image file to a loop device.
683
684 Creates the image file, if one has not been created, by calling
685 create().
686
687 Returns:
688 The path of the loop device where the image file is attached to.
689 """
690 if self._loop_device:
691 return self._loop_device
692
693 if not self._image_file:
694 self.create()
695
696 logging.debug('Attaching image file "%s" to loop device',
697 self._image_file.name)
698 utils.run('losetup -f %s' % self._image_file.name)
699 output = utils.system_output('losetup -j %s' % self._image_file.name)
700 # output should look like: "/dev/loop0: [000d]:6329 (/tmp/test.img)"
701 self._loop_device = output.split(':')[0]
702 logging.debug('Attached image file "%s" to loop device "%s"',
703 self._image_file.name, self._loop_device)
Ben Chan33b5f042017-08-21 13:45:30 -0700704
705 self._loop_device_stat = os.stat(self._loop_device)
706 logging.debug('Loop device "%s" (uid=%d, gid=%d, permissions=%04o)',
707 self._loop_device,
708 self._loop_device_stat.st_uid,
709 self._loop_device_stat.st_gid,
710 stat.S_IMODE(self._loop_device_stat.st_mode))
Ben Chan0499e532011-08-29 10:53:18 -0700711 return self._loop_device
712
713 def detach_from_loop_device(self):
714 """Detaches the image file from the loop device."""
715 if not self._loop_device:
716 return
717
718 self.unmount()
719
720 logging.debug('Cleaning up remaining mount points of loop device "%s"',
721 self._loop_device)
722 utils.run('umount -f %s' % self._loop_device, ignore_status=True)
723
Ben Chan33b5f042017-08-21 13:45:30 -0700724 logging.debug('Restore ownership/permissions of loop device "%s"',
725 self._loop_device)
726 os.chmod(self._loop_device,
727 stat.S_IMODE(self._loop_device_stat.st_mode))
728 os.chown(self._loop_device,
729 self._loop_device_stat.st_uid, self._loop_device_stat.st_gid)
730
Ben Chan0499e532011-08-29 10:53:18 -0700731 logging.debug('Detaching image file "%s" from loop device "%s"',
732 self._image_file.name, self._loop_device)
733 utils.run('losetup -d %s' % self._loop_device)
734 self._loop_device = None
735
736 def format(self):
737 """Formats the image file as the specified filesystem."""
738 self.attach_to_loop_device()
739 try:
740 logging.debug('Formatting image file at "%s" as "%s" filesystem',
741 self._image_file.name, self._filesystem_type)
742 utils.run('yes | mkfs -t %s %s %s' %
743 (self._filesystem_type, ' '.join(self._mkfs_options),
744 self._loop_device))
745 logging.debug('blkid: %s', utils.system_output(
746 'blkid -c /dev/null %s' % self._loop_device,
747 ignore_status=True))
748 except error.CmdError as exc:
749 message = 'Failed to format filesystem image: %s' % exc
750 raise RuntimeError(message)
751
752 def mount(self, options=None):
753 """Mounts the image file to a directory.
754
755 Args:
756 options: An optional list of mount options.
757 """
758 if self._mount_dir:
759 return self._mount_dir.name
760
761 if options is None:
762 options = []
763
764 options_arg = ','.join(options)
765 if options_arg:
766 options_arg = '-o ' + options_arg
767
768 self.attach_to_loop_device()
769 self._mount_dir = autotemp.tempdir(unique_id='fsImage')
770 try:
771 logging.debug('Mounting image file "%s" (%s) to directory "%s"',
772 self._image_file.name, self._loop_device,
773 self._mount_dir.name)
774 utils.run('mount -t %s %s %s %s' %
775 (self._mount_filesystem_type, options_arg,
776 self._loop_device, self._mount_dir.name))
777 except error.CmdError as exc:
778 self._remove_mount_dir()
779 message = ('Failed to mount virtual filesystem image "%s": %s' %
780 (self._image_file.name, exc))
781 raise RuntimeError(message)
782 return self._mount_dir.name
783
784 def unmount(self):
785 """Unmounts the image file from the mounted directory."""
786 if not self._mount_dir:
787 return
788
789 try:
790 logging.debug('Unmounting image file "%s" (%s) from directory "%s"',
791 self._image_file.name, self._loop_device,
792 self._mount_dir.name)
793 utils.run('umount %s' % self._mount_dir.name)
794 except error.CmdError as exc:
795 message = ('Failed to unmount virtual filesystem image "%s": %s' %
796 (self._image_file.name, exc))
797 raise RuntimeError(message)
798 finally:
799 self._remove_mount_dir()
Klemen Kozjek7740bfd2017-08-15 16:40:30 +0900800
801 def get_volume_label(self):
802 """Gets volume name information of |self._loop_device|
803
804 @return a string with volume name if it exists.
805 """
806 # This script is run as root in a normal autotest run,
807 # so this works: It doesn't have access to the necessary info
808 # when run as a non-privileged user
809 cmd = "blkid -c /dev/null -o udev %s" % self._loop_device
810 output = utils.system_output(cmd, ignore_status=True)
811
812 for line in output.splitlines():
813 udev_key, udev_val = line.split('=')
814
815 if udev_key == 'ID_FS_LABEL':
816 return udev_val
817
818 return None