mbligh | 9bfef38 | 2009-05-21 01:32:27 +0000 | [diff] [blame] | 1 | """ |
jadmanski | 543dceb | 2010-07-30 21:42:56 +0000 | [diff] [blame] | 2 | APIs to write tests and control files that handle partition creation, deletion |
| 3 | and formatting. |
| 4 | |
| 5 | @copyright: Google 2006-2008 |
| 6 | @author: Martin Bligh (mbligh@google.com) |
mbligh | 9bfef38 | 2009-05-21 01:32:27 +0000 | [diff] [blame] | 7 | """ |
mbligh | d7fb4a6 | 2006-10-01 00:57:53 +0000 | [diff] [blame] | 8 | |
jadmanski | 543dceb | 2010-07-30 21:42:56 +0000 | [diff] [blame] | 9 | import os, re, string, sys, fcntl, logging |
| 10 | from autotest_lib.client.bin import os_dep, utils |
| 11 | from autotest_lib.client.common_lib import error |
| 12 | |
| 13 | |
| 14 | class FsOptions(object): |
| 15 | """ |
| 16 | A class encapsulating a filesystem test's parameters. |
| 17 | """ |
| 18 | # NOTE(gps): This class could grow or be merged with something else in the |
| 19 | # future that actually uses the encapsulated data (say to run mkfs) rather |
| 20 | # than just being a container. |
| 21 | # Ex: fsdev_disks.mkfs_all_disks really should become a method. |
| 22 | |
| 23 | __slots__ = ('fstype', 'mkfs_flags', 'mount_options', 'fs_tag') |
| 24 | |
| 25 | def __init__(self, fstype, fs_tag, mkfs_flags=None, mount_options=None): |
| 26 | """ |
| 27 | Fill in our properties. |
| 28 | |
| 29 | @param fstype: The filesystem type ('ext2', 'ext4', 'xfs', etc.) |
| 30 | @param fs_tag: A short name for this filesystem test to use |
| 31 | in the results. |
| 32 | @param mkfs_flags: Optional. Additional command line options to mkfs. |
| 33 | @param mount_options: Optional. The options to pass to mount -o. |
| 34 | """ |
| 35 | |
| 36 | if not fstype or not fs_tag: |
| 37 | raise ValueError('A filesystem and fs_tag are required.') |
| 38 | self.fstype = fstype |
| 39 | self.fs_tag = fs_tag |
| 40 | self.mkfs_flags = mkfs_flags or "" |
| 41 | self.mount_options = mount_options or "" |
| 42 | |
| 43 | |
| 44 | def __str__(self): |
| 45 | val = ('FsOptions(fstype=%r, mkfs_flags=%r, ' |
| 46 | 'mount_options=%r, fs_tag=%r)' % |
| 47 | (self.fstype, self.mkfs_flags, |
| 48 | self.mount_options, self.fs_tag)) |
| 49 | return val |
| 50 | |
| 51 | |
| 52 | def partname_to_device(part): |
| 53 | """ Converts a partition name to its associated device """ |
| 54 | return os.path.join(os.sep, 'dev', part) |
| 55 | |
| 56 | |
| 57 | def list_mount_devices(): |
| 58 | devices = [] |
| 59 | # list mounted filesystems |
| 60 | for line in utils.system_output('mount').splitlines(): |
| 61 | devices.append(line.split()[0]) |
| 62 | # list mounted swap devices |
| 63 | for line in utils.system_output('swapon -s').splitlines(): |
| 64 | if line.startswith('/'): # skip header line |
| 65 | devices.append(line.split()[0]) |
| 66 | return devices |
| 67 | |
| 68 | |
| 69 | def list_mount_points(): |
| 70 | mountpoints = [] |
| 71 | for line in utils.system_output('mount').splitlines(): |
| 72 | mountpoints.append(line.split()[2]) |
| 73 | return mountpoints |
| 74 | |
| 75 | |
| 76 | def get_iosched_path(device_name, component): |
| 77 | return '/sys/block/%s/queue/%s' % (device_name, component) |
| 78 | |
| 79 | |
| 80 | def wipe_filesystem(job, mountpoint): |
| 81 | wipe_cmd = 'rm -rf %s/*' % mountpoint |
| 82 | try: |
| 83 | utils.system(wipe_cmd) |
| 84 | except: |
| 85 | job.record('FAIL', None, wipe_cmd, error.format_error()) |
| 86 | raise |
| 87 | else: |
| 88 | job.record('GOOD', None, wipe_cmd) |
| 89 | |
| 90 | |
| 91 | def is_linux_fs_type(device): |
| 92 | """ |
| 93 | Checks if specified partition is type 83 |
| 94 | |
| 95 | @param device: the device, e.g. /dev/sda3 |
| 96 | |
| 97 | @return: False if the supplied partition name is not type 83 linux, True |
| 98 | otherwise |
| 99 | """ |
| 100 | disk_device = device.rstrip('0123456789') |
| 101 | |
| 102 | # Parse fdisk output to get partition info. Ugly but it works. |
| 103 | fdisk_fd = os.popen("/sbin/fdisk -l -u '%s'" % disk_device) |
| 104 | fdisk_lines = fdisk_fd.readlines() |
| 105 | fdisk_fd.close() |
| 106 | for line in fdisk_lines: |
| 107 | if not line.startswith(device): |
| 108 | continue |
| 109 | info_tuple = line.split() |
| 110 | # The Id will be in one of two fields depending on if the boot flag |
| 111 | # was set. Caveat: this assumes no boot partition will be 83 blocks. |
| 112 | for fsinfo in info_tuple[4:6]: |
| 113 | if fsinfo == '83': # hex 83 is the linux fs partition type |
| 114 | return True |
| 115 | return False |
| 116 | |
| 117 | |
| 118 | def get_partition_list(job, min_blocks=0, filter_func=None, exclude_swap=True, |
| 119 | open_func=open): |
| 120 | """ |
| 121 | Get a list of partition objects for all disk partitions on the system. |
| 122 | |
| 123 | Loopback devices and unnumbered (whole disk) devices are always excluded. |
| 124 | |
| 125 | @param job: The job instance to pass to the partition object |
| 126 | constructor. |
| 127 | @param min_blocks: The minimum number of blocks for a partition to |
| 128 | be considered. |
| 129 | @param filter_func: A callable that returns True if a partition is |
| 130 | desired. It will be passed one parameter: |
| 131 | The partition name (hdc3, etc.). |
| 132 | Some useful filter functions are already defined in this module. |
| 133 | @param exclude_swap: If True any partition actively in use as a swap |
| 134 | device will be excluded. |
| 135 | @param __open: Reserved for unit testing. |
| 136 | |
| 137 | @return: A list of L{partition} objects. |
| 138 | """ |
| 139 | active_swap_devices = set() |
| 140 | if exclude_swap: |
| 141 | for swapline in open_func('/proc/swaps'): |
| 142 | if swapline.startswith('/'): |
| 143 | active_swap_devices.add(swapline.split()[0]) |
| 144 | |
| 145 | partitions = [] |
| 146 | for partline in open_func('/proc/partitions').readlines(): |
| 147 | fields = partline.strip().split() |
| 148 | if len(fields) != 4 or partline.startswith('major'): |
| 149 | continue |
| 150 | (major, minor, blocks, partname) = fields |
| 151 | blocks = int(blocks) |
| 152 | |
| 153 | # The partition name better end with a digit, else it's not a partition |
| 154 | if not partname[-1].isdigit(): |
| 155 | continue |
| 156 | |
| 157 | # We don't want the loopback device in the partition list |
| 158 | if 'loop' in partname: |
| 159 | continue |
| 160 | |
| 161 | device = partname_to_device(partname) |
| 162 | if exclude_swap and device in active_swap_devices: |
| 163 | logging.debug('Skipping %s - Active swap.' % partname) |
| 164 | continue |
| 165 | |
| 166 | if min_blocks and blocks < min_blocks: |
| 167 | logging.debug('Skipping %s - Too small.' % partname) |
| 168 | continue |
| 169 | |
| 170 | if filter_func and not filter_func(partname): |
| 171 | logging.debug('Skipping %s - Filter func.' % partname) |
| 172 | continue |
| 173 | |
| 174 | partitions.append(partition(job, device)) |
| 175 | |
| 176 | return partitions |
| 177 | |
| 178 | |
Eric Li | e0493a4 | 2010-11-15 13:05:43 -0800 | [diff] [blame] | 179 | def get_mount_info(partition_list): |
| 180 | """ |
| 181 | Picks up mount point information about the machine mounts. By default, we |
| 182 | try to associate mount points with UUIDs, because in newer distros the |
| 183 | partitions are uniquely identified using them. |
| 184 | """ |
| 185 | mount_info = set() |
| 186 | for p in partition_list: |
| 187 | try: |
Eric Li | 861b2d5 | 2011-02-04 14:50:35 -0800 | [diff] [blame^] | 188 | uuid = utils.system_output('blkid -p -s UUID -o value %s' % p.device) |
Eric Li | e0493a4 | 2010-11-15 13:05:43 -0800 | [diff] [blame] | 189 | except error.CmdError: |
| 190 | # fall back to using the partition |
| 191 | uuid = p.device |
| 192 | mount_info.add((uuid, p.get_mountpoint())) |
| 193 | |
| 194 | return mount_info |
| 195 | |
| 196 | |
jadmanski | 543dceb | 2010-07-30 21:42:56 +0000 | [diff] [blame] | 197 | def filter_partition_list(partitions, devnames): |
| 198 | """ |
| 199 | Pick and choose which partition to keep. |
| 200 | |
| 201 | filter_partition_list accepts a list of partition objects and a list |
| 202 | of strings. If a partition has the device name of the strings it |
| 203 | is returned in a list. |
| 204 | |
| 205 | @param partitions: A list of L{partition} objects |
| 206 | @param devnames: A list of devnames of the form '/dev/hdc3' that |
| 207 | specifies which partitions to include in the returned list. |
| 208 | |
| 209 | @return: A list of L{partition} objects specified by devnames, in the |
| 210 | order devnames specified |
| 211 | """ |
| 212 | |
| 213 | filtered_list = [] |
| 214 | for p in partitions: |
| 215 | for d in devnames: |
| 216 | if p.device == d and p not in filtered_list: |
| 217 | filtered_list.append(p) |
| 218 | |
| 219 | return filtered_list |
| 220 | |
| 221 | |
| 222 | def get_unmounted_partition_list(root_part, job=None, min_blocks=0, |
| 223 | filter_func=None, exclude_swap=True, |
| 224 | open_func=open): |
| 225 | """ |
| 226 | Return a list of partition objects that are not mounted. |
| 227 | |
| 228 | @param root_part: The root device name (without the '/dev/' prefix, example |
| 229 | 'hda2') that will be filtered from the partition list. |
| 230 | |
| 231 | Reasoning: in Linux /proc/mounts will never directly mention the |
| 232 | root partition as being mounted on / instead it will say that |
| 233 | /dev/root is mounted on /. Thus require this argument to filter out |
| 234 | the root_part from the ones checked to be mounted. |
| 235 | @param job, min_blocks, filter_func, exclude_swap, open_func: Forwarded |
| 236 | to get_partition_list(). |
| 237 | @return List of L{partition} objects that are not mounted. |
| 238 | """ |
| 239 | partitions = get_partition_list(job=job, min_blocks=min_blocks, |
| 240 | filter_func=filter_func, exclude_swap=exclude_swap, open_func=open_func) |
| 241 | |
| 242 | unmounted = [] |
| 243 | for part in partitions: |
| 244 | if (part.device != partname_to_device(root_part) and |
| 245 | not part.get_mountpoint(open_func=open_func)): |
| 246 | unmounted.append(part) |
| 247 | |
| 248 | return unmounted |
| 249 | |
| 250 | |
| 251 | def parallel(partitions, method_name, *args, **dargs): |
| 252 | """ |
| 253 | Run a partition method (with appropriate arguments) in parallel, |
| 254 | across a list of partition objects |
| 255 | """ |
| 256 | if not partitions: |
| 257 | return |
| 258 | job = partitions[0].job |
| 259 | flist = [] |
| 260 | if (not hasattr(partition, method_name) or |
| 261 | not callable(getattr(partition, method_name))): |
| 262 | err = "partition.parallel got invalid method %s" % method_name |
| 263 | raise RuntimeError(err) |
| 264 | |
| 265 | for p in partitions: |
| 266 | print_args = list(args) |
| 267 | print_args += ['%s=%s' % (key, dargs[key]) for key in dargs.keys()] |
| 268 | logging.debug('%s.%s(%s)' % (str(p), method_name, |
| 269 | ', '.join(print_args))) |
| 270 | sys.stdout.flush() |
| 271 | def _run_named_method(function, part=p): |
| 272 | getattr(part, method_name)(*args, **dargs) |
| 273 | flist.append((_run_named_method, ())) |
| 274 | job.parallel(*flist) |
| 275 | |
| 276 | |
| 277 | def filesystems(): |
| 278 | """ |
| 279 | Return a list of all available filesystems |
| 280 | """ |
| 281 | return [re.sub('(nodev)?\s*', '', fs) for fs in open('/proc/filesystems')] |
| 282 | |
| 283 | |
| 284 | def unmount_partition(device): |
| 285 | """ |
| 286 | Unmount a mounted partition |
| 287 | |
| 288 | @param device: e.g. /dev/sda1, /dev/hda1 |
| 289 | """ |
| 290 | p = partition(job=None, device=device) |
| 291 | p.unmount(record=False) |
| 292 | |
| 293 | |
| 294 | def is_valid_partition(device): |
| 295 | """ |
| 296 | Checks if a partition is valid |
| 297 | |
| 298 | @param device: e.g. /dev/sda1, /dev/hda1 |
| 299 | """ |
| 300 | parts = get_partition_list(job=None) |
| 301 | p_list = [ p.device for p in parts ] |
| 302 | if device in p_list: |
| 303 | return True |
| 304 | |
| 305 | return False |
| 306 | |
| 307 | |
| 308 | def is_valid_disk(device): |
| 309 | """ |
| 310 | Checks if a disk is valid |
| 311 | |
| 312 | @param device: e.g. /dev/sda, /dev/hda |
| 313 | """ |
| 314 | partitions = [] |
| 315 | for partline in open('/proc/partitions').readlines(): |
| 316 | fields = partline.strip().split() |
| 317 | if len(fields) != 4 or partline.startswith('major'): |
| 318 | continue |
| 319 | (major, minor, blocks, partname) = fields |
| 320 | blocks = int(blocks) |
| 321 | |
| 322 | if not partname[-1].isdigit(): |
| 323 | # Disk name does not end in number, AFAIK |
| 324 | # so use it as a reference to a disk |
| 325 | if device.strip("/dev/") == partname: |
| 326 | return True |
| 327 | |
| 328 | return False |
| 329 | |
| 330 | |
| 331 | def run_test_on_partitions(job, test, partitions, mountpoint_func, |
Eric Li | e0493a4 | 2010-11-15 13:05:43 -0800 | [diff] [blame] | 332 | tag, fs_opt, do_fsck=True, **dargs): |
jadmanski | 543dceb | 2010-07-30 21:42:56 +0000 | [diff] [blame] | 333 | """ |
| 334 | Run a test that requires multiple partitions. Filesystems will be |
| 335 | made on the partitions and mounted, then the test will run, then the |
Eric Li | e0493a4 | 2010-11-15 13:05:43 -0800 | [diff] [blame] | 336 | filesystems will be unmounted and optionally fsck'd. |
jadmanski | 543dceb | 2010-07-30 21:42:56 +0000 | [diff] [blame] | 337 | |
| 338 | @param job: A job instance to run the test |
| 339 | @param test: A string containing the name of the test |
| 340 | @param partitions: A list of partition objects, these are passed to the |
| 341 | test as partitions= |
| 342 | @param mountpoint_func: A callable that returns a mountpoint given a |
| 343 | partition instance |
| 344 | @param tag: A string tag to make this test unique (Required for control |
| 345 | files that make multiple calls to this routine with the same value |
| 346 | of 'test'.) |
| 347 | @param fs_opt: An FsOptions instance that describes what filesystem to make |
Eric Li | e0493a4 | 2010-11-15 13:05:43 -0800 | [diff] [blame] | 348 | @param do_fsck: include fsck in post-test partition cleanup. |
jadmanski | 543dceb | 2010-07-30 21:42:56 +0000 | [diff] [blame] | 349 | @param dargs: Dictionary of arguments to be passed to job.run_test() and |
| 350 | eventually the test |
| 351 | """ |
| 352 | # setup the filesystem parameters for all the partitions |
| 353 | for p in partitions: |
| 354 | p.set_fs_options(fs_opt) |
| 355 | |
| 356 | # make and mount all the partitions in parallel |
| 357 | parallel(partitions, 'setup_before_test', mountpoint_func=mountpoint_func) |
| 358 | |
| 359 | mountpoint = mountpoint_func(partitions[0]) |
| 360 | |
| 361 | # run the test against all the partitions |
| 362 | job.run_test(test, tag=tag, partitions=partitions, dir=mountpoint, **dargs) |
| 363 | |
Eric Li | e0493a4 | 2010-11-15 13:05:43 -0800 | [diff] [blame] | 364 | parallel(partitions, 'unmount') # unmount all partitions in parallel |
| 365 | if do_fsck: |
| 366 | parallel(partitions, 'fsck') # fsck all partitions in parallel |
| 367 | # else fsck is done by caller |
jadmanski | 543dceb | 2010-07-30 21:42:56 +0000 | [diff] [blame] | 368 | |
| 369 | |
| 370 | class partition(object): |
| 371 | """ |
| 372 | Class for handling partitions and filesystems |
| 373 | """ |
| 374 | |
| 375 | def __init__(self, job, device, loop_size=0, mountpoint=None): |
| 376 | """ |
| 377 | @param job: A L{client.bin.job} instance. |
| 378 | @param device: The device in question (e.g."/dev/hda2"). If device is a |
| 379 | file it will be mounted as loopback. If you have job config |
| 380 | 'partition.partitions', e.g., |
| 381 | job.config_set('partition.partitions', ["/dev/sda2", "/dev/sda3"]) |
| 382 | you may specify a partition in the form of "partN" e.g. "part0", |
| 383 | "part1" to refer to elements of the partition list. This is |
| 384 | specially useful if you run a test in various machines and you |
| 385 | don't want to hardcode device names as those may vary. |
| 386 | @param loop_size: Size of loopback device (in MB). Defaults to 0. |
| 387 | """ |
| 388 | # NOTE: This code is used by IBM / ABAT. Do not remove. |
| 389 | part = re.compile(r'^part(\d+)$') |
| 390 | m = part.match(device) |
| 391 | if m: |
| 392 | number = int(m.groups()[0]) |
| 393 | partitions = job.config_get('partition.partitions') |
| 394 | try: |
| 395 | device = partitions[number] |
| 396 | except: |
| 397 | raise NameError("Partition '" + device + "' not available") |
| 398 | |
| 399 | self.device = device |
| 400 | self.name = os.path.basename(device) |
| 401 | self.job = job |
| 402 | self.loop = loop_size |
| 403 | self.fstype = None |
| 404 | self.mountpoint = mountpoint |
| 405 | self.mkfs_flags = None |
| 406 | self.mount_options = None |
| 407 | self.fs_tag = None |
| 408 | if self.loop: |
| 409 | cmd = 'dd if=/dev/zero of=%s bs=1M count=%d' % (device, loop_size) |
| 410 | utils.system(cmd) |
| 411 | |
| 412 | |
| 413 | def __repr__(self): |
| 414 | return '<Partition: %s>' % self.device |
| 415 | |
| 416 | |
| 417 | def set_fs_options(self, fs_options): |
| 418 | """ |
| 419 | Set filesystem options |
| 420 | |
| 421 | @param fs_options: A L{FsOptions} object |
| 422 | """ |
| 423 | |
| 424 | self.fstype = fs_options.fstype |
| 425 | self.mkfs_flags = fs_options.mkfs_flags |
| 426 | self.mount_options = fs_options.mount_options |
| 427 | self.fs_tag = fs_options.fs_tag |
| 428 | |
| 429 | |
| 430 | def run_test(self, test, **dargs): |
| 431 | self.job.run_test(test, dir=self.get_mountpoint(), **dargs) |
| 432 | |
| 433 | |
| 434 | def setup_before_test(self, mountpoint_func): |
| 435 | """ |
| 436 | Prepare a partition for running a test. Unmounts any |
| 437 | filesystem that's currently mounted on the partition, makes a |
| 438 | new filesystem (according to this partition's filesystem |
| 439 | options) and mounts it where directed by mountpoint_func. |
| 440 | |
| 441 | @param mountpoint_func: A callable that returns a path as a string, |
| 442 | given a partition instance. |
| 443 | """ |
| 444 | mountpoint = mountpoint_func(self) |
| 445 | if not mountpoint: |
| 446 | raise ValueError('Don\'t know where to put this partition') |
| 447 | self.unmount(ignore_status=True, record=False) |
| 448 | self.mkfs() |
| 449 | if not os.path.isdir(mountpoint): |
| 450 | os.makedirs(mountpoint) |
| 451 | self.mount(mountpoint) |
| 452 | |
| 453 | |
jadmanski | 543dceb | 2010-07-30 21:42:56 +0000 | [diff] [blame] | 454 | def run_test_on_partition(self, test, mountpoint_func, **dargs): |
| 455 | """ |
| 456 | Executes a test fs-style (umount,mkfs,mount,test) |
| 457 | |
| 458 | Here we unmarshal the args to set up tags before running the test. |
| 459 | Tests are also run by first umounting, mkfsing and then mounting |
| 460 | before executing the test. |
| 461 | |
| 462 | @param test: name of test to run |
| 463 | @param mountpoint_func: function to return mount point string |
| 464 | """ |
| 465 | tag = dargs.get('tag') |
| 466 | if tag: |
| 467 | tag = '%s.%s' % (self.name, tag) |
| 468 | elif self.fs_tag: |
| 469 | tag = '%s.%s' % (self.name, self.fs_tag) |
| 470 | else: |
| 471 | tag = self.name |
| 472 | |
| 473 | # If there's a 'suffix' argument, append it to the tag and remove it |
| 474 | suffix = dargs.pop('suffix', None) |
| 475 | if suffix: |
| 476 | tag = '%s.%s' % (tag, suffix) |
| 477 | |
| 478 | dargs['tag'] = test + '.' + tag |
| 479 | |
| 480 | def _make_partition_and_run_test(test_tag, dir=None, **dargs): |
| 481 | self.setup_before_test(mountpoint_func) |
| 482 | try: |
| 483 | self.job.run_test(test, tag=test_tag, dir=mountpoint, **dargs) |
| 484 | finally: |
Eric Li | e0493a4 | 2010-11-15 13:05:43 -0800 | [diff] [blame] | 485 | self.unmount() |
| 486 | self.fsck() |
| 487 | |
jadmanski | 543dceb | 2010-07-30 21:42:56 +0000 | [diff] [blame] | 488 | |
| 489 | mountpoint = mountpoint_func(self) |
| 490 | |
| 491 | # The tag is the tag for the group (get stripped off by run_group) |
| 492 | # The test_tag is the tag for the test itself |
| 493 | self.job.run_group(_make_partition_and_run_test, |
| 494 | test_tag=tag, dir=mountpoint, **dargs) |
| 495 | |
| 496 | |
| 497 | def get_mountpoint(self, open_func=open, filename=None): |
| 498 | """ |
| 499 | Find the mount point of this partition object. |
| 500 | |
| 501 | @param open_func: the function to use for opening the file containing |
| 502 | the mounted partitions information |
| 503 | @param filename: where to look for the mounted partitions information |
| 504 | (default None which means it will search /proc/mounts and/or |
| 505 | /etc/mtab) |
| 506 | |
| 507 | @returns a string with the mount point of the partition or None if not |
| 508 | mounted |
| 509 | """ |
| 510 | if filename: |
| 511 | for line in open_func(filename).readlines(): |
| 512 | parts = line.split() |
Eric Li | e0493a4 | 2010-11-15 13:05:43 -0800 | [diff] [blame] | 513 | if parts[0] == self.device or parts[1] == self.mountpoint: |
jadmanski | 543dceb | 2010-07-30 21:42:56 +0000 | [diff] [blame] | 514 | return parts[1] # The mountpoint where it's mounted |
| 515 | return None |
| 516 | |
| 517 | # no specific file given, look in /proc/mounts |
| 518 | res = self.get_mountpoint(open_func=open_func, filename='/proc/mounts') |
| 519 | if not res: |
| 520 | # sometimes the root partition is reported as /dev/root in |
| 521 | # /proc/mounts in this case, try /etc/mtab |
| 522 | res = self.get_mountpoint(open_func=open_func, filename='/etc/mtab') |
| 523 | |
| 524 | # trust /etc/mtab only about / |
| 525 | if res != '/': |
| 526 | res = None |
| 527 | |
| 528 | return res |
| 529 | |
| 530 | |
| 531 | def mkfs_exec(self, fstype): |
| 532 | """ |
| 533 | Return the proper mkfs executable based on fs |
| 534 | """ |
| 535 | if fstype == 'ext4': |
| 536 | if os.path.exists('/sbin/mkfs.ext4'): |
| 537 | return 'mkfs' |
| 538 | # If ext4 supported e2fsprogs is not installed we use the |
| 539 | # autotest supplied one in tools dir which is statically linked""" |
| 540 | auto_mkfs = os.path.join(self.job.toolsdir, 'mkfs.ext4dev') |
| 541 | if os.path.exists(auto_mkfs): |
| 542 | return auto_mkfs |
| 543 | else: |
| 544 | return 'mkfs' |
| 545 | |
| 546 | raise NameError('Error creating partition for filesystem type %s' % |
| 547 | fstype) |
| 548 | |
| 549 | |
| 550 | def mkfs(self, fstype=None, args='', record=True): |
| 551 | """ |
| 552 | Format a partition to filesystem type |
| 553 | |
| 554 | @param fstype: the filesystem type, e.g.. "ext3", "ext2" |
| 555 | @param args: arguments to be passed to mkfs command. |
| 556 | @param record: if set, output result of mkfs operation to autotest |
| 557 | output |
| 558 | """ |
| 559 | |
| 560 | if list_mount_devices().count(self.device): |
| 561 | raise NameError('Attempted to format mounted device %s' % |
| 562 | self.device) |
| 563 | |
| 564 | if not fstype: |
| 565 | if self.fstype: |
| 566 | fstype = self.fstype |
| 567 | else: |
| 568 | fstype = 'ext2' |
| 569 | |
| 570 | if self.mkfs_flags: |
| 571 | args += ' ' + self.mkfs_flags |
| 572 | if fstype == 'xfs': |
| 573 | args += ' -f' |
| 574 | |
| 575 | if self.loop: |
| 576 | # BAH. Inconsistent mkfs syntax SUCKS. |
| 577 | if fstype.startswith('ext'): |
| 578 | args += ' -F' |
| 579 | elif fstype == 'reiserfs': |
| 580 | args += ' -f' |
| 581 | |
| 582 | # If there isn't already a '-t <type>' argument, add one. |
| 583 | if not "-t" in args: |
| 584 | args = "-t %s %s" % (fstype, args) |
| 585 | |
| 586 | args = args.strip() |
| 587 | |
| 588 | mkfs_cmd = "%s %s %s" % (self.mkfs_exec(fstype), args, self.device) |
| 589 | |
| 590 | sys.stdout.flush() |
| 591 | try: |
| 592 | # We throw away the output here - we only need it on error, in |
| 593 | # which case it's in the exception |
| 594 | utils.system_output("yes | %s" % mkfs_cmd) |
| 595 | except error.CmdError, e: |
| 596 | logging.error(e.result_obj) |
| 597 | if record: |
| 598 | self.job.record('FAIL', None, mkfs_cmd, error.format_error()) |
| 599 | raise |
| 600 | except: |
| 601 | if record: |
| 602 | self.job.record('FAIL', None, mkfs_cmd, error.format_error()) |
| 603 | raise |
| 604 | else: |
| 605 | if record: |
| 606 | self.job.record('GOOD', None, mkfs_cmd) |
| 607 | self.fstype = fstype |
| 608 | |
| 609 | |
| 610 | def get_fsck_exec(self): |
| 611 | """ |
| 612 | Return the proper mkfs executable based on self.fstype |
| 613 | """ |
| 614 | if self.fstype == 'ext4': |
| 615 | if os.path.exists('/sbin/fsck.ext4'): |
| 616 | return 'fsck' |
| 617 | # If ext4 supported e2fsprogs is not installed we use the |
| 618 | # autotest supplied one in tools dir which is statically linked""" |
| 619 | auto_fsck = os.path.join(self.job.toolsdir, 'fsck.ext4dev') |
| 620 | if os.path.exists(auto_fsck): |
| 621 | return auto_fsck |
| 622 | else: |
| 623 | return 'fsck' |
| 624 | |
| 625 | raise NameError('Error creating partition for filesystem type %s' % |
| 626 | self.fstype) |
| 627 | |
| 628 | |
| 629 | def fsck(self, args='-fy', record=True): |
| 630 | """ |
| 631 | Run filesystem check |
| 632 | |
| 633 | @param args: arguments to filesystem check tool. Default is "-n" |
| 634 | which works on most tools. |
| 635 | """ |
| 636 | |
| 637 | # I hate reiserfstools. |
| 638 | # Requires an explit Yes for some inane reason |
| 639 | fsck_cmd = '%s %s %s' % (self.get_fsck_exec(), self.device, args) |
| 640 | if self.fstype == 'reiserfs': |
| 641 | fsck_cmd = 'yes "Yes" | ' + fsck_cmd |
| 642 | sys.stdout.flush() |
| 643 | try: |
| 644 | utils.system_output(fsck_cmd) |
| 645 | except: |
| 646 | if record: |
| 647 | self.job.record('FAIL', None, fsck_cmd, error.format_error()) |
| 648 | raise error.TestError('Fsck found errors with the underlying ' |
| 649 | 'file system') |
| 650 | else: |
| 651 | if record: |
| 652 | self.job.record('GOOD', None, fsck_cmd) |
| 653 | |
| 654 | |
Eric Li | 6f27d4f | 2010-09-29 10:55:17 -0700 | [diff] [blame] | 655 | def mount(self, mountpoint=None, fstype=None, args='', record=True): |
jadmanski | 543dceb | 2010-07-30 21:42:56 +0000 | [diff] [blame] | 656 | """ |
| 657 | Mount this partition to a mount point |
| 658 | |
| 659 | @param mountpoint: If you have not provided a mountpoint to partition |
| 660 | object or want to use a different one, you may specify it here. |
| 661 | @param fstype: Filesystem type. If not provided partition object value |
| 662 | will be used. |
| 663 | @param args: Arguments to be passed to "mount" command. |
| 664 | @param record: If True, output result of mount operation to autotest |
| 665 | output. |
| 666 | """ |
| 667 | |
| 668 | if fstype is None: |
| 669 | fstype = self.fstype |
| 670 | else: |
| 671 | assert(self.fstype is None or self.fstype == fstype); |
| 672 | |
| 673 | if self.mount_options: |
| 674 | args += ' -o ' + self.mount_options |
| 675 | if fstype: |
| 676 | args += ' -t ' + fstype |
| 677 | if self.loop: |
| 678 | args += ' -o loop' |
| 679 | args = args.lstrip() |
| 680 | |
| 681 | if not mountpoint and not self.mountpoint: |
| 682 | raise ValueError("No mountpoint specified and no default " |
| 683 | "provided to this partition object") |
| 684 | if not mountpoint: |
| 685 | mountpoint = self.mountpoint |
| 686 | |
| 687 | mount_cmd = "mount %s %s %s" % (args, self.device, mountpoint) |
| 688 | |
| 689 | if list_mount_devices().count(self.device): |
| 690 | err = 'Attempted to mount mounted device' |
| 691 | self.job.record('FAIL', None, mount_cmd, err) |
| 692 | raise NameError(err) |
| 693 | if list_mount_points().count(mountpoint): |
| 694 | err = 'Attempted to mount busy mountpoint' |
| 695 | self.job.record('FAIL', None, mount_cmd, err) |
| 696 | raise NameError(err) |
| 697 | |
| 698 | mtab = open('/etc/mtab') |
| 699 | # We have to get an exclusive lock here - mount/umount are racy |
| 700 | fcntl.flock(mtab.fileno(), fcntl.LOCK_EX) |
| 701 | sys.stdout.flush() |
| 702 | try: |
| 703 | utils.system(mount_cmd) |
| 704 | mtab.close() |
| 705 | except: |
| 706 | mtab.close() |
| 707 | if record: |
| 708 | self.job.record('FAIL', None, mount_cmd, error.format_error()) |
| 709 | raise |
| 710 | else: |
| 711 | if record: |
| 712 | self.job.record('GOOD', None, mount_cmd) |
| 713 | self.fstype = fstype |
| 714 | |
| 715 | |
| 716 | def unmount_force(self): |
| 717 | """ |
| 718 | Kill all other jobs accessing this partition. Use fuser and ps to find |
| 719 | all mounts on this mountpoint and unmount them. |
| 720 | |
| 721 | @return: true for success or false for any errors |
| 722 | """ |
| 723 | |
| 724 | logging.debug("Standard umount failed, will try forcing. Users:") |
| 725 | try: |
| 726 | cmd = 'fuser ' + self.get_mountpoint() |
| 727 | logging.debug(cmd) |
| 728 | fuser = utils.system_output(cmd) |
| 729 | logging.debug(fuser) |
| 730 | users = re.sub('.*:', '', fuser).split() |
| 731 | for user in users: |
| 732 | m = re.match('(\d+)(.*)', user) |
| 733 | (pid, usage) = (m.group(1), m.group(2)) |
| 734 | try: |
| 735 | ps = utils.system_output('ps -p %s | sed 1d' % pid) |
| 736 | logging.debug('%s %s %s' % (usage, pid, ps)) |
| 737 | except Exception: |
| 738 | pass |
| 739 | utils.system('ls -l ' + self.device) |
| 740 | umount_cmd = "umount -f " + self.device |
| 741 | utils.system(umount_cmd) |
| 742 | return True |
| 743 | except error.CmdError: |
| 744 | logging.debug('Umount_force failed for %s' % self.device) |
| 745 | return False |
| 746 | |
| 747 | |
| 748 | |
| 749 | def unmount(self, ignore_status=False, record=True): |
| 750 | """ |
| 751 | Umount this partition. |
| 752 | |
| 753 | It's easier said than done to umount a partition. |
| 754 | We need to lock the mtab file to make sure we don't have any |
| 755 | locking problems if we are umounting in paralllel. |
| 756 | |
| 757 | If there turns out to be a problem with the simple umount we |
| 758 | end up calling umount_force to get more agressive. |
| 759 | |
| 760 | @param ignore_status: should we notice the umount status |
| 761 | @param record: if True, output result of umount operation to |
| 762 | autotest output |
| 763 | """ |
| 764 | |
| 765 | mountpoint = self.get_mountpoint() |
| 766 | if not mountpoint: |
| 767 | # It's not even mounted to start with |
| 768 | if record and not ignore_status: |
| 769 | msg = 'umount for dev %s has no mountpoint' % self.device |
| 770 | self.job.record('FAIL', None, msg, 'Not mounted') |
| 771 | return |
| 772 | |
| 773 | umount_cmd = "umount " + mountpoint |
| 774 | mtab = open('/etc/mtab') |
| 775 | |
| 776 | # We have to get an exclusive lock here - mount/umount are racy |
| 777 | fcntl.flock(mtab.fileno(), fcntl.LOCK_EX) |
| 778 | sys.stdout.flush() |
| 779 | try: |
| 780 | utils.system(umount_cmd) |
| 781 | mtab.close() |
| 782 | if record: |
| 783 | self.job.record('GOOD', None, umount_cmd) |
| 784 | except (error.CmdError, IOError): |
| 785 | mtab.close() |
| 786 | |
| 787 | # Try the forceful umount |
| 788 | if self.unmount_force(): |
| 789 | return |
| 790 | |
| 791 | # If we are here we cannot umount this partition |
| 792 | if record and not ignore_status: |
| 793 | self.job.record('FAIL', None, umount_cmd, error.format_error()) |
| 794 | raise |
| 795 | |
| 796 | |
| 797 | def wipe(self): |
| 798 | """ |
| 799 | Delete all files of a given partition filesystem. |
| 800 | """ |
| 801 | wipe_filesystem(self.job, self.get_mountpoint()) |
| 802 | |
| 803 | |
| 804 | def get_io_scheduler_list(self, device_name): |
| 805 | names = open(self.__sched_path(device_name)).read() |
| 806 | return names.translate(string.maketrans('[]', ' ')).split() |
| 807 | |
| 808 | |
| 809 | def get_io_scheduler(self, device_name): |
| 810 | return re.split('[\[\]]', |
| 811 | open(self.__sched_path(device_name)).read())[1] |
| 812 | |
| 813 | |
| 814 | def set_io_scheduler(self, device_name, name): |
| 815 | if name not in self.get_io_scheduler_list(device_name): |
| 816 | raise NameError('No such IO scheduler: %s' % name) |
| 817 | f = open(self.__sched_path(device_name), 'w') |
| 818 | f.write(name) |
| 819 | f.close() |
| 820 | |
| 821 | |
| 822 | def __sched_path(self, device_name): |
| 823 | return '/sys/block/%s/queue/scheduler' % device_name |
| 824 | |
| 825 | |
| 826 | class virtual_partition: |
| 827 | """ |
| 828 | Handles block device emulation using file images of disks. |
| 829 | It's important to note that this API can be used only if |
| 830 | we have the following programs present on the client machine: |
| 831 | |
| 832 | * sfdisk |
| 833 | * losetup |
| 834 | * kpartx |
| 835 | """ |
| 836 | def __init__(self, file_img, file_size): |
| 837 | """ |
| 838 | Creates a virtual partition, keeping record of the device created |
| 839 | under /dev/mapper (device attribute) so test writers can use it |
| 840 | on their filesystem tests. |
| 841 | |
| 842 | @param file_img: Path to the desired disk image file. |
| 843 | @param file_size: Size of the desired image in Bytes. |
| 844 | """ |
| 845 | logging.debug('Sanity check before attempting to create virtual ' |
| 846 | 'partition') |
| 847 | try: |
| 848 | os_dep.commands('sfdisk', 'losetup', 'kpartx') |
| 849 | except ValueError, e: |
| 850 | e_msg = 'Unable to create virtual partition: %s' % e |
| 851 | raise error.AutotestError(e_msg) |
| 852 | |
| 853 | logging.debug('Creating virtual partition') |
| 854 | self.img = self._create_disk_img(file_img, file_size) |
| 855 | self.loop = self._attach_img_loop(self.img) |
| 856 | self._create_single_partition(self.loop) |
| 857 | self.device = self._create_entries_partition(self.loop) |
| 858 | logging.debug('Virtual partition successfuly created') |
| 859 | logging.debug('Image disk: %s', self.img) |
| 860 | logging.debug('Loopback device: %s', self.loop) |
| 861 | logging.debug('Device path: %s', self.device) |
| 862 | |
| 863 | |
| 864 | def destroy(self): |
| 865 | """ |
| 866 | Removes the virtual partition from /dev/mapper, detaches the image file |
| 867 | from the loopback device and removes the image file. |
| 868 | """ |
| 869 | logging.debug('Removing virtual partition - device %s', self.device) |
| 870 | self._remove_entries_partition() |
| 871 | self._detach_img_loop() |
| 872 | self._remove_disk_img() |
| 873 | |
| 874 | |
| 875 | def _create_disk_img(self, img_path, size): |
| 876 | """ |
| 877 | Creates a disk image using dd. |
| 878 | |
| 879 | @param img_path: Path to the desired image file. |
| 880 | @param size: Size of the desired image in Bytes. |
| 881 | @returns: Path of the image created. |
| 882 | """ |
| 883 | logging.debug('Creating disk image %s, size = %d Bytes', img_path, size) |
| 884 | try: |
| 885 | cmd = 'dd if=/dev/zero of=%s bs=1024 count=%d' % (img_path, size) |
| 886 | utils.system(cmd) |
| 887 | except error.CmdError, e: |
| 888 | e_msg = 'Error creating disk image %s: %s' % (img_path, e) |
| 889 | raise error.AutotestError(e_msg) |
| 890 | return img_path |
| 891 | |
| 892 | |
| 893 | def _attach_img_loop(self, img_path): |
| 894 | """ |
| 895 | Attaches a file image to a loopback device using losetup. |
| 896 | |
| 897 | @param img_path: Path of the image file that will be attached to a |
| 898 | loopback device |
| 899 | @returns: Path of the loopback device associated. |
| 900 | """ |
| 901 | logging.debug('Attaching image %s to a loop device', img_path) |
| 902 | try: |
| 903 | cmd = 'losetup -f' |
| 904 | loop_path = utils.system_output(cmd) |
| 905 | cmd = 'losetup -f %s' % img_path |
| 906 | utils.system(cmd) |
| 907 | except error.CmdError, e: |
| 908 | e_msg = 'Error attaching image %s to a loop device: %s' % \ |
| 909 | (img_path, e) |
| 910 | raise error.AutotestError(e_msg) |
| 911 | return loop_path |
| 912 | |
| 913 | |
| 914 | def _create_single_partition(self, loop_path): |
| 915 | """ |
| 916 | Creates a single partition encompassing the whole 'disk' using cfdisk. |
| 917 | |
| 918 | @param loop_path: Path to the loopback device. |
| 919 | """ |
| 920 | logging.debug('Creating single partition on %s', loop_path) |
| 921 | try: |
| 922 | single_part_cmd = '0,,c\n' |
| 923 | sfdisk_file_path = '/tmp/create_partition.sfdisk' |
| 924 | sfdisk_cmd_file = open(sfdisk_file_path, 'w') |
| 925 | sfdisk_cmd_file.write(single_part_cmd) |
| 926 | sfdisk_cmd_file.close() |
| 927 | utils.system('sfdisk %s < %s' % (loop_path, sfdisk_file_path)) |
| 928 | except error.CmdError, e: |
| 929 | e_msg = 'Error partitioning device %s: %s' % (loop_path, e) |
| 930 | raise error.AutotestError(e_msg) |
| 931 | |
| 932 | |
| 933 | def _create_entries_partition(self, loop_path): |
| 934 | """ |
| 935 | Takes the newly created partition table on the loopback device and |
| 936 | makes all its devices available under /dev/mapper. As we previously |
| 937 | have partitioned it using a single partition, only one partition |
| 938 | will be returned. |
| 939 | |
| 940 | @param loop_path: Path to the loopback device. |
| 941 | """ |
| 942 | logging.debug('Creating entries under /dev/mapper for %s loop dev', |
| 943 | loop_path) |
| 944 | try: |
| 945 | cmd = 'kpartx -a %s' % loop_path |
| 946 | utils.system(cmd) |
| 947 | l_cmd = 'kpartx -l %s | cut -f1 -d " "' % loop_path |
| 948 | device = utils.system_output(l_cmd) |
| 949 | except error.CmdError, e: |
| 950 | e_msg = 'Error creating entries for %s: %s' % (loop_path, e) |
| 951 | raise error.AutotestError(e_msg) |
| 952 | return os.path.join('/dev/mapper', device) |
| 953 | |
| 954 | |
| 955 | def _remove_entries_partition(self): |
| 956 | """ |
| 957 | Removes the entries under /dev/mapper for the partition associated |
| 958 | to the loopback device. |
| 959 | """ |
| 960 | logging.debug('Removing the entry on /dev/mapper for %s loop dev', |
| 961 | self.loop) |
| 962 | try: |
| 963 | cmd = 'kpartx -d %s' % self.loop |
| 964 | utils.system(cmd) |
| 965 | except error.CmdError, e: |
| 966 | e_msg = 'Error removing entries for loop %s: %s' % (self.loop, e) |
| 967 | raise error.AutotestError(e_msg) |
| 968 | |
| 969 | |
| 970 | def _detach_img_loop(self): |
| 971 | """ |
| 972 | Detaches the image file from the loopback device. |
| 973 | """ |
| 974 | logging.debug('Detaching image %s from loop device %s', self.img, |
| 975 | self.loop) |
| 976 | try: |
| 977 | cmd = 'losetup -d %s' % self.loop |
| 978 | utils.system(cmd) |
| 979 | except error.CmdError, e: |
| 980 | e_msg = ('Error detaching image %s from loop device %s: %s' % |
| 981 | (self.img, self.loop, e)) |
| 982 | raise error.AutotestError(e_msg) |
| 983 | |
| 984 | |
| 985 | def _remove_disk_img(self): |
| 986 | """ |
| 987 | Removes the disk image. |
| 988 | """ |
| 989 | logging.debug('Removing disk image %s', self.img) |
| 990 | try: |
| 991 | os.remove(self.img) |
| 992 | except: |
| 993 | e_msg = 'Error removing image file %s' % self.img |
| 994 | raise error.AutotestError(e_msg) |
| 995 | |
| 996 | # import a site partition module to allow it to override functions |
jadmanski | 69bdaac | 2010-07-28 16:27:20 +0000 | [diff] [blame] | 997 | try: |
| 998 | from autotest_lib.client.bin.site_partition import * |
| 999 | except ImportError: |
| 1000 | pass |