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