Home | History | Annotate | Download | only in cros
      1 # Copyright (c) 2012 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 
      5 import dbus, gobject, logging, os, stat
      6 from dbus.mainloop.glib import DBusGMainLoop
      7 
      8 import common
      9 from autotest_lib.client.bin import utils
     10 from autotest_lib.client.common_lib import autotemp, error
     11 from mainloop import ExceptionForward
     12 from mainloop import GenericTesterMainLoop
     13 
     14 
     15 """This module contains several helper classes for writing tests to verify the
     16 CrosDisks DBus interface. In particular, the CrosDisksTester class can be used
     17 to derive functional tests that interact with the CrosDisks server over DBus.
     18 """
     19 
     20 
     21 class 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 
     52 class 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):
    116         """Waits for the reception of a signal.
    117 
    118         Args:
    119             signal_name: The name of the signal to wait for.
    120 
    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 
    142     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 
    173 
    174 class 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'
    182     FORMAT_COMPLETED_SIGNAL = 'FormatCompleted'
    183     FORMAT_COMPLETED_SIGNAL_ARGUMENTS = (
    184         'status', 'path'
    185     )
    186     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,
    206                            self.FORMAT_COMPLETED_SIGNAL,
    207                            self.FORMAT_COMPLETED_SIGNAL_ARGUMENTS)
    208         self.handle_signal(self.CROS_DISKS_INTERFACE,
    209                            self.MOUNT_COMPLETED_SIGNAL,
    210                            self.MOUNT_COMPLETED_SIGNAL_ARGUMENTS)
    211 
    212     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 
    249     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 
    292     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 
    338         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         """
    343         return self.expect_signal(self.MOUNT_COMPLETED_SIGNAL,
    344                                   expected_content)
    345 
    346 
    347 class 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 
    382 class 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 
    441 class 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 
    475 class 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)
    487             with ExceptionSuppressor(OSError):
    488                 os.chmod(path, self._mode)
    489             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 
    500 class 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 
    514 class 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()
    736