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