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