partition.py revision 53da18eddf69243ca175d9a4603cba5b55300726
1# Copyright Martin J. Bligh, Google, 2006-2008 2 3import os, re, string, sys, fcntl 4from autotest_lib.client.bin import utils 5from autotest_lib.client.common_lib import error 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, 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 loop_size 227 Size of loopback device (in MB). Defaults to 0. 228 """ 229 230 # NOTE: This code is used by IBM / ABAT. Do not remove. 231 part = re.compile(r'^part(\d+)$') 232 m = part.match(device) 233 if m: 234 number = int(m.groups()[0]) 235 partitions = job.config_get('partition.partitions') 236 try: 237 device = partitions[number] 238 except: 239 raise NameError("Partition '" + device + "' not available") 240 241 self.device = device 242 self.name = os.path.basename(device) 243 self.job = job 244 self.loop = loop_size 245 self.fstype = None 246 self.mkfs_flags = None 247 self.mount_options = None 248 self.fs_tag = None 249 if self.loop: 250 cmd = 'dd if=/dev/zero of=%s bs=1M count=%d' % (device, loop_size) 251 utils.system(cmd) 252 253 254 def __repr__(self): 255 return '<Partition: %s>' % self.device 256 257 258 def set_fs_options(self, fs_options): 259 self.fstype = fs_options.fstype 260 self.mkfs_flags = fs_options.mkfs_flags 261 self.mount_options = fs_options.mount_options 262 self.fs_tag = fs_options.fs_tag 263 264 265 def run_test(self, test, **dargs): 266 self.job.run_test(test, dir=self.get_mountpoint(), **dargs) 267 268 269 def run_test_on_partition(self, test, mountpoint_func, **dargs): 270 """ executes a test fs-style (umount,mkfs,mount,test) 271 Here we unmarshal the args to set up tags before running the test. 272 Tests are also run by first umounting, mkfsing and then mounting 273 before executing the test. 274 275 Args: 276 test - name of test to run 277 mountpoint_func - function to return mount point string 278 dargs - dictionary of args 279 """ 280 tag = dargs.get('tag') 281 if tag: 282 tag = '%s.%s' % (self.name, tag) 283 elif self.fs_tag: 284 tag = '%s.%s' % (self.name, self.fs_tag) 285 else: 286 tag = self.name 287 288 dargs['tag'] = test + '.' + tag 289 290 def func(test_tag, dir=None, **dargs): 291 self.unmount(ignore_status=True, record=False) 292 self.mkfs() 293 self.mount(dir) 294 try: 295 self.job.run_test(test, tag=test_tag, dir=mountpoint, **dargs) 296 finally: 297 self.unmount() 298 self.fsck() 299 300 mountpoint = mountpoint_func(self) 301 302 # The tag is the tag for the group (get stripped off by run_group) 303 # The test_tag is the tag for the test itself 304 self.job.run_group(func, test_tag=tag, dir=mountpoint, **dargs) 305 306 307 def get_mountpoint(self): 308 for line in open('/proc/mounts', 'r').readlines(): 309 parts = line.split() 310 if parts[0] == self.device: 311 return parts[1] # The mountpoint where it's mounted 312 return None 313 314 315 def mkfs_exec(self, fstype): 316 """ 317 Return the proper mkfs executable based on fs 318 """ 319 if fstype == 'ext4': 320 if os.path.exists('/sbin/mkfs.ext4'): 321 return 'mkfs' 322 # If ext4 supported e2fsprogs is not installed we use the 323 # autotest supplied one in tools dir which is statically linked""" 324 auto_mkfs = os.path.join(self.job.toolsdir, 'mkfs.ext4dev') 325 if os.path.exists(auto_mkfs): 326 return auto_mkfs 327 else: 328 return 'mkfs' 329 330 raise NameError('Error creating partition for filesystem type %s' % 331 fstype) 332 333 334 def mkfs(self, fstype=None, args='', record=True): 335 """ 336 Format a partition to fstype 337 """ 338 if list_mount_devices().count(self.device): 339 raise NameError('Attempted to format mounted device %s' % 340 self.device) 341 342 if not fstype: 343 if self.fstype: 344 fstype = self.fstype 345 else: 346 fstype = 'ext2' 347 348 if self.mkfs_flags: 349 args += ' ' + self.mkfs_flags 350 if fstype == 'xfs': 351 args += ' -f' 352 353 if self.loop: 354 # BAH. Inconsistent mkfs syntax SUCKS. 355 if fstype.startswith('ext'): 356 args += ' -F' 357 elif fstype == 'reiserfs': 358 args += ' -f' 359 args = args.strip() 360 361 mkfs_cmd = "%s -t %s %s %s" % (self.mkfs_exec(fstype), fstype, args, 362 self.device) 363 print mkfs_cmd 364 sys.stdout.flush() 365 try: 366 # We throw away the output here - we only need it on error, in 367 # which case it's in the exception 368 utils.system_output("yes | %s" % mkfs_cmd) 369 except error.CmdError, e: 370 print e.result_obj 371 if record: 372 self.job.record('FAIL', None, mkfs_cmd, error.format_error()) 373 raise 374 except: 375 if record: 376 self.job.record('FAIL', None, mkfs_cmd, error.format_error()) 377 raise 378 else: 379 if record: 380 self.job.record('GOOD', None, mkfs_cmd) 381 self.fstype = fstype 382 383 384 def get_fsck_exec(self): 385 """ 386 Return the proper mkfs executable based on self.fstype 387 """ 388 if self.fstype == 'ext4': 389 if os.path.exists('/sbin/fsck.ext4'): 390 return 'fsck' 391 # If ext4 supported e2fsprogs is not installed we use the 392 # autotest supplied one in tools dir which is statically linked""" 393 auto_fsck = os.path.join(self.job.toolsdir, 'fsck.ext4dev') 394 if os.path.exists(auto_fsck): 395 return auto_fsck 396 else: 397 return 'fsck' 398 399 raise NameError('Error creating partition for filesystem type %s' % 400 self.fstype) 401 402 403 def fsck(self, args='-n', record=True): 404 # I hate reiserfstools. 405 # Requires an explit Yes for some inane reason 406 fsck_cmd = '%s %s %s' % (self.get_fsck_exec(), self.device, args) 407 if self.fstype == 'reiserfs': 408 fsck_cmd = 'yes "Yes" | ' + fsck_cmd 409 print fsck_cmd 410 sys.stdout.flush() 411 try: 412 utils.system("yes | " + fsck_cmd) 413 except: 414 if record: 415 self.job.record('FAIL', None, fsck_cmd, error.format_error()) 416 raise 417 else: 418 if record: 419 self.job.record('GOOD', None, fsck_cmd) 420 421 422 def mount(self, mountpoint, fstype=None, args='', record=True): 423 if fstype is None: 424 fstype = self.fstype 425 else: 426 assert(self.fstype is None or self.fstype == fstype); 427 428 if self.mount_options: 429 args += ' -o ' + self.mount_options 430 if fstype: 431 args += ' -t ' + fstype 432 if self.loop: 433 args += ' -o loop' 434 args = args.lstrip() 435 436 if not mountpoint: 437 raise ValueError('No mount point specified') 438 439 mount_cmd = "mount %s %s %s" % (args, self.device, mountpoint) 440 print mount_cmd 441 442 if list_mount_devices().count(self.device): 443 err = 'Attempted to mount mounted device' 444 self.job.record('FAIL', None, mount_cmd, err) 445 raise NameError(err) 446 if list_mount_points().count(mountpoint): 447 err = 'Attempted to mount busy mountpoint' 448 self.job.record('FAIL', None, mount_cmd, err) 449 raise NameError(err) 450 451 mtab = open('/etc/mtab') 452 # We have to get an exclusive lock here - mount/umount are racy 453 fcntl.flock(mtab.fileno(), fcntl.LOCK_EX) 454 print mount_cmd 455 sys.stdout.flush() 456 try: 457 utils.system(mount_cmd) 458 mtab.close() 459 except: 460 mtab.close() 461 if record: 462 self.job.record('FAIL', None, mount_cmd, error.format_error()) 463 raise 464 else: 465 if record: 466 self.job.record('GOOD', None, mount_cmd) 467 self.fstype = fstype 468 469 470 def unmount_force(self): 471 """Kill all other jobs accessing this partition 472 473 Use fuser and ps to find all mounts on this mountpoint and unmount them. 474 475 Returns: true for success or false for any errors 476 """ 477 478 print "Standard umount failed, will try forcing. Users:" 479 try: 480 cmd = 'fuser ' + self.get_mountpoint() 481 print cmd 482 fuser = utils.system_output(cmd) 483 print fuser 484 users = re.sub('.*:', '', fuser).split() 485 for user in users: 486 m = re.match('(\d+)(.*)', user) 487 (pid, usage) = (m.group(1), m.group(2)) 488 try: 489 ps = utils.system_output('ps -p %s | tail +2' % pid) 490 print '%s %s %s' % (usage, pid, ps) 491 except Exception: 492 pass 493 utils.system('ls -l ' + self.device) 494 umount_cmd = "umount -f " + self.device 495 print umount_cmd 496 utils.system(umount_cmd) 497 return True 498 except error.CmdError: 499 print 'umount_force failed for %s' % self.device 500 return False 501 502 503 504 def unmount(self, ignore_status=False, record=True): 505 """Umount this partition. 506 507 It's easier said than done to umount a partition. 508 We need to lock the mtab file to make sure we don't have any 509 locking problems if we are umounting in paralllel. 510 511 If there turns out to be a problem with the simple umount we 512 end up calling umount_force to get more agressive. 513 514 Args: 515 ignore_status - should we notice the umount status 516 record - should we record the success or failure 517 """ 518 519 mountpoint = self.get_mountpoint() 520 if not mountpoint: 521 # It's not even mounted to start with 522 if record and not ignore_status: 523 msg = 'umount for dev %s has no mountpoint' % self.device 524 self.job.record('FAIL', None, msg, 'Not mounted') 525 return 526 527 umount_cmd = "umount " + mountpoint 528 mtab = open('/etc/mtab') 529 530 # We have to get an exclusive lock here - mount/umount are racy 531 fcntl.flock(mtab.fileno(), fcntl.LOCK_EX) 532 print umount_cmd 533 sys.stdout.flush() 534 try: 535 utils.system(umount_cmd) 536 mtab.close() 537 if record: 538 self.job.record('GOOD', None, umount_cmd) 539 except (error.CmdError, IOError): 540 mtab.close() 541 542 # Try the forceful umount 543 if self.unmount_force(): 544 return 545 546 # If we are here we cannot umount this partition 547 if record and not ignore_status: 548 self.job.record('FAIL', None, umount_cmd, error.format_error()) 549 raise 550 551 552 def wipe(self): 553 wipe_filesystem(self.job, self.get_mountpoint()) 554 555 556 def get_io_scheduler_list(self, device_name): 557 names = open(self.__sched_path(device_name)).read() 558 return names.translate(string.maketrans('[]', ' ')).split() 559 560 561 def get_io_scheduler(self, device_name): 562 return re.split('[\[\]]', 563 open(self.__sched_path(device_name)).read())[1] 564 565 566 def set_io_scheduler(self, device_name, name): 567 if name not in self.get_io_scheduler_list(device_name): 568 raise NameError('No such IO scheduler: %s' % name) 569 f = open(self.__sched_path(device_name), 'w') 570 print >> f, name 571 f.close() 572 573 574 def __sched_path(self, device_name): 575 return '/sys/block/%s/queue/scheduler' % device_name 576