partition.py revision e2a68ac3727cb561a64b310b4e2a50148490b968
1# Copyright Martin J. Bligh, Google, 2006-2008 2 3import os, re, string, sys, fcntl 4from autotest_lib.client.bin import autotest_utils 5from autotest_lib.client.common_lib import error, utils 6 7 8class FsOptions(object): 9 """A class encapsulating a filesystem test's parameters. 10 11 Properties: (all strings) 12 fstype: The filesystem type ('ext2', 'ext4', 'xfs', etc.) 13 mkfs_flags: Additional command line options to mkfs or '' if none. 14 mount_options: The options to pass to mount -o or '' if none. 15 fs_tag: A short name for this filesystem test to use in the results. 16 """ 17 # NOTE(gps): This class could grow or be merged with something else in the 18 # future that actually uses the encapsulated data (say to run mkfs) rather 19 # than just being a container. 20 # Ex: fsdev_disks.mkfs_all_disks really should become a method. 21 22 __slots__ = ('fstype', 'mkfs_flags', 'mount_options', 'fs_tag') 23 24 def __init__(self, fstype, mkfs_flags, mount_options, fs_tag): 25 """Fill in our properties.""" 26 if not fstype or not fs_tag: 27 raise ValueError('A filesystem and fs_tag are required.') 28 self.fstype = fstype 29 self.mkfs_flags = mkfs_flags 30 self.mount_options = mount_options 31 self.fs_tag = fs_tag 32 33 34 def __str__(self): 35 val = ('FsOptions(fstype=%r, mkfs_flags=%r, ' 36 'mount_options=%r, fs_tag=%r)' % 37 (self.fstype, self.mkfs_flags, 38 self.mount_options, self.fs_tag)) 39 return val 40 41 42def list_mount_devices(): 43 devices = [] 44 # list mounted filesystems 45 for line in utils.system_output('mount').splitlines(): 46 devices.append(line.split()[0]) 47 # list mounted swap devices 48 for line in utils.system_output('swapon -s').splitlines(): 49 if line.startswith('/'): # skip header line 50 devices.append(line.split()[0]) 51 return devices 52 53 54def list_mount_points(): 55 mountpoints = [] 56 for line in utils.system_output('mount').splitlines(): 57 mountpoints.append(line.split()[2]) 58 return mountpoints 59 60 61def get_iosched_path(device_name, component): 62 return '/sys/block/%s/queue/%s' % (device_name, component) 63 64 65def wipe_filesystem(job, mountpoint): 66 wipe_cmd = 'rm -rf %s/*' % mountpoint 67 try: 68 utils.system(wipe_cmd) 69 except: 70 job.record('FAIL', None, wipe_cmd, error.format_error()) 71 raise 72 else: 73 job.record('GOOD', None, wipe_cmd) 74 75 76def filter_non_linux(part_name): 77 """Return false if the supplied partition name is not type 83 linux.""" 78 part_device = '/dev/' + part_name 79 disk_device = part_device.rstrip('0123456789') 80 # Parse fdisk output to get partition info. Ugly but it works. 81 fdisk_fd = os.popen("/sbin/fdisk -l -u '%s'" % disk_device) 82 fdisk_lines = fdisk_fd.readlines() 83 fdisk_fd.close() 84 for line in fdisk_lines: 85 if not line.startswith(part_device): 86 continue 87 info_tuple = line.split() 88 # The Id will be in one of two fields depending on if the boot flag 89 # was set. Caveat: this assumes no boot partition will be 83 blocks. 90 for fsinfo in info_tuple[4:6]: 91 if fsinfo == '83': # hex 83 is the linux fs partition type 92 return True 93 return False 94 95 96def get_partition_list(job, min_blocks=0, filter_func=None, exclude_swap=True, 97 __open=open): 98 """Get a list of partition objects for all disk partitions on the system. 99 100 Loopback devices and unnumbered (whole disk) devices are always excluded. 101 102 Args: 103 job: The job instance to pass to the partition object constructor. 104 min_blocks: The minimum number of blocks for a partition to be considered. 105 filter_func: A callable that returns True if a partition is desired. 106 It will be passed one parameter: The partition name (hdc3, etc.). 107 Some useful filter functions are already defined in this module. 108 exclude_swap: If True any partition actively in use as a swap device 109 will be excluded. 110 __open: Reserved for unit testing. 111 112 Returns: 113 A list of partition object instances. 114 """ 115 active_swap_devices = set() 116 if exclude_swap: 117 for swapline in __open('/proc/swaps'): 118 if swapline.startswith('/'): 119 active_swap_devices.add(swapline.split()[0]) 120 121 partitions = [] 122 for partline in __open('/proc/partitions').readlines(): 123 fields = partline.strip().split() 124 if len(fields) != 4 or partline.startswith('major'): 125 continue 126 (major, minor, blocks, partname) = fields 127 blocks = int(blocks) 128 129 # The partition name better end with a digit, else it's not a partition 130 if not partname[-1].isdigit(): 131 continue 132 133 # We don't want the loopback device in the partition list 134 if 'loop' in partname: 135 continue 136 137 device = '/dev/' + partname 138 if exclude_swap and device in active_swap_devices: 139 print 'get_partition_list() skipping', partname, '- Active swap.' 140 continue 141 142 if min_blocks and blocks < min_blocks: 143 print 'get_partition_list() skipping', partname, '- Too small.' 144 continue 145 146 if filter_func and not filter_func(partname): 147 print 'get_partition_list() skipping', partname, '- filter_func.' 148 continue 149 150 partitions.append(partition(job, device)) 151 152 return partitions 153 154 155def filter_partition_list(partitions, devnames): 156 """Pick and choose which partition to keep. 157 158 filter_partition_list accepts a list of partition objects and a list 159 of strings. If a partition has the device name of the strings it 160 is returned in a list. 161 162 Args: 163 partitions: A list of partition objects 164 devnames: A list of devnames of the form '/dev/hdc3' that 165 specifies which partitions to include in the returned list. 166 167 Returns: A list of partition objects specified by devnames, in the 168 order devnames specified 169 """ 170 171 filtered_list = [] 172 for p in partitions: 173 for d in devnames: 174 if p.device == d and p not in filtered_list: 175 filtered_list.append(p) 176 177 return filtered_list 178 179 180def parallel(partitions, method_name, *args, **dargs): 181 """\ 182 Run a partition method (with appropriate arguments) in parallel, 183 across a list of partition objects 184 """ 185 if not partitions: 186 return 187 job = partitions[0].job 188 flist = [] 189 if (not hasattr(partition, method_name) or 190 not callable(getattr(partition, method_name))): 191 err = "partition.parallel got invalid method %s" % method_name 192 raise RuntimeError(err) 193 194 for p in partitions: 195 print_args = list(args) 196 print_args += ['%s=%s' % (key, dargs[key]) for key in dargs.keys()] 197 print '%s.%s(%s)' % (str(p), method_name, ', '.join(print_args)) 198 sys.stdout.flush() 199 def func(function, part=p): 200 getattr(part, method_name)(*args, **dargs) 201 flist.append((func, ())) 202 job.parallel(*flist) 203 204 205def filesystems(): 206 """\ 207 Return a list of all available filesystems 208 """ 209 return [re.sub('(nodev)?\s*', '', fs) for fs in open('/proc/filesystems')] 210 211 212class partition(object): 213 """ 214 Class for handling partitions and filesystems 215 """ 216 217 def __init__(self, job, device, mountpoint=None, loop_size=0): 218 """ 219 device should be able to be a file as well 220 which we mount as loopback. 221 222 job 223 A client.bin.job instance. 224 device 225 The device in question (eg "/dev/hda2") 226 mountpoint 227 Default mountpoint for the device. 228 loop_size 229 Size of loopback device (in MB). Defaults to 0. 230 """ 231 232 self.device = device 233 self.name = os.path.basename(device) 234 self.job = job 235 self.loop = loop_size 236 self.fstype = None 237 self.mkfs_flags = None 238 self.mount_options = None 239 self.fs_tag = None 240 if self.loop: 241 cmd = 'dd if=/dev/zero of=%s bs=1M count=%d' % (device, loop_size) 242 utils.system(cmd) 243 if mountpoint: 244 self.mountpoint = mountpoint 245 else: 246 self.mountpoint = self.get_mountpoint() 247 248 249 def __repr__(self): 250 return '<Partition: %s>' % self.device 251 252 253 def set_fs_options(self, fs_options): 254 self.fstype = fs_options.fstype 255 self.mkfs_flags = fs_options.mkfs_flags 256 self.mount_options = fs_options.mount_options 257 self.fs_tag = fs_options.fs_tag 258 259 260 def run_test(self, test, **dargs): 261 self.job.run_test(test, dir=self.mountpoint, **dargs) 262 263 264 def run_test_on_partition(self, test, **dargs): 265 tag = dargs.pop('tag', None) 266 if tag: 267 tag = '%s.%s' % (self.name, tag) 268 else: 269 if self.fs_tag: 270 tag = '%s.%s' % (self.name, self.fs_tag) 271 else: 272 tag = self.name 273 274 def func(test_tag, dir, **dargs): 275 self.unmount(ignore_status=True, record=False) 276 self.mkfs() 277 self.mount() 278 try: 279 self.job.run_test(test, tag=test_tag, dir=dir, **dargs) 280 finally: 281 self.unmount() 282 self.fsck() 283 284 # The tag is the tag for the group (get stripped off by run_group) 285 # The test_tag is the tag for the test itself 286 self.job.run_group(func, test_tag=tag, tag=test + '.' + tag, 287 dir=self.mountpoint, **dargs) 288 289 290 def get_mountpoint(self): 291 for line in open('/proc/mounts', 'r').readlines(): 292 parts = line.split() 293 if parts[0] == self.device: 294 return parts[1] # The mountpoint where it's mounted 295 return None 296 297 298 def mkfs_exec(self, fstype): 299 """ 300 Return the proper mkfs executable based on fs 301 """ 302 if fstype == 'ext4': 303 if os.path.exists('/sbin/mkfs.ext4'): 304 return 'mkfs' 305 # If ext4 supported e2fsprogs is not installed we use the 306 # autotest supplied one in tools dir which is statically linked""" 307 auto_mkfs = os.path.join(self.job.toolsdir, 'mkfs.ext4dev') 308 if os.path.exists(auto_mkfs): 309 return auto_mkfs 310 else: 311 return 'mkfs' 312 313 raise NameError('Error creating partition for filesystem type %s' % 314 fstype) 315 316 317 def mkfs(self, fstype=None, args='', record=True): 318 """ 319 Format a partition to fstype 320 """ 321 if list_mount_devices().count(self.device): 322 raise NameError('Attempted to format mounted device %s' % 323 self.device) 324 325 if not fstype: 326 if self.fstype: 327 fstype = self.fstype 328 else: 329 fstype = 'ext2' 330 331 if self.mkfs_flags: 332 args += ' ' + self.mkfs_flags 333 if fstype == 'xfs': 334 args += ' -f' 335 336 if self.loop: 337 # BAH. Inconsistent mkfs syntax SUCKS. 338 if fstype.startswith('ext'): 339 args += ' -F' 340 elif fstype == 'reiserfs': 341 args += ' -f' 342 args = args.strip() 343 344 mkfs_cmd = "%s -t %s %s %s" % (self.mkfs_exec(fstype), fstype, args, 345 self.device) 346 print mkfs_cmd 347 sys.stdout.flush() 348 try: 349 # We throw away the output here - we only need it on error, in 350 # which case it's in the exception 351 utils.system_output("yes | %s" % mkfs_cmd) 352 except error.CmdError, e: 353 print e.result_obj 354 if record: 355 self.job.record('FAIL', None, mkfs_cmd, error.format_error()) 356 raise 357 except: 358 if record: 359 self.job.record('FAIL', None, mkfs_cmd, error.format_error()) 360 raise 361 else: 362 if record: 363 self.job.record('GOOD', None, mkfs_cmd) 364 self.fstype = fstype 365 366 367 def get_fsck_exec(self): 368 """ 369 Return the proper mkfs executable based on self.fstype 370 """ 371 if self.fstype == 'ext4': 372 if os.path.exists('/sbin/fsck.ext4'): 373 return 'fsck' 374 # If ext4 supported e2fsprogs is not installed we use the 375 # autotest supplied one in tools dir which is statically linked""" 376 auto_fsck = os.path.join(self.job.toolsdir, 'fsck.ext4dev') 377 if os.path.exists(auto_fsck): 378 return auto_fsck 379 else: 380 return 'fsck' 381 382 raise NameError('Error creating partition for filesystem type %s' % 383 self.fstype) 384 385 386 def fsck(self, args='-n', record=True): 387 # I hate reiserfstools. 388 # Requires an explit Yes for some inane reason 389 fsck_cmd = '%s %s %s' % (self.get_fsck_exec(), self.device, args) 390 if self.fstype == 'reiserfs': 391 fsck_cmd = 'yes "Yes" | ' + fsck_cmd 392 print fsck_cmd 393 sys.stdout.flush() 394 try: 395 utils.system("yes | " + fsck_cmd) 396 except: 397 if record: 398 self.job.record('FAIL', None, fsck_cmd, error.format_error()) 399 raise 400 else: 401 if record: 402 self.job.record('GOOD', None, fsck_cmd) 403 404 405 def mount(self, mountpoint=None, fstype=None, args='', record=True): 406 if fstype is None: 407 fstype = self.fstype 408 else: 409 assert(self.fstype == None or self.fstype == fstype); 410 411 if self.mount_options: 412 args += ' -o ' + self.mount_options 413 if fstype: 414 args += ' -t ' + fstype 415 if self.loop: 416 args += ' -o loop' 417 args = args.lstrip() 418 419 if not mountpoint: 420 mountpoint = self.mountpoint 421 422 mount_cmd = "mount %s %s %s" % (args, self.device, mountpoint) 423 print mount_cmd 424 425 if list_mount_devices().count(self.device): 426 err = 'Attempted to mount mounted device' 427 self.job.record('FAIL', None, mount_cmd, err) 428 raise NameError(err) 429 if list_mount_points().count(mountpoint): 430 err = 'Attempted to mount busy mountpoint' 431 self.job.record('FAIL', None, mount_cmd, err) 432 raise NameError(err) 433 434 mtab = open('/etc/mtab') 435 # We have to get an exclusive lock here - mount/umount are racy 436 fcntl.flock(mtab.fileno(), fcntl.LOCK_EX) 437 print mount_cmd 438 sys.stdout.flush() 439 try: 440 utils.system(mount_cmd) 441 mtab.close() 442 except: 443 mtab.close() 444 if record: 445 self.job.record('FAIL', None, mount_cmd, error.format_error()) 446 raise 447 else: 448 if record: 449 self.job.record('GOOD', None, mount_cmd) 450 self.mountpoint = mountpoint 451 self.fstype = fstype 452 453 454 def unmount(self, handle=None, ignore_status=False, record=True): 455 if not handle: 456 handle = self.device 457 umount_cmd = "umount " + handle 458 if not self.get_mountpoint(): 459 # It's not even mounted to start with 460 if record and not ignore_status: 461 self.job.record('FAIL', None, umount_cmd, 'Not mounted') 462 return 463 mtab = open('/etc/mtab') 464 # We have to get an exclusive lock here - mount/umount are racy 465 fcntl.flock(mtab.fileno(), fcntl.LOCK_EX) 466 print umount_cmd 467 sys.stdout.flush() 468 try: 469 utils.system(umount_cmd) 470 mtab.close() 471 except Exception: 472 print "Standard umount failed, will try forcing. Users:" 473 try: 474 cmd = 'fuser ' + self.get_mountpoint() 475 print cmd 476 fuser = utils.system_output(cmd) 477 print fuser 478 users = re.sub('.*:', '', fuser).split() 479 for user in users: 480 m = re.match('(\d+)(.*)', user) 481 (pid, usage) = (m.group(1), m.group(2)) 482 try: 483 ps = utils.system_output('ps -p %s | tail +2' % pid) 484 print '%s %s %s' % (usage, pid, ps) 485 except Exception: 486 pass 487 utils.system('ls -l ' + handle) 488 umount_cmd = "umount -f " + handle 489 print umount_cmd 490 utils.system(umount_cmd) 491 mtab.close() 492 except Exception: 493 mtab.close() 494 if record and not ignore_status: 495 self.job.record('FAIL', None, umount_cmd, 496 error.format_error()) 497 raise 498 499 if record: 500 self.job.record('GOOD', None, umount_cmd) 501 502 503 def wipe(self): 504 wipe_filesystem(self.job, self.mountpoint) 505 506 507 def get_io_scheduler_list(self, device_name): 508 names = open(self.__sched_path(device_name)).read() 509 return names.translate(string.maketrans('[]', ' ')).split() 510 511 512 def get_io_scheduler(self, device_name): 513 return re.split('[\[\]]', 514 open(self.__sched_path(device_name)).read())[1] 515 516 517 def set_io_scheduler(self, device_name, name): 518 if name not in self.get_io_scheduler_list(device_name): 519 raise NameError('No such IO scheduler: %s' % name) 520 f = open(self.__sched_path(device_name), 'w') 521 print >> f, name 522 f.close() 523 524 525 def __sched_path(self, device_name): 526 return '/sys/block/%s/queue/scheduler' % device_name 527