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