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