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