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