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