partition.py revision bdaab795cffa33f9a37995bb283047cb03699272
1"""
2APIs to write tests and control files that handle partition creation, deletion
3and formatting.
4
5@copyright: Google 2006-2008
6@author: Martin Bligh (mbligh@google.com)
7"""
8
9import os, re, string, sys, fcntl, logging
10from autotest_lib.client.bin import os_dep, utils
11from autotest_lib.client.common_lib import error
12
13
14class 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
52def partname_to_device(part):
53    """ Converts a partition name to its associated device """
54    return os.path.join(os.sep, 'dev', part)
55
56
57def 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
69def 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
76def get_iosched_path(device_name, component):
77    return '/sys/block/%s/queue/%s' % (device_name, component)
78
79
80def 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
91def 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
118def 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
179def 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:
188            uuid = utils.system_output('blkid -s UUID -o value %s' % p.device)
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
197def 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
222def 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
251def 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
277def 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
284def 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
294def 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
308def 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
331def run_test_on_partitions(job, test, partitions, mountpoint_func,
332                           tag, fs_opt, do_fsck=True, **dargs):
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
336    filesystems will be unmounted and optionally fsck'd.
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
348    @param do_fsck: include fsck in post-test partition cleanup.
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
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
368
369
370class 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
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:
485                self.unmount()
486                self.fsck()
487
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()
513                if parts[0] == self.device or parts[1] == self.mountpoint:
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
655    def mount(self, mountpoint=None, fstype=None, args='', record=True):
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
826class 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
997try:
998    from autotest_lib.client.bin.site_partition import *
999except ImportError:
1000    pass
1001