blob: 7b387699a08986ef8f472eaeeb091b9ede1bce12 [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
Lutz Justen1c6be452018-05-29 13:37:00 +020011from autotest_lib.client.cros import dbus_util
barfab@chromium.orgb6d29932012-04-11 09:46:43 +020012from mainloop import ExceptionForward
13from mainloop import GenericTesterMainLoop
Ben Chan0499e532011-08-29 10:53:18 -070014
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 """
Lutz Justen1c6be452018-05-29 13:37:00 +020059
60 def __init__(self, main_loop, bus, bus_name, object_path, timeout=None):
Ben Chan0499e532011-08-29 10:53:18 -070061 """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.
Lutz Justen1c6be452018-05-29 13:37:00 +020068 timeout: Maximum time in seconds to wait for the DBus connection.
Ben Chan0499e532011-08-29 10:53:18 -070069 """
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)
Lutz Justen1c6be452018-05-29 13:37:00 +020075 self.proxy_object = dbus_util.get_dbus_object(bus, bus_name,
76 object_path, timeout)
Ben Chan0499e532011-08-29 10:53:18 -070077
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):
Ben Chan81904f12011-11-21 17:20:18 -0800120 """Waits for the reception of a signal.
121
122 Args:
123 signal_name: The name of the signal to wait for.
Ben Chan0499e532011-08-29 10:53:18 -0700124
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
Ben Chan81904f12011-11-21 17:20:18 -0800146 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
Ben Chan0499e532011-08-29 10:53:18 -0700177
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'
Ben Chan81904f12011-11-21 17:20:18 -0800186 FORMAT_COMPLETED_SIGNAL = 'FormatCompleted'
187 FORMAT_COMPLETED_SIGNAL_ARGUMENTS = (
188 'status', 'path'
189 )
Ben Chan0499e532011-08-29 10:53:18 -0700190 MOUNT_COMPLETED_SIGNAL = 'MountCompleted'
191 MOUNT_COMPLETED_SIGNAL_ARGUMENTS = (
192 'status', 'source_path', 'source_type', 'mount_path'
193 )
Klemen Kozjek7740bfd2017-08-15 16:40:30 +0900194 RENAME_COMPLETED_SIGNAL = 'RenameCompleted'
195 RENAME_COMPLETED_SIGNAL_ARGUMENTS = (
196 'status', 'path'
197 )
Ben Chan0499e532011-08-29 10:53:18 -0700198
Anand K Mistry33812262019-01-08 15:38:21 +1100199 def __init__(self, main_loop, bus, timeout_seconds=None):
Ben Chan0499e532011-08-29 10:53:18 -0700200 """Initializes the instance.
201
202 Args:
203 main_loop: The GLib main loop.
204 bus: The bus where the DBus server is connected to.
Anand K Mistry33812262019-01-08 15:38:21 +1100205 timeout_seconds: Maximum time in seconds to wait for the DBus
206 connection.
Ben Chan0499e532011-08-29 10:53:18 -0700207 """
208 super(CrosDisksClient, self).__init__(main_loop, bus,
209 self.CROS_DISKS_BUS_NAME,
Anand K Mistry33812262019-01-08 15:38:21 +1100210 self.CROS_DISKS_OBJECT_PATH,
211 timeout_seconds)
Ben Chan0499e532011-08-29 10:53:18 -0700212 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,
Ben Chan81904f12011-11-21 17:20:18 -0800217 self.FORMAT_COMPLETED_SIGNAL,
218 self.FORMAT_COMPLETED_SIGNAL_ARGUMENTS)
219 self.handle_signal(self.CROS_DISKS_INTERFACE,
Ben Chan0499e532011-08-29 10:53:18 -0700220 self.MOUNT_COMPLETED_SIGNAL,
221 self.MOUNT_COMPLETED_SIGNAL_ARGUMENTS)
Klemen Kozjek7740bfd2017-08-15 16:40:30 +0900222 self.handle_signal(self.CROS_DISKS_INTERFACE,
223 self.RENAME_COMPLETED_SIGNAL,
224 self.RENAME_COMPLETED_SIGNAL_ARGUMENTS)
Ben Chan0499e532011-08-29 10:53:18 -0700225
Ben Chan0499e532011-08-29 10:53:18 -0700226 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
Ben Chan81904f12011-11-21 17:20:18 -0800246 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)
Ben Chan577e66b2017-10-12 19:18:03 -0700259 self.interface.Format(path, filesystem_type,
260 dbus.Array(options, signature='s'))
Ben Chan81904f12011-11-21 17:20:18 -0800261
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
Klemen Kozjek7740bfd2017-08-15 16:40:30 +0900290 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
Ben Chan0499e532011-08-29 10:53:18 -0700330 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)
Ben Chan577e66b2017-10-12 19:18:03 -0700343 self.interface.Mount(path, filesystem_type,
344 dbus.Array(options, signature='s'))
Ben Chan0499e532011-08-29 10:53:18 -0700345
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.
Anand K Mistry966caf72018-08-15 15:02:41 +1000352
353 Returns:
354 The mount error code.
Ben Chan0499e532011-08-29 10:53:18 -0700355 """
356 if options is None:
357 options = []
Anand K Mistry966caf72018-08-15 15:02:41 +1000358 return self.interface.Unmount(path, dbus.Array(options, signature='s'))
Ben Chan0499e532011-08-29 10:53:18 -0700359
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
Ben Chan0499e532011-08-29 10:53:18 -0700380 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 """
Ben Chan81904f12011-11-21 17:20:18 -0800385 return self.expect_signal(self.MOUNT_COMPLETED_SIGNAL,
386 expected_content)
Ben Chan0499e532011-08-29 10:53:18 -0700387
Sergei Datsenkof97bc2a2020-09-21 23:50:59 +1000388 def add_loopback_to_allowlist(self, path):
389 """Adds a device by its path to the allowlist for testing.
390
391 Args:
392 path: path to the /dev/loopX device.
393 """
394 sys_path = '/sys/devices/virtual/block/' + os.path.basename(path)
395 self.interface.AddDeviceToAllowlist(sys_path)
396
397 def remove_loopback_from_allowlist(self, path):
398 """Removes a device by its sys path from the allowlist for testing.
399
400 Args:
401 path: path to the /dev/loopX device.
402 """
403 sys_path = '/sys/devices/virtual/block/' + os.path.basename(path)
404 self.interface.RemoveDeviceFromAllowlist(sys_path)
405
Ben Chan0499e532011-08-29 10:53:18 -0700406
407class CrosDisksTester(GenericTesterMainLoop):
408 """A base tester class for testing the CrosDisks server.
409
410 A derived class should override the get_tests method to return a list of
411 test methods. The perform_one_test method invokes each test method in the
412 list to verify some functionalities of CrosDisks server.
413 """
414 def __init__(self, test):
415 bus_loop = DBusGMainLoop(set_as_default=True)
Anand K Mistry33812262019-01-08 15:38:21 +1100416 self.bus = dbus.SystemBus(mainloop=bus_loop)
Ben Chan0499e532011-08-29 10:53:18 -0700417 self.main_loop = gobject.MainLoop()
418 super(CrosDisksTester, self).__init__(test, self.main_loop)
Anand K Mistry33812262019-01-08 15:38:21 +1100419 self.cros_disks = CrosDisksClient(self.main_loop, self.bus)
Ben Chan0499e532011-08-29 10:53:18 -0700420
421 def get_tests(self):
422 """Returns a list of test methods to be invoked by perform_one_test.
423
424 A derived class should override this method.
425
426 Returns:
427 A list of test methods.
428 """
429 return []
430
431 @ExceptionForward
432 def perform_one_test(self):
433 """Exercises each test method in the list returned by get_tests.
434 """
435 tests = self.get_tests()
436 self.remaining_requirements = set([test.func_name for test in tests])
437 for test in tests:
438 test()
439 self.requirement_completed(test.func_name)
440
Anand K Mistry33812262019-01-08 15:38:21 +1100441 def reconnect_client(self, timeout_seconds=None):
François Degros2cf82be2020-09-07 17:29:32 +1000442 """"Reconnect the CrosDisks DBus client.
Anand K Mistry33812262019-01-08 15:38:21 +1100443
François Degros2cf82be2020-09-07 17:29:32 +1000444 Args:
445 timeout_seconds: Maximum time in seconds to wait for the DBus
446 connection.
447 """
448 self.cros_disks = CrosDisksClient(self.main_loop, self.bus,
449 timeout_seconds)
Anand K Mistry33812262019-01-08 15:38:21 +1100450
Ben Chan0499e532011-08-29 10:53:18 -0700451
452class FilesystemTestObject(object):
453 """A base class to represent a filesystem test object.
454
455 A filesystem test object can be a file, directory or symbolic link.
456 A derived class should override the _create and _verify method to implement
457 how the test object should be created and verified, respectively, on a
458 filesystem.
459 """
460 def __init__(self, path, content, mode):
461 """Initializes the instance.
462
463 Args:
464 path: The relative path of the test object.
465 content: The content of the test object.
466 mode: The file permissions given to the test object.
467 """
468 self._path = path
469 self._content = content
470 self._mode = mode
471
472 def create(self, base_dir):
473 """Creates the test object in a base directory.
474
475 Args:
476 base_dir: The base directory where the test object is created.
477
478 Returns:
479 True if the test object is created successfully or False otherwise.
480 """
481 if not self._create(base_dir):
482 logging.debug('Failed to create filesystem test object at "%s"',
483 os.path.join(base_dir, self._path))
484 return False
485 return True
486
487 def verify(self, base_dir):
488 """Verifies the test object in a base directory.
489
490 Args:
491 base_dir: The base directory where the test object is expected to be
492 found.
493
494 Returns:
495 True if the test object is found in the base directory and matches
496 the expected content, or False otherwise.
497 """
498 if not self._verify(base_dir):
François Degros2cf82be2020-09-07 17:29:32 +1000499 logging.error('Mismatched filesystem object at "%s"',
Ben Chan0499e532011-08-29 10:53:18 -0700500 os.path.join(base_dir, self._path))
501 return False
502 return True
503
504 def _create(self, base_dir):
505 return False
506
507 def _verify(self, base_dir):
508 return False
509
510
511class FilesystemTestDirectory(FilesystemTestObject):
512 """A filesystem test object that represents a directory."""
513
514 def __init__(self, path, content, mode=stat.S_IRWXU|stat.S_IRGRP| \
François Degros2cf82be2020-09-07 17:29:32 +1000515 stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH, strict=False):
516 """Initializes the directory.
517
518 Args:
519 path: The name of this directory.
520 content: The list of items in this directory.
521 mode: The file permissions given to this directory.
522 strict: Whether verify() strictly compares directory contents for
523 equality. This flag only applies to this directory, and not
524 to any child directories.
525 """
Ben Chan0499e532011-08-29 10:53:18 -0700526 super(FilesystemTestDirectory, self).__init__(path, content, mode)
François Degros2cf82be2020-09-07 17:29:32 +1000527 self._strict = strict
Ben Chan0499e532011-08-29 10:53:18 -0700528
529 def _create(self, base_dir):
530 path = os.path.join(base_dir, self._path) if self._path else base_dir
531
532 if self._path:
533 with ExceptionSuppressor(OSError):
534 os.makedirs(path)
535 os.chmod(path, self._mode)
536
537 if not os.path.isdir(path):
538 return False
539
540 for content in self._content:
541 if not content.create(path):
542 return False
François Degros2cf82be2020-09-07 17:29:32 +1000543
Ben Chan0499e532011-08-29 10:53:18 -0700544 return True
545
546 def _verify(self, base_dir):
547 path = os.path.join(base_dir, self._path) if self._path else base_dir
548 if not os.path.isdir(path):
549 return False
550
François Degros95be77c2020-10-15 11:28:45 +1100551 result = True
François Degros2cf82be2020-09-07 17:29:32 +1000552 seen = set()
François Degros95be77c2020-10-15 11:28:45 +1100553
Ben Chan0499e532011-08-29 10:53:18 -0700554 for content in self._content:
555 if not content.verify(path):
François Degros95be77c2020-10-15 11:28:45 +1100556 result = False
François Degros2cf82be2020-09-07 17:29:32 +1000557 seen.add(content._path)
558
559 if self._strict:
560 for child in os.listdir(path):
561 if child not in seen:
562 logging.error('Unexpected filesystem entry "%s"',
563 os.path.join(path, child))
François Degros95be77c2020-10-15 11:28:45 +1100564 result = False
François Degros2cf82be2020-09-07 17:29:32 +1000565
François Degros95be77c2020-10-15 11:28:45 +1100566 return result
Ben Chan0499e532011-08-29 10:53:18 -0700567
568
569class FilesystemTestFile(FilesystemTestObject):
570 """A filesystem test object that represents a file."""
571
François Degros95be77c2020-10-15 11:28:45 +1100572 def __init__(self,
573 path,
574 content,
575 mode=stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP \
576 | stat.S_IROTH,
577 mtime=None):
578 """Initializes the file.
579
580 Args:
581 path: The name of this file.
582 content: A byte string with the expected file contents.
583 mode: The file permissions given to this file.
584 mtime: If set, the expected file modification timestamp.
585 """
Ben Chan0499e532011-08-29 10:53:18 -0700586 super(FilesystemTestFile, self).__init__(path, content, mode)
François Degros95be77c2020-10-15 11:28:45 +1100587 self._mtime = mtime
Ben Chan0499e532011-08-29 10:53:18 -0700588
589 def _create(self, base_dir):
590 path = os.path.join(base_dir, self._path)
591 with ExceptionSuppressor(IOError):
592 with open(path, 'wb+') as f:
593 f.write(self._content)
Ben Chanf6a74c52013-02-19 14:06:17 -0800594 with ExceptionSuppressor(OSError):
595 os.chmod(path, self._mode)
Ben Chan0499e532011-08-29 10:53:18 -0700596 return True
597 return False
598
599 def _verify(self, base_dir):
600 path = os.path.join(base_dir, self._path)
601 with ExceptionSuppressor(IOError):
François Degros95be77c2020-10-15 11:28:45 +1100602 result = True
603
604 if self._content is not None:
605 with open(path, 'rb') as f:
606 if f.read() != self._content:
607 logging.error('Mismatched file contents for "%s"',
608 path)
609 result = False
610
611 if self._mtime is not None:
612 st = os.stat(path)
613 if st.st_mtime != self._mtime:
614 logging.error(
615 'Mismatched file modification time for "%s": ' +
616 'want %d, got %d', path, self._mtime, st.st_mtime)
617 result = False
618
619 return result
620
Ben Chan0499e532011-08-29 10:53:18 -0700621 return False
622
623
624class DefaultFilesystemTestContent(FilesystemTestDirectory):
625 def __init__(self):
626 super(DefaultFilesystemTestContent, self).__init__('', [
627 FilesystemTestFile('file1', '0123456789'),
628 FilesystemTestDirectory('dir1', [
629 FilesystemTestFile('file1', ''),
630 FilesystemTestFile('file2', 'abcdefg'),
631 FilesystemTestDirectory('dir2', [
632 FilesystemTestFile('file3', 'abcdefg'),
Anand K Mistrye00f8a42018-08-21 11:09:54 +1000633 FilesystemTestFile('file4', 'a' * 65536),
Ben Chan0499e532011-08-29 10:53:18 -0700634 ]),
635 ]),
636 ], stat.S_IRWXU|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH)
637
638
639class VirtualFilesystemImage(object):
640 def __init__(self, block_size, block_count, filesystem_type,
641 *args, **kwargs):
642 """Initializes the instance.
643
644 Args:
645 block_size: The number of bytes of each block in the image.
646 block_count: The number of blocks in the image.
647 filesystem_type: The filesystem type to be given to the mkfs
648 program for formatting the image.
649
650 Keyword Args:
651 mount_filesystem_type: The filesystem type to be given to the
652 mount program for mounting the image.
653 mkfs_options: A list of options to be given to the mkfs program.
654 """
655 self._block_size = block_size
656 self._block_count = block_count
657 self._filesystem_type = filesystem_type
658 self._mount_filesystem_type = kwargs.get('mount_filesystem_type')
659 if self._mount_filesystem_type is None:
660 self._mount_filesystem_type = filesystem_type
661 self._mkfs_options = kwargs.get('mkfs_options')
662 if self._mkfs_options is None:
663 self._mkfs_options = []
664 self._image_file = None
665 self._loop_device = None
Ben Chan33b5f042017-08-21 13:45:30 -0700666 self._loop_device_stat = None
Ben Chan0499e532011-08-29 10:53:18 -0700667 self._mount_dir = None
668
669 def __del__(self):
670 with ExceptionSuppressor(Exception):
671 self.clean()
672
673 def __enter__(self):
674 self.create()
675 return self
676
677 def __exit__(self, exc_type, exc_value, traceback):
678 self.clean()
679 return False
680
681 def _remove_temp_path(self, temp_path):
682 """Removes a temporary file or directory created using autotemp."""
683 if temp_path:
684 with ExceptionSuppressor(Exception):
685 path = temp_path.name
686 temp_path.clean()
687 logging.debug('Removed "%s"', path)
688
689 def _remove_image_file(self):
690 """Removes the image file if one has been created."""
691 self._remove_temp_path(self._image_file)
692 self._image_file = None
693
694 def _remove_mount_dir(self):
695 """Removes the mount directory if one has been created."""
696 self._remove_temp_path(self._mount_dir)
697 self._mount_dir = None
698
699 @property
700 def image_file(self):
701 """Gets the path of the image file.
702
703 Returns:
704 The path of the image file or None if no image file has been
705 created.
706 """
707 return self._image_file.name if self._image_file else None
708
709 @property
710 def loop_device(self):
711 """Gets the loop device where the image file is attached to.
712
713 Returns:
714 The path of the loop device where the image file is attached to or
715 None if no loop device is attaching the image file.
716 """
717 return self._loop_device
718
719 @property
720 def mount_dir(self):
721 """Gets the directory where the image file is mounted to.
722
723 Returns:
724 The directory where the image file is mounted to or None if no
725 mount directory has been created.
726 """
727 return self._mount_dir.name if self._mount_dir else None
728
729 def create(self):
730 """Creates a zero-filled image file with the specified size.
731
732 The created image file is temporary and removed when clean()
733 is called.
734 """
735 self.clean()
736 self._image_file = autotemp.tempfile(unique_id='fsImage')
737 try:
738 logging.debug('Creating zero-filled image file at "%s"',
739 self._image_file.name)
740 utils.run('dd if=/dev/zero of=%s bs=%s count=%s' %
741 (self._image_file.name, self._block_size,
742 self._block_count))
743 except error.CmdError as exc:
744 self._remove_image_file()
745 message = 'Failed to create filesystem image: %s' % exc
746 raise RuntimeError(message)
747
748 def clean(self):
749 """Removes the image file if one has been created.
750
751 Before removal, the image file is detached from the loop device that
752 it is attached to.
753 """
754 self.detach_from_loop_device()
755 self._remove_image_file()
756
757 def attach_to_loop_device(self):
758 """Attaches the created image file to a loop device.
759
760 Creates the image file, if one has not been created, by calling
761 create().
762
763 Returns:
764 The path of the loop device where the image file is attached to.
765 """
766 if self._loop_device:
767 return self._loop_device
768
769 if not self._image_file:
770 self.create()
771
772 logging.debug('Attaching image file "%s" to loop device',
773 self._image_file.name)
774 utils.run('losetup -f %s' % self._image_file.name)
775 output = utils.system_output('losetup -j %s' % self._image_file.name)
776 # output should look like: "/dev/loop0: [000d]:6329 (/tmp/test.img)"
777 self._loop_device = output.split(':')[0]
778 logging.debug('Attached image file "%s" to loop device "%s"',
779 self._image_file.name, self._loop_device)
Ben Chan33b5f042017-08-21 13:45:30 -0700780
781 self._loop_device_stat = os.stat(self._loop_device)
782 logging.debug('Loop device "%s" (uid=%d, gid=%d, permissions=%04o)',
783 self._loop_device,
784 self._loop_device_stat.st_uid,
785 self._loop_device_stat.st_gid,
786 stat.S_IMODE(self._loop_device_stat.st_mode))
Ben Chan0499e532011-08-29 10:53:18 -0700787 return self._loop_device
788
789 def detach_from_loop_device(self):
790 """Detaches the image file from the loop device."""
791 if not self._loop_device:
792 return
793
794 self.unmount()
795
796 logging.debug('Cleaning up remaining mount points of loop device "%s"',
797 self._loop_device)
798 utils.run('umount -f %s' % self._loop_device, ignore_status=True)
799
Ben Chan33b5f042017-08-21 13:45:30 -0700800 logging.debug('Restore ownership/permissions of loop device "%s"',
801 self._loop_device)
802 os.chmod(self._loop_device,
803 stat.S_IMODE(self._loop_device_stat.st_mode))
804 os.chown(self._loop_device,
805 self._loop_device_stat.st_uid, self._loop_device_stat.st_gid)
806
Ben Chan0499e532011-08-29 10:53:18 -0700807 logging.debug('Detaching image file "%s" from loop device "%s"',
808 self._image_file.name, self._loop_device)
809 utils.run('losetup -d %s' % self._loop_device)
810 self._loop_device = None
811
812 def format(self):
813 """Formats the image file as the specified filesystem."""
814 self.attach_to_loop_device()
815 try:
816 logging.debug('Formatting image file at "%s" as "%s" filesystem',
817 self._image_file.name, self._filesystem_type)
818 utils.run('yes | mkfs -t %s %s %s' %
819 (self._filesystem_type, ' '.join(self._mkfs_options),
820 self._loop_device))
821 logging.debug('blkid: %s', utils.system_output(
822 'blkid -c /dev/null %s' % self._loop_device,
823 ignore_status=True))
824 except error.CmdError as exc:
825 message = 'Failed to format filesystem image: %s' % exc
826 raise RuntimeError(message)
827
828 def mount(self, options=None):
829 """Mounts the image file to a directory.
830
831 Args:
832 options: An optional list of mount options.
833 """
834 if self._mount_dir:
835 return self._mount_dir.name
836
837 if options is None:
838 options = []
839
840 options_arg = ','.join(options)
841 if options_arg:
842 options_arg = '-o ' + options_arg
843
844 self.attach_to_loop_device()
845 self._mount_dir = autotemp.tempdir(unique_id='fsImage')
846 try:
847 logging.debug('Mounting image file "%s" (%s) to directory "%s"',
848 self._image_file.name, self._loop_device,
849 self._mount_dir.name)
850 utils.run('mount -t %s %s %s %s' %
851 (self._mount_filesystem_type, options_arg,
852 self._loop_device, self._mount_dir.name))
853 except error.CmdError as exc:
854 self._remove_mount_dir()
855 message = ('Failed to mount virtual filesystem image "%s": %s' %
856 (self._image_file.name, exc))
857 raise RuntimeError(message)
858 return self._mount_dir.name
859
860 def unmount(self):
861 """Unmounts the image file from the mounted directory."""
862 if not self._mount_dir:
863 return
864
865 try:
866 logging.debug('Unmounting image file "%s" (%s) from directory "%s"',
867 self._image_file.name, self._loop_device,
868 self._mount_dir.name)
869 utils.run('umount %s' % self._mount_dir.name)
870 except error.CmdError as exc:
871 message = ('Failed to unmount virtual filesystem image "%s": %s' %
872 (self._image_file.name, exc))
873 raise RuntimeError(message)
874 finally:
875 self._remove_mount_dir()
Klemen Kozjek7740bfd2017-08-15 16:40:30 +0900876
877 def get_volume_label(self):
878 """Gets volume name information of |self._loop_device|
879
880 @return a string with volume name if it exists.
881 """
882 # This script is run as root in a normal autotest run,
883 # so this works: It doesn't have access to the necessary info
884 # when run as a non-privileged user
885 cmd = "blkid -c /dev/null -o udev %s" % self._loop_device
886 output = utils.system_output(cmd, ignore_status=True)
887
888 for line in output.splitlines():
889 udev_key, udev_val = line.split('=')
890
891 if udev_key == 'ID_FS_LABEL':
892 return udev_val
893
894 return None