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 """Storage device utilities to be used in storage device based tests
      6 """
      7 
      8 import logging, re, os, time, hashlib
      9 
     10 from autotest_lib.client.bin import test, utils
     11 from autotest_lib.client.common_lib import error
     12 from autotest_lib.client.cros import liststorage
     13 
     14 
     15 class StorageException(error.TestError):
     16     """Indicates that a storage/volume operation failed.
     17     It is fatal to the test unless caught.
     18     """
     19     pass
     20 
     21 
     22 class StorageScanner(object):
     23     """Scan device for storage points.
     24 
     25     It also performs basic operations on found storage devices as mount/umount,
     26     creating file with randomized content or checksum file content.
     27 
     28     Each storage device is defined by a dictionary containing the following
     29     keys:
     30 
     31     device: the device path (e.g. /dev/sdb1)
     32     bus: the bus name (e.g. usb, ata, etc)
     33     model: the kind of device (e.g. Multi-Card, USB_DISK_2.0, SanDisk)
     34     size: the size of the volume/partition ib bytes (int)
     35     fs_uuid: the UUID for the filesystem (str)
     36     fstype: filesystem type
     37     is_mounted: wether the FS is mounted (0=False,1=True)
     38     mountpoint: where the FS is mounted (if mounted=1) or a suggestion where to
     39                 mount it (if mounted=0)
     40 
     41     Also |filter()| and |scan()| will use the same dictionary keys associated
     42     with regular expression in order to filter a result set.
     43     Multiple keys act in an AND-fashion way. The absence of a key in the filter
     44     make the filter matching all the values for said key in the storage
     45     dictionary.
     46 
     47     Example: {'device':'/dev/sd[ab]1', 'is_mounted':'0'} will match all the
     48     found devices which block device file is either /dev/sda1 or /dev/sdb1, AND
     49     are not mounted, excluding all other devices from the matched result.
     50     """
     51     storages = None
     52 
     53 
     54     def __init__(self):
     55         self.__mounted = {}
     56 
     57 
     58     def filter(self, storage_filter={}):
     59         """Filters a stored result returning a list of matching devices.
     60 
     61         The passed dictionary represent the filter and its values are regular
     62         expressions (str). If an element of self.storage matches the regex
     63         defined in all the keys for a filter, the item will be part of the
     64         returning value.
     65 
     66         Calling this method does not change self.storages, thus can be called
     67         several times against the same result set.
     68 
     69         @param storage_filter: a dictionary representing the filter.
     70 
     71         @return a list of dictionaries representing the found devices after the
     72                 application of the filter. The list can be empty if no device
     73                 has been found.
     74         """
     75         ret = []
     76 
     77         for storage in self.storages:
     78             matches = True
     79             for key in storage_filter:
     80                 if not re.match(storage_filter[key], storage[key]):
     81                     matches = False
     82                     break
     83             if matches:
     84                 ret.append(storage.copy())
     85 
     86         return ret
     87 
     88 
     89     def scan(self, storage_filter={}):
     90         """Scan the current storage devices.
     91 
     92         If no parameter is given, it will return all the storage devices found.
     93         Otherwise it will internally call self.filter() with the passed
     94         filter.
     95         The result (being it filtered or not) will be saved in self.storages.
     96 
     97         Such list can be (re)-filtered using self.filter().
     98 
     99         @param storage_filter: a dict representing the filter, default is
    100                 matching anything.
    101 
    102         @return a list of found dictionaries representing the found devices.
    103                  The list can be empty if no device has been found.
    104         """
    105         self.storages = liststorage.get_all()
    106 
    107         if storage_filter:
    108             self.storages = self.filter(storage_filter)
    109 
    110         return self.storages
    111 
    112 
    113     def mount_volume(self, index=None, storage_dict=None, args=''):
    114         """Mount the passed volume.
    115 
    116         Either index or storage_dict can be set, but not both at the same time.
    117         If neither is passed, it will mount the first volume found in
    118         self.storage.
    119 
    120         @param index: (int) the index in self.storages for the storage
    121                 device/volume to be mounted.
    122         @param storage_dict: (dict) the storage dictionary representing the
    123                 storage device, the dictionary should be obtained from
    124                 self.storage or using self.scan() or self.filter().
    125         @param args: (str) args to be passed to the mount command, if needed.
    126                      e.g., "-o foo,bar -t ext3".
    127         """
    128         if index is None and storage_dict is None:
    129             storage_dict = self.storages[0]
    130         elif isinstance(index, int):
    131             storage_dict = self.storages[index]
    132         elif not isinstance(storage_dict, dict):
    133             raise TypeError('Either index or storage_dict passed '
    134                             'with the wrong type')
    135 
    136         if storage_dict['is_mounted']:
    137             logging.debug('Volume "%s" is already mounted, skipping '
    138                           'mount_volume().')
    139             return
    140 
    141         logging.info('Mounting %(device)s in %(mountpoint)s.', storage_dict)
    142 
    143         try:
    144             # Create the dir in case it does not exist.
    145             os.mkdir(storage_dict['mountpoint'])
    146         except OSError, e:
    147             # If it's not "file exists", report the exception.
    148             if e.errno != 17:
    149                 raise e
    150         cmd = 'mount %s' % args
    151         cmd += ' %(device)s %(mountpoint)s' % storage_dict
    152         utils.system(cmd)
    153         storage_dict['is_mounted'] = True
    154         self.__mounted[storage_dict['mountpoint']] = storage_dict
    155 
    156 
    157     def umount_volume(self, index=None, storage_dict=None, args=''):
    158         """Un-mount the passed volume, by index or storage dictionary.
    159 
    160         Either index or storage_dict can be set, but not both at the same time.
    161         If neither is passed, it will mount the first volume found in
    162         self.storage.
    163 
    164         @param index: (int) the index in self.storages for the storage
    165                 device/volume to be mounted.
    166         @param storage_dict: (dict) the storage dictionary representing the
    167                 storage device, the dictionary should be obtained from
    168                 self.storage or using self.scan() or self.filter().
    169         @param args: (str) args to be passed to the umount command, if needed.
    170                      e.g., '-f -t' for force+lazy umount.
    171         """
    172         if index is None and storage_dict is None:
    173             storage_dict = self.storages[0]
    174         elif isinstance(index, int):
    175             storage_dict = self.storages[index]
    176         elif not isinstance(storage_dict, dict):
    177             raise TypeError('Either index or storage_dict passed '
    178                             'with the wrong type')
    179 
    180 
    181         if not storage_dict['is_mounted']:
    182             logging.debug('Volume "%s" is already unmounted: skipping '
    183                           'umount_volume().')
    184             return
    185 
    186         logging.info('Unmounting %(device)s from %(mountpoint)s.',
    187                      storage_dict)
    188         cmd = 'umount %s' % args
    189         cmd += ' %(device)s' % storage_dict
    190         utils.system(cmd)
    191         # We don't care if it fails, it might be busy for a /proc/mounts issue.
    192         # See BUG=chromium-os:32105
    193         try:
    194             os.rmdir(storage_dict['mountpoint'])
    195         except OSError, e:
    196             logging.debug('Removing %s failed: %s: ignoring.',
    197                           storage_dict['mountpoint'], e)
    198         storage_dict['is_mounted'] = False
    199         # If we previously mounted it, remove it from our internal list.
    200         if storage_dict['mountpoint'] in self.__mounted:
    201             del self.__mounted[storage_dict['mountpoint']]
    202 
    203 
    204     def unmount_all(self):
    205         """Unmount all volumes mounted by self.mount_volume().
    206         """
    207         # We need to copy it since we are iterating over a dict which will
    208         # change size.
    209         for volume in self.__mounted.copy():
    210             self.umount_volume(storage_dict=self.__mounted[volume])
    211 
    212 
    213 class StorageTester(test.test):
    214     """This is a class all tests about Storage can use.
    215 
    216     It has methods to
    217     - create random files
    218     - compute a file's md5 checksum
    219     - look/wait for a specific device (specified using StorageScanner
    220       dictionary format)
    221 
    222     Subclasses can override the _prepare_volume() method in order to disable
    223     them or change their behaviours.
    224 
    225     Subclasses should take care of unmount all the mounted filesystems when
    226     needed (e.g. on cleanup phase), calling self.umount_volume() or
    227     self.unmount_all().
    228     """
    229     scanner = None
    230 
    231 
    232     def initialize(self, filter_dict={'bus':'usb'}, filesystem='ext2'):
    233         """Initialize the test.
    234 
    235         Instantiate a StorageScanner instance to be used by tests and prepare
    236         any volume matched by |filter_dict|.
    237         Volume preparation is done by the _prepare_volume() method, which can be
    238         overriden by subclasses.
    239 
    240         @param filter_dict: a dictionary to filter attached USB devices to be
    241                             initialized.
    242         @param filesystem: the filesystem name to format the attached device.
    243         """
    244         super(StorageTester, self).initialize()
    245 
    246         self.scanner = StorageScanner()
    247 
    248         self._prepare_volume(filter_dict, filesystem=filesystem)
    249 
    250         # Be sure that if any operation above uses self.scanner related
    251         # methods, its result is cleaned after use.
    252         self.storages = None
    253 
    254 
    255     def _prepare_volume(self, filter_dict, filesystem='ext2'):
    256         """Prepare matching volumes for test.
    257 
    258         Prepare all the volumes matching |filter_dict| for test by formatting
    259         the matching storages with |filesystem|.
    260 
    261         This method is called by StorageTester.initialize(), a subclass can
    262         override this method to change its behaviour.
    263         Setting it to None (or a not callable) will disable it.
    264 
    265         @param filter_dict: a filter for the storages to be prepared.
    266         @param filesystem: filesystem with which volumes will be formatted.
    267         """
    268         if not os.path.isfile('/sbin/mkfs.%s' % filesystem):
    269             raise error.TestError('filesystem not supported by mkfs installed '
    270                                   'on this device')
    271 
    272         try:
    273             storages = self.wait_for_devices(filter_dict, cycles=1,
    274                                              mount_volume=False)[0]
    275 
    276             for storage in storages:
    277                 logging.debug('Preparing volume on %s.', storage['device'])
    278                 cmd = 'mkfs.%s %s' % (filesystem, storage['device'])
    279                 utils.system(cmd)
    280         except StorageException, e:
    281             logging.warning("%s._prepare_volume() didn't find any device "
    282                             "attached: skipping volume preparation: %s",
    283                             self.__class__.__name__, e)
    284         except error.CmdError, e:
    285             logging.warning("%s._prepare_volume() couldn't format volume: %s",
    286                             self.__class__.__name__, e)
    287 
    288         logging.debug('Volume preparation finished.')
    289 
    290 
    291     def wait_for_devices(self, storage_filter, time_to_sleep=1, cycles=10,
    292                          mount_volume=True):
    293         """Cycles |cycles| times waiting |time_to_sleep| seconds each cycle,
    294         looking for a device matching |storage_filter|
    295 
    296         @param storage_filter: a dictionary holding a set of  storage device's
    297                 keys which are used as filter, to look for devices.
    298                 @see StorageDevice class documentation.
    299         @param time_to_sleep: time (int) to wait after each |cycles|.
    300         @param cycles: number of tentatives. Use -1 for infinite.
    301 
    302         @raises StorageException if no device can be found.
    303 
    304         @return (storage_dict, waited_time) tuple. storage_dict is the found
    305                  device list and waited_time is the time spent waiting for the
    306                  device to be found.
    307         """
    308         msg = ('Scanning for %s for %d times, waiting each time '
    309                '%d secs' % (storage_filter, cycles, time_to_sleep))
    310         if mount_volume:
    311             logging.debug('%s and mounting each matched volume.', msg)
    312         else:
    313             logging.debug('%s, but not mounting each matched volume.', msg)
    314 
    315         if cycles == -1:
    316             logging.info('Waiting until device is inserted, '
    317                          'no timeout has been set.')
    318 
    319         cycle = 0
    320         while cycles == -1 or cycle < cycles:
    321             ret = self.scanner.scan(storage_filter)
    322             if ret:
    323                 logging.debug('Found %s (mount_volume=%d).', ret, mount_volume)
    324                 if mount_volume:
    325                     for storage in ret:
    326                         self.scanner.mount_volume(storage_dict=storage)
    327 
    328                 return (ret, cycle*time_to_sleep)
    329             else:
    330                 logging.debug('Storage %s not found, wait and rescan '
    331                               '(cycle %d).', storage_filter, cycle)
    332                 # Wait a bit and rescan storage list.
    333                 time.sleep(time_to_sleep)
    334                 cycle += 1
    335 
    336         # Device still not found.
    337         msg = ('Could not find anything matching "%s" after %d seconds' %
    338                 (storage_filter, time_to_sleep*cycles))
    339         raise StorageException(msg)
    340 
    341 
    342     def wait_for_device(self, storage_filter, time_to_sleep=1, cycles=10,
    343                         mount_volume=True):
    344         """Cycles |cycles| times waiting |time_to_sleep| seconds each cycle,
    345         looking for a device matching |storage_filter|.
    346 
    347         This method needs to match one and only one device.
    348         @raises StorageException if no device can be found or more than one is
    349                  found.
    350 
    351         @param storage_filter: a dictionary holding a set of  storage device's
    352                 keys which are used as filter, to look for devices
    353                 The filter has to be match a single device, a multiple matching
    354                 filter will lead to StorageException to e risen. Use
    355                 self.wait_for_devices() if more than one device is allowed to
    356                 be found.
    357                 @see StorageDevice class documentation.
    358         @param time_to_sleep: time (int) to wait after each |cycles|.
    359         @param cycles: number of tentatives. Use -1 for infinite.
    360 
    361         @return (storage_dict, waited_time) tuple. storage_dict is the found
    362                  device list and waited_time is the time spent waiting for the
    363                  device to be found.
    364         """
    365         storages, waited_time = self.wait_for_devices(storage_filter,
    366             time_to_sleep=time_to_sleep,
    367             cycles=cycles,
    368             mount_volume=mount_volume)
    369         if len(storages) > 1:
    370             msg = ('filter matched more than one storage volume, use '
    371                 '%s.wait_for_devices() if you need more than one match' %
    372                 self.__class__)
    373             raise StorageException(msg)
    374 
    375         # Return the first element if only this one has been matched.
    376         return (storages[0], waited_time)
    377 
    378 
    379 # Some helpers not present in utils.py to abstract normal file operations.
    380 
    381 def create_file(path, size):
    382     """Create a file using /dev/urandom.
    383 
    384     @param path: the path of the file.
    385     @param size: the file size in bytes.
    386     """
    387     logging.debug('Creating %s (size %d) from /dev/urandom.', path, size)
    388     with file('/dev/urandom', 'rb') as urandom:
    389         utils.open_write_close(path, urandom.read(size))
    390 
    391 
    392 def checksum_file(path):
    393     """Compute the MD5 Checksum for a file.
    394 
    395     @param path: the path of the file.
    396 
    397     @return a string with the checksum.
    398     """
    399     chunk_size = 1024
    400 
    401     m = hashlib.md5()
    402     with file(path, 'rb') as f:
    403         for chunk in f.read(chunk_size):
    404             m.update(chunk)
    405 
    406     logging.debug("MD5 checksum for %s is %s.", path, m.hexdigest())
    407 
    408     return m.hexdigest()
    409 
    410 
    411 def args_to_storage_dict(args):
    412     """Map args into storage dictionaries.
    413 
    414     This function is to be used (likely) in control files to obtain a storage
    415     dictionary from command line arguments.
    416 
    417     @param args: a list of arguments as passed to control file.
    418 
    419     @return a tuple (storage_dict, rest_of_args) where storage_dict is a
    420             dictionary for storage filtering and rest_of_args is a dictionary
    421             of keys which do not match storage dict keys.
    422     """
    423     args_dict = utils.args_to_dict(args)
    424     storage_dict = {}
    425 
    426     # A list of all allowed keys and their type.
    427     key_list = ('device', 'bus', 'model', 'size', 'fs_uuid', 'fstype',
    428                 'is_mounted', 'mountpoint')
    429 
    430     def set_if_exists(src, dst, key):
    431         """If |src| has |key| copies its value to |dst|.
    432 
    433         @return True if |key| exists in |src|, False otherwise.
    434         """
    435         if key in src:
    436             dst[key] = src[key]
    437             return True
    438         else:
    439             return False
    440 
    441     for key in key_list:
    442         if set_if_exists(args_dict, storage_dict, key):
    443             del args_dict[key]
    444 
    445     # Return the storage dict and the leftovers of the args to be evaluated
    446     # later.
    447     return storage_dict, args_dict
    448