diff options
| author | Scott Moser <smoser@ubuntu.com> | 2013-03-05 16:39:23 -0500 | 
|---|---|---|
| committer | Scott Moser <smoser@ubuntu.com> | 2013-03-05 16:39:23 -0500 | 
| commit | 90ed3dee9672ba2756dd4a303f94e3de47e70404 (patch) | |
| tree | 0816042bc12aa7d59a76a4c566dfa406d52ce59c | |
| parent | b4fa42f0cb841b1f096bd8d654eda7230053935c (diff) | |
| parent | f9fe61cb0fff4391212c33ff5fc8af7402ad112c (diff) | |
| download | vyos-cloud-init-90ed3dee9672ba2756dd4a303f94e3de47e70404.tar.gz vyos-cloud-init-90ed3dee9672ba2756dd4a303f94e3de47e70404.zip | |
add 'growpart' config module.
This adds support for resizing partition tables for mounted partitions.
It thus allows us to remove 'cloud-initramfs-growpart' from running in
the initramfs, and do it here instead.
That depends on:
 a.) growpart in cloud-utils 0.2.7 or later
     or
     parted with 'resizepart' support
 b.) kernel 3.8.
| -rw-r--r-- | ChangeLog | 1 | ||||
| -rw-r--r-- | cloudinit/config/cc_growpart.py | 272 | ||||
| -rw-r--r-- | cloudinit/config/cc_resizefs.py | 85 | ||||
| -rw-r--r-- | cloudinit/util.py | 83 | ||||
| -rw-r--r-- | config/cloud.cfg | 1 | ||||
| -rw-r--r-- | doc/examples/cloud-config-growpart.txt | 24 | ||||
| -rw-r--r-- | tests/unittests/test_handler/test_handler_growpart.py | 253 | 
7 files changed, 635 insertions, 84 deletions
| @@ -46,6 +46,7 @@   - fix issue when writing ssh keys to .ssh/authorized_keys (LP: #1136343)   - upstart: cloud-init-nonet.conf trap the TERM signal, so that dmesg or other     output does not get a 'killed by TERM signal' message. + - support resizing partitions via growpart or parted (LP: #1136936)  0.7.1:   - sysvinit: fix missing dependency in cloud-init job for RHEL 5.6  diff --git a/cloudinit/config/cc_growpart.py b/cloudinit/config/cc_growpart.py new file mode 100644 index 00000000..b6e1fd37 --- /dev/null +++ b/cloudinit/config/cc_growpart.py @@ -0,0 +1,272 @@ +# vi: ts=4 expandtab +# +#    Copyright (C) 2011 Canonical Ltd. +# +#    Author: Scott Moser <scott.moser@canonical.com> +# +#    This program is free software: you can redistribute it and/or modify +#    it under the terms of the GNU General Public License version 3, as +#    published by the Free Software Foundation. +# +#    This program is distributed in the hope that it will be useful, +#    but WITHOUT ANY WARRANTY; without even the implied warranty of +#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +#    GNU General Public License for more details. +# +#    You should have received a copy of the GNU General Public License +#    along with this program.  If not, see <http://www.gnu.org/licenses/>. + +import os +import os.path +import re +import stat + +from cloudinit import log as logging +from cloudinit.settings import PER_ALWAYS +from cloudinit import util + +frequency = PER_ALWAYS + +DEFAULT_CONFIG = { +    'mode': 'auto', +    'devices': ['/'], +} + + +def enum(**enums): +    return type('Enum', (), enums) + + +RESIZE = enum(SKIPPED="SKIPPED", CHANGED="CHANGED", NOCHANGE="NOCHANGE", +              FAILED="FAILED") + +LOG = logging.getLogger(__name__) + + +def resizer_factory(mode): +    resize_class = None +    if mode == "auto": +        for (_name, resizer) in RESIZERS: +            cur = resizer() +            if cur.available(): +                resize_class = cur +                break + +        if not resize_class: +            raise ValueError("No resizers available") + +    else: +        mmap = {} +        for (k, v) in RESIZERS: +            mmap[k] = v + +        if mode not in mmap: +            raise TypeError("unknown resize mode %s" % mode) + +        mclass = mmap[mode]() +        if mclass.available(): +            resize_class = mclass + +        if not resize_class: +            raise ValueError("mode %s not available" % mode) + +    return resize_class + + +class ResizeFailedException(Exception): +    pass + + +class ResizeParted(object): +    def available(self): +        myenv = os.environ.copy() +        myenv['LANG'] = 'C' + +        try: +            (out, _err) = util.subp(["parted", "--help"], env=myenv) +            if re.search(r"COMMAND.*resizepart\s+", out, re.DOTALL): +                return True + +        except util.ProcessExecutionError: +            pass +        return False + +    def resize(self, diskdev, partnum, partdev): +        before = get_size(partdev) +        try: +            util.subp(["parted", "resizepart", diskdev, partnum]) +        except util.ProcessExecutionError as e: +            raise ResizeFailedException(e) + +        return (before, get_size(partdev)) + + +class ResizeGrowPart(object): +    def available(self): +        myenv = os.environ.copy() +        myenv['LANG'] = 'C' + +        try: +            (out, _err) = util.subp(["growpart", "--help"], env=myenv) +            if re.search(r"--update\s+", out, re.DOTALL): +                return True + +        except util.ProcessExecutionError: +            pass +        return False + +    def resize(self, diskdev, partnum, partdev): +        before = get_size(partdev) +        try: +            util.subp(["growpart", '--dry-run', diskdev, partnum]) +        except util.ProcessExecutionError as e: +            if e.exit_code != 1: +                util.logexc(LOG, ("Failed growpart --dry-run for (%s, %s)" % +                                  (diskdev, partnum))) +                raise ResizeFailedException(e) +            return (before, before) + +        try: +            util.subp(["growpart", diskdev, partnum]) +        except util.ProcessExecutionError as e: +            util.logexc(LOG, "Failed: growpart %s %s" % (diskdev, partnum)) +            raise ResizeFailedException(e) + +        return (before, get_size(partdev)) + + +def get_size(filename): +    fd = os.open(filename, os.O_RDONLY) +    try: +        return os.lseek(fd, 0, os.SEEK_END) +    finally: +        os.close(fd) + + +def device_part_info(devpath): +    # convert an entry in /dev/ to parent disk and partition number + +    # input of /dev/vdb or /dev/disk/by-label/foo +    # rpath is hopefully a real-ish path in /dev (vda, sdb..) +    rpath = os.path.realpath(devpath) + +    bname = os.path.basename(rpath) +    syspath = "/sys/class/block/%s" % bname + +    if not os.path.exists(syspath): +        raise ValueError("%s had no syspath (%s)" % (devpath, syspath)) + +    ptpath = os.path.join(syspath, "partition") +    if not os.path.exists(ptpath): +        raise TypeError("%s not a partition" % devpath) + +    ptnum = util.load_file(ptpath).rstrip() + +    # for a partition, real syspath is something like: +    # /sys/devices/pci0000:00/0000:00:04.0/virtio1/block/vda/vda1 +    rsyspath = os.path.realpath(syspath) +    disksyspath = os.path.dirname(rsyspath) + +    diskmajmin = util.load_file(os.path.join(disksyspath, "dev")).rstrip() +    diskdevpath = os.path.realpath("/dev/block/%s" % diskmajmin) + +    # diskdevpath has something like 253:0 +    # and udev has put links in /dev/block/253:0 to the device name in /dev/ +    return (diskdevpath, ptnum) + + +def devent2dev(devent): +    if devent.startswith("/dev/"): +        return devent +    else: +        result = util.get_mount_info(devent) +        if not result: +            raise ValueError("Could not determine device of '%s' % dev_ent") +        return result[0] + + +def resize_devices(resizer, devices): +    # returns a tuple of tuples containing (entry-in-devices, action, message) +    info = [] +    for devent in devices: +        try: +            blockdev = devent2dev(devent) +        except ValueError as e: +            info.append((devent, RESIZE.SKIPPED, +                         "unable to convert to device: %s" % e,)) +            continue + +        try: +            statret = os.stat(blockdev) +        except OSError as e: +            info.append((devent, RESIZE.SKIPPED, +                         "stat of '%s' failed: %s" % (blockdev, e),)) +            continue + +        if not stat.S_ISBLK(statret.st_mode): +            info.append((devent, RESIZE.SKIPPED, +                         "device '%s' not a block device" % blockdev,)) +            continue + +        try: +            (disk, ptnum) = device_part_info(blockdev) +        except (TypeError, ValueError) as e: +            info.append((devent, RESIZE.SKIPPED, +                         "device_part_info(%s) failed: %s" % (blockdev, e),)) +            continue + +        try: +            (old, new) = resizer.resize(disk, ptnum, blockdev) +            if old == new: +                info.append((devent, RESIZE.NOCHANGE, +                             "no change necessary (%s, %s)" % (disk, ptnum),)) +            else: +                info.append((devent, RESIZE.CHANGED, +                             "changed (%s, %s) from %s to %s" % +                             (disk, ptnum, old, new),)) + +        except ResizeFailedException as e: +            info.append((devent, RESIZE.FAILED, +                         "failed to resize: disk=%s, ptnum=%s: %s" % +                         (disk, ptnum, e),)) + +    return info + + +def handle(_name, cfg, _cloud, log, _args): +    if 'growpart' not in cfg: +        log.debug("No 'growpart' entry in cfg.  Using default: %s" % +                  DEFAULT_CONFIG) +        cfg['growpart'] = DEFAULT_CONFIG + +    mycfg = cfg.get('growpart') +    if not isinstance(mycfg, dict): +        log.warn("'growpart' in config was not a dict") +        return + +    mode = mycfg.get('mode', "auto") +    if util.is_false(mode): +        log.debug("growpart disabled: mode=%s" % mode) +        return + +    devices = util.get_cfg_option_list(cfg, "devices", ["/"]) +    if not len(devices): +        log.debug("growpart: empty device list") +        return + +    try: +        resizer = resizer_factory(mode) +    except (ValueError, TypeError) as e: +        log.debug("growpart unable to find resizer for '%s': %s" % (mode, e)) +        if mode != "auto": +            raise e +        return + +    resized = resize_devices(resizer, devices) +    for (entry, action, msg) in resized: +        if action == RESIZE.CHANGED: +            log.info("'%s' resized: %s" % (entry, msg)) +        else: +            log.debug("'%s' %s: %s" % (entry, action, msg)) + +RESIZERS = (('parted', ResizeParted), ('growpart', ResizeGrowPart)) diff --git a/cloudinit/config/cc_resizefs.py b/cloudinit/config/cc_resizefs.py index 44b27933..51dead2f 100644 --- a/cloudinit/config/cc_resizefs.py +++ b/cloudinit/config/cc_resizefs.py @@ -51,89 +51,6 @@ RESIZE_FS_PREFIXES_CMDS = [  NOBLOCK = "noblock" -def get_mount_info(path, log): -    # Use /proc/$$/mountinfo to find the device where path is mounted. -    # This is done because with a btrfs filesystem using os.stat(path) -    # does not return the ID of the device. -    # -    # Here, / has a device of 18 (decimal). -    # -    # $ stat / -    #   File: '/' -    #   Size: 234               Blocks: 0          IO Block: 4096   directory -    # Device: 12h/18d   Inode: 256         Links: 1 -    # Access: (0755/drwxr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root) -    # Access: 2013-01-13 07:31:04.358011255 +0000 -    # Modify: 2013-01-13 18:48:25.930011255 +0000 -    # Change: 2013-01-13 18:48:25.930011255 +0000 -    #  Birth: - -    # -    # Find where / is mounted: -    # -    # $ mount | grep ' / ' -    # /dev/vda1 on / type btrfs (rw,subvol=@,compress=lzo) -    # -    # And the device ID for /dev/vda1 is not 18: -    # -    # $ ls -l /dev/vda1 -    # brw-rw---- 1 root disk 253, 1 Jan 13 08:29 /dev/vda1 -    # -    # So use /proc/$$/mountinfo to find the device underlying the -    # input path. -    path_elements = [e for e in path.split('/') if e] -    devpth = None -    fs_type = None -    match_mount_point = None -    match_mount_point_elements = None -    mountinfo_path = '/proc/%s/mountinfo' % os.getpid() -    for line in util.load_file(mountinfo_path).splitlines(): -        parts = line.split() - -        mount_point = parts[4] -        mount_point_elements = [e for e in mount_point.split('/') if e] - -        # Ignore mounts deeper than the path in question. -        if len(mount_point_elements) > len(path_elements): -            continue - -        # Ignore mounts where the common path is not the same. -        l = min(len(mount_point_elements), len(path_elements)) -        if mount_point_elements[0:l] != path_elements[0:l]: -            continue - -        # Ignore mount points higher than an already seen mount -        # point. -        if (match_mount_point_elements is not None and -            len(match_mount_point_elements) > len(mount_point_elements)): -            continue - -        # Find the '-' which terminates a list of optional columns to -        # find the filesystem type and the path to the device.  See -        # man 5 proc for the format of this file. -        try: -            i = parts.index('-') -        except ValueError: -            log.debug("Did not find column named '-' in %s", -                      mountinfo_path) -            return None - -        # Get the path to the device. -        try: -            fs_type = parts[i + 1] -            devpth = parts[i + 2] -        except IndexError: -            log.debug("Too few columns in %s after '-' column", mountinfo_path) -            return None - -        match_mount_point = mount_point -        match_mount_point_elements = mount_point_elements - -    if devpth and fs_type and match_mount_point: -        return (devpth, fs_type, match_mount_point) -    else: -        return None - -  def handle(name, cfg, _cloud, log, args):      if len(args) != 0:          resize_root = args[0] @@ -150,7 +67,7 @@ def handle(name, cfg, _cloud, log, args):      # TODO(harlowja): allow what is to be resized to be configurable??      resize_what = "/" -    result = get_mount_info(resize_what, log) +    result = util.get_mount_info(resize_what, log)      if not result:          log.warn("Could not determine filesystem type of %s", resize_what)          return diff --git a/cloudinit/util.py b/cloudinit/util.py index ffe844b2..d0a6f81c 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -1586,3 +1586,86 @@ def expand_package_list(version_fmt, pkgs):              raise RuntimeError("Invalid package type.")      return pkglist + + +def get_mount_info(path, log=LOG): +    # Use /proc/$$/mountinfo to find the device where path is mounted. +    # This is done because with a btrfs filesystem using os.stat(path) +    # does not return the ID of the device. +    # +    # Here, / has a device of 18 (decimal). +    # +    # $ stat / +    #   File: '/' +    #   Size: 234               Blocks: 0          IO Block: 4096   directory +    # Device: 12h/18d   Inode: 256         Links: 1 +    # Access: (0755/drwxr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root) +    # Access: 2013-01-13 07:31:04.358011255 +0000 +    # Modify: 2013-01-13 18:48:25.930011255 +0000 +    # Change: 2013-01-13 18:48:25.930011255 +0000 +    #  Birth: - +    # +    # Find where / is mounted: +    # +    # $ mount | grep ' / ' +    # /dev/vda1 on / type btrfs (rw,subvol=@,compress=lzo) +    # +    # And the device ID for /dev/vda1 is not 18: +    # +    # $ ls -l /dev/vda1 +    # brw-rw---- 1 root disk 253, 1 Jan 13 08:29 /dev/vda1 +    # +    # So use /proc/$$/mountinfo to find the device underlying the +    # input path. +    path_elements = [e for e in path.split('/') if e] +    devpth = None +    fs_type = None +    match_mount_point = None +    match_mount_point_elements = None +    mountinfo_path = '/proc/%s/mountinfo' % os.getpid() +    for line in load_file(mountinfo_path).splitlines(): +        parts = line.split() + +        mount_point = parts[4] +        mount_point_elements = [e for e in mount_point.split('/') if e] + +        # Ignore mounts deeper than the path in question. +        if len(mount_point_elements) > len(path_elements): +            continue + +        # Ignore mounts where the common path is not the same. +        l = min(len(mount_point_elements), len(path_elements)) +        if mount_point_elements[0:l] != path_elements[0:l]: +            continue + +        # Ignore mount points higher than an already seen mount +        # point. +        if (match_mount_point_elements is not None and +            len(match_mount_point_elements) > len(mount_point_elements)): +            continue + +        # Find the '-' which terminates a list of optional columns to +        # find the filesystem type and the path to the device.  See +        # man 5 proc for the format of this file. +        try: +            i = parts.index('-') +        except ValueError: +            log.debug("Did not find column named '-' in %s", +                      mountinfo_path) +            return None + +        # Get the path to the device. +        try: +            fs_type = parts[i + 1] +            devpth = parts[i + 2] +        except IndexError: +            log.debug("Too few columns in %s after '-' column", mountinfo_path) +            return None + +        match_mount_point = mount_point +        match_mount_point_elements = mount_point_elements + +    if devpth and fs_type and match_mount_point: +        return (devpth, fs_type, match_mount_point) +    else: +        return None diff --git a/config/cloud.cfg b/config/cloud.cfg index a8c74486..b61b8a7d 100644 --- a/config/cloud.cfg +++ b/config/cloud.cfg @@ -26,6 +26,7 @@ cloud_init_modules:   - migrator   - bootcmd   - write-files + - growpart   - resizefs   - set_hostname   - update_hostname diff --git a/doc/examples/cloud-config-growpart.txt b/doc/examples/cloud-config-growpart.txt new file mode 100644 index 00000000..705f02c2 --- /dev/null +++ b/doc/examples/cloud-config-growpart.txt @@ -0,0 +1,24 @@ +#cloud-config +# +# growpart entry is a dict, if it is not present at all +# in config, then the default is used ({'mode': 'auto', 'devices': ['/']}) +# +#  mode: +#    values: +#     * auto: use any option possible (growpart or parted) +#             if none are available, do not warn, but debug. +#     * growpart: use growpart to grow partitions +#             if growpart is not available, this is an error. +#     * parted: use parted (parted resizepart) to resize partitions +#             if parted is not available, this is an error. +#     * off, false +# +# devices: +#   a list of things to resize. +#   items can be filesystem paths or devices (in /dev) +#   examples: +#     devices: [/, /dev/vdb1] +# +growpart: +  mode: auto +  devices: ['/'] diff --git a/tests/unittests/test_handler/test_handler_growpart.py b/tests/unittests/test_handler/test_handler_growpart.py new file mode 100644 index 00000000..74c254e0 --- /dev/null +++ b/tests/unittests/test_handler/test_handler_growpart.py @@ -0,0 +1,253 @@ +from mocker import MockerTestCase + +from cloudinit import cloud +from cloudinit import helpers +from cloudinit import util + +from cloudinit.config import cc_growpart + +import errno +import logging +import os +import mocker +import re +import stat + +# growpart: +#   mode: auto  # off, on, auto, 'growpart', 'parted' +#   devices: ['root'] + +HELP_PARTED_NO_RESIZE = """ +Usage: parted [OPTION]... [DEVICE [COMMAND [PARAMETERS]...]...] +Apply COMMANDs with PARAMETERS to DEVICE.  If no COMMAND(s) are given, run in +interactive mode. + +OPTIONs: +<SNIP> + +COMMANDs: +<SNIP> +  quit                                     exit program +  rescue START END                         rescue a lost partition near START +        and END +  resize NUMBER START END                  resize partition NUMBER and its file +        system +  rm NUMBER                                delete partition NUMBER +<SNIP> +Report bugs to bug-parted@gnu.org +""" + +HELP_PARTED_RESIZE = """ +Usage: parted [OPTION]... [DEVICE [COMMAND [PARAMETERS]...]...] +Apply COMMANDs with PARAMETERS to DEVICE.  If no COMMAND(s) are given, run in +interactive mode. + +OPTIONs: +<SNIP> + +COMMANDs: +<SNIP> +  quit                                     exit program +  rescue START END                         rescue a lost partition near START +        and END +  resize NUMBER START END                  resize partition NUMBER and its file +        system +  resizepart NUMBER END                    resize partition NUMBER +  rm NUMBER                                delete partition NUMBER +<SNIP> +Report bugs to bug-parted@gnu.org +""" + +HELP_GROWPART_RESIZE = """ +growpart disk partition +   rewrite partition table so that partition takes up all the space it can +   options: +    -h | --help       print Usage and exit +<SNIP> +    -u | --update  R  update the the kernel partition table info after growing +                      this requires kernel support and 'partx --update' +                      R is one of: +                       - 'auto'  : [default] update partition if possible +<SNIP> +   Example: +    - growpart /dev/sda 1 +      Resize partition 1 on /dev/sda +""" + +HELP_GROWPART_NO_RESIZE = """ +growpart disk partition +   rewrite partition table so that partition takes up all the space it can +   options: +    -h | --help       print Usage and exit +<SNIP> +   Example: +    - growpart /dev/sda 1 +      Resize partition 1 on /dev/sda +""" + +class TestDisabled(MockerTestCase): +    def setUp(self): +        super(TestDisabled, self).setUp() +        self.name = "growpart" +        self.cloud_init = None +        self.log = logging.getLogger("TestDisabled") +        self.args = [] + +        self.handle = cc_growpart.handle + +    def test_mode_off(self): +        #Test that nothing is done if mode is off. + +        # this really only verifies that resizer_factory isn't called +        config = {'growpart': {'mode': 'off'}} +        self.mocker.replace(cc_growpart.resizer_factory, +                            passthrough=False) +        self.mocker.replay() + +        self.handle(self.name, config, self.cloud_init, self.log, self.args) + +class TestConfig(MockerTestCase): +    def setUp(self): +        super(TestConfig, self).setUp() +        self.name = "growpart" +        self.paths = None +        self.cloud = cloud.Cloud(None, self.paths, None, None, None) +        self.log = logging.getLogger("TestConfig") +        self.args = [] +        os.environ = {} + +        self.cloud_init = None +        self.handle = cc_growpart.handle + +        # Order must be correct +        self.mocker.order() + +    def test_no_resizers_auto_is_fine(self): +        subp = self.mocker.replace(util.subp, passthrough=False) +        subp(['parted', '--help'], env={'LANG': 'C'}) +        self.mocker.result((HELP_PARTED_NO_RESIZE,"")) +        subp(['growpart', '--help'], env={'LANG': 'C'}) +        self.mocker.result((HELP_GROWPART_NO_RESIZE,"")) +        self.mocker.replay() + +        config = {'growpart': {'mode': 'auto'}} +        self.handle(self.name, config, self.cloud_init, self.log, self.args) + +    def test_no_resizers_mode_growpart_is_exception(self): +        subp = self.mocker.replace(util.subp, passthrough=False) +        subp(['growpart', '--help'], env={'LANG': 'C'}) +        self.mocker.result((HELP_GROWPART_NO_RESIZE,"")) +        self.mocker.replay() + +        config = {'growpart': {'mode': "growpart"}} +        self.assertRaises(ValueError, self.handle, self.name, config, +                          self.cloud_init, self.log, self.args) + +    def test_mode_auto_prefers_parted(self): +        subp = self.mocker.replace(util.subp, passthrough=False) +        subp(['parted', '--help'], env={'LANG': 'C'}) +        self.mocker.result((HELP_PARTED_RESIZE,"")) +        self.mocker.replay() + +        ret = cc_growpart.resizer_factory(mode="auto") +        self.assertTrue(isinstance(ret, cc_growpart.ResizeParted)) + +    def test_handle_with_no_growpart_entry(self): +        #if no 'growpart' entry in config, then mode=auto should be used + +        myresizer = object() + +        factory = self.mocker.replace(cc_growpart.resizer_factory, +                                      passthrough=False) +        rsdevs = self.mocker.replace(cc_growpart.resize_devices, +                                     passthrough=False) +        factory("auto") +        self.mocker.result(myresizer) +        rsdevs(myresizer, ["/"]) +        self.mocker.result((("/", cc_growpart.RESIZE.CHANGED, "my-message",),)) +        self.mocker.replay() + +        try: +            orig_resizers = cc_growpart.RESIZERS +            cc_growpart.RESIZERS = (('mysizer', object),) +            self.handle(self.name, {}, self.cloud_init, self.log, self.args) +        finally: +            cc_growpart.RESIZERS = orig_resizers +             + +class TestResize(MockerTestCase): +    def setUp(self): +        super(TestResize, self).setUp() +        self.name = "growpart" +        self.log = logging.getLogger("TestResize") + +        # Order must be correct +        self.mocker.order() + +    def test_simple_devices(self): +        #test simple device list +        # this patches out devent2dev, os.stat, and device_part_info +        # so in the end, doesn't test a lot +        devs = ["/dev/XXda1", "/dev/YYda2"] +        devstat_ret = Bunch(st_mode=25008, st_ino=6078, st_dev=5L, +                            st_nlink=1, st_uid=0, st_gid=6, st_size=0, +                            st_atime=0, st_mtime=0, st_ctime=0) +        enoent = ["/dev/NOENT"] +        real_stat = os.stat +        resize_calls = [] + +        class myresizer(): +            def resize(self, diskdev, partnum, partdev): +                resize_calls.append((diskdev, partnum, partdev)) +                if partdev == "/dev/YYda2": +                    return (1024, 2048) +                return (1024, 1024)  # old size, new size + +        def mystat(path): +            if path in devs: +                return devstat_ret +            if path in enoent: +                e = OSError("%s: does not exist" % path) +                e.errno = errno.ENOENT +                raise e +            return real_stat(path) + +        try: +            opinfo = cc_growpart.device_part_info +            cc_growpart.device_part_info = simple_device_part_info +            os.stat = mystat + +            resized = cc_growpart.resize_devices(myresizer(), devs + enoent) + +            def find(name, res): +                for f in res: +                    if f[0] == name: +                        return f +                return None +                 +            self.assertEqual(cc_growpart.RESIZE.NOCHANGE, +                             find("/dev/XXda1", resized)[1]) +            self.assertEqual(cc_growpart.RESIZE.CHANGED, +                             find("/dev/YYda2", resized)[1]) +            self.assertEqual(cc_growpart.RESIZE.SKIPPED, +                             find(enoent[0], resized)[1]) +            #self.assertEqual(resize_calls, +                             #[("/dev/XXda", "1", "/dev/XXda1"), +                              #("/dev/YYda", "2", "/dev/YYda2")]) +        finally: +            cc_growpart.device_part_info = opinfo +            os.stat = real_stat + + +def simple_device_part_info(devpath): +    # simple stupid return (/dev/vda, 1) for /dev/vda +    ret = re.search("([^0-9]*)([0-9]*)$", devpath) +    x = (ret.group(1), ret.group(2)) +    return x +         +class Bunch: +    def __init__(self, **kwds): +        self.__dict__.update(kwds) + + +# vi: ts=4 expandtab | 
