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