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