blob: d7f6ffe375b491839f03e17f89c530a65dd646c6 [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
541 self._mount_dir = None
542
543 def __del__(self):
544 with ExceptionSuppressor(Exception):
545 self.clean()
546
547 def __enter__(self):
548 self.create()
549 return self
550
551 def __exit__(self, exc_type, exc_value, traceback):
552 self.clean()
553 return False
554
555 def _remove_temp_path(self, temp_path):
556 """Removes a temporary file or directory created using autotemp."""
557 if temp_path:
558 with ExceptionSuppressor(Exception):
559 path = temp_path.name
560 temp_path.clean()
561 logging.debug('Removed "%s"', path)
562
563 def _remove_image_file(self):
564 """Removes the image file if one has been created."""
565 self._remove_temp_path(self._image_file)
566 self._image_file = None
567
568 def _remove_mount_dir(self):
569 """Removes the mount directory if one has been created."""
570 self._remove_temp_path(self._mount_dir)
571 self._mount_dir = None
572
573 @property
574 def image_file(self):
575 """Gets the path of the image file.
576
577 Returns:
578 The path of the image file or None if no image file has been
579 created.
580 """
581 return self._image_file.name if self._image_file else None
582
583 @property
584 def loop_device(self):
585 """Gets the loop device where the image file is attached to.
586
587 Returns:
588 The path of the loop device where the image file is attached to or
589 None if no loop device is attaching the image file.
590 """
591 return self._loop_device
592
593 @property
594 def mount_dir(self):
595 """Gets the directory where the image file is mounted to.
596
597 Returns:
598 The directory where the image file is mounted to or None if no
599 mount directory has been created.
600 """
601 return self._mount_dir.name if self._mount_dir else None
602
603 def create(self):
604 """Creates a zero-filled image file with the specified size.
605
606 The created image file is temporary and removed when clean()
607 is called.
608 """
609 self.clean()
610 self._image_file = autotemp.tempfile(unique_id='fsImage')
611 try:
612 logging.debug('Creating zero-filled image file at "%s"',
613 self._image_file.name)
614 utils.run('dd if=/dev/zero of=%s bs=%s count=%s' %
615 (self._image_file.name, self._block_size,
616 self._block_count))
617 except error.CmdError as exc:
618 self._remove_image_file()
619 message = 'Failed to create filesystem image: %s' % exc
620 raise RuntimeError(message)
621
622 def clean(self):
623 """Removes the image file if one has been created.
624
625 Before removal, the image file is detached from the loop device that
626 it is attached to.
627 """
628 self.detach_from_loop_device()
629 self._remove_image_file()
630
631 def attach_to_loop_device(self):
632 """Attaches the created image file to a loop device.
633
634 Creates the image file, if one has not been created, by calling
635 create().
636
637 Returns:
638 The path of the loop device where the image file is attached to.
639 """
640 if self._loop_device:
641 return self._loop_device
642
643 if not self._image_file:
644 self.create()
645
646 logging.debug('Attaching image file "%s" to loop device',
647 self._image_file.name)
648 utils.run('losetup -f %s' % self._image_file.name)
649 output = utils.system_output('losetup -j %s' % self._image_file.name)
650 # output should look like: "/dev/loop0: [000d]:6329 (/tmp/test.img)"
651 self._loop_device = output.split(':')[0]
652 logging.debug('Attached image file "%s" to loop device "%s"',
653 self._image_file.name, self._loop_device)
654 return self._loop_device
655
656 def detach_from_loop_device(self):
657 """Detaches the image file from the loop device."""
658 if not self._loop_device:
659 return
660
661 self.unmount()
662
663 logging.debug('Cleaning up remaining mount points of loop device "%s"',
664 self._loop_device)
665 utils.run('umount -f %s' % self._loop_device, ignore_status=True)
666
667 logging.debug('Detaching image file "%s" from loop device "%s"',
668 self._image_file.name, self._loop_device)
669 utils.run('losetup -d %s' % self._loop_device)
670 self._loop_device = None
671
672 def format(self):
673 """Formats the image file as the specified filesystem."""
674 self.attach_to_loop_device()
675 try:
676 logging.debug('Formatting image file at "%s" as "%s" filesystem',
677 self._image_file.name, self._filesystem_type)
678 utils.run('yes | mkfs -t %s %s %s' %
679 (self._filesystem_type, ' '.join(self._mkfs_options),
680 self._loop_device))
681 logging.debug('blkid: %s', utils.system_output(
682 'blkid -c /dev/null %s' % self._loop_device,
683 ignore_status=True))
684 except error.CmdError as exc:
685 message = 'Failed to format filesystem image: %s' % exc
686 raise RuntimeError(message)
687
688 def mount(self, options=None):
689 """Mounts the image file to a directory.
690
691 Args:
692 options: An optional list of mount options.
693 """
694 if self._mount_dir:
695 return self._mount_dir.name
696
697 if options is None:
698 options = []
699
700 options_arg = ','.join(options)
701 if options_arg:
702 options_arg = '-o ' + options_arg
703
704 self.attach_to_loop_device()
705 self._mount_dir = autotemp.tempdir(unique_id='fsImage')
706 try:
707 logging.debug('Mounting image file "%s" (%s) to directory "%s"',
708 self._image_file.name, self._loop_device,
709 self._mount_dir.name)
710 utils.run('mount -t %s %s %s %s' %
711 (self._mount_filesystem_type, options_arg,
712 self._loop_device, self._mount_dir.name))
713 except error.CmdError as exc:
714 self._remove_mount_dir()
715 message = ('Failed to mount virtual filesystem image "%s": %s' %
716 (self._image_file.name, exc))
717 raise RuntimeError(message)
718 return self._mount_dir.name
719
720 def unmount(self):
721 """Unmounts the image file from the mounted directory."""
722 if not self._mount_dir:
723 return
724
725 try:
726 logging.debug('Unmounting image file "%s" (%s) from directory "%s"',
727 self._image_file.name, self._loop_device,
728 self._mount_dir.name)
729 utils.run('umount %s' % self._mount_dir.name)
730 except error.CmdError as exc:
731 message = ('Failed to unmount virtual filesystem image "%s": %s' %
732 (self._image_file.name, exc))
733 raise RuntimeError(message)
734 finally:
735 self._remove_mount_dir()