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