summaryrefslogtreecommitdiff
path: root/cloudinit/config/cc_growpart.py
diff options
context:
space:
mode:
Diffstat (limited to 'cloudinit/config/cc_growpart.py')
-rw-r--r--cloudinit/config/cc_growpart.py272
1 files changed, 272 insertions, 0 deletions
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))