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