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