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