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