• 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
5"""Storage device utilities to be used in storage device based tests
6"""
7
8import logging, re, os, time, hashlib
9
10from autotest_lib.client.bin import test, utils
11from autotest_lib.client.common_lib import error
12from autotest_lib.client.cros import liststorage
13
14
15class 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
22class 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
213class 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
381def 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
392def 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
411def 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