diff options
-rw-r--r-- | cloudinit/config/cc_growpart.py | 83 | ||||
-rw-r--r-- | doc/examples/cloud-config-growpart.txt | 2 | ||||
-rw-r--r-- | tests/unittests/test_handler/test_handler_growpart.py | 56 |
3 files changed, 137 insertions, 4 deletions
diff --git a/cloudinit/config/cc_growpart.py b/cloudinit/config/cc_growpart.py index 9f338ad1..6399bfb7 100644 --- a/cloudinit/config/cc_growpart.py +++ b/cloudinit/config/cc_growpart.py @@ -68,7 +68,9 @@ import os import os.path import re import stat +import platform +from functools import lru_cache from cloudinit import log as logging from cloudinit.settings import PER_ALWAYS from cloudinit import subp @@ -93,6 +95,58 @@ class RESIZE(object): LOG = logging.getLogger(__name__) +@lru_cache() +def is_lvm_lv(devpath): + if util.is_Linux(): + # all lvm lvs will have a realpath as a 'dm-*' name. + rpath = os.path.realpath(devpath) + if not os.path.basename(rpath).startswith("dm-"): + return False + out, _ = subp.subp("udevadm", "info", devpath) + # lvs should have DM_LV_NAME=<lvmuuid> and also DM_VG_NAME + return 'DM_LV_NAME=' in out + else: + LOG.info("Not an LVM Logical Volume partition") + return False + + +@lru_cache() +def get_pvs_for_lv(devpath): + myenv = {'LANG': 'C'} + + if not util.is_Linux(): + LOG.info("No support for LVM on %s", platform.system()) + return None + if not subp.which('lvm'): + LOG.info("No 'lvm' command present") + return None + + try: + (out, _err) = subp.subp(["lvm", "lvs", devpath, "--options=vgname", + "--noheadings"], update_env=myenv) + vgname = out.strip() + except subp.ProcessExecutionError as e: + if e.exit_code != 0: + util.logexc(LOG, "Failed: can't get Volume Group information " + "from %s", devpath) + raise ResizeFailedException(e) from e + + try: + (out, _err) = subp.subp(["lvm", "vgs", vgname, "--options=pvname", + "--noheadings"], update_env=myenv) + pvs = [p.strip() for p in out.splitlines()] + if len(pvs) > 1: + LOG.info("Do not know how to resize multiple Physical" + " Volumes") + else: + return pvs[0] + except subp.ProcessExecutionError as e: + if e.exit_code != 0: + util.logexc(LOG, "Failed: can't get Physical Volume " + "information from Volume Group %s", vgname) + raise ResizeFailedException(e) from e + + def resizer_factory(mode): resize_class = None if mode == "auto": @@ -208,13 +262,18 @@ def get_size(filename): os.close(fd) -def device_part_info(devpath): +def device_part_info(devpath, is_lvm): # 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) + # first check if this is an LVM and get its PVs + lvm_rpath = get_pvs_for_lv(devpath) + if is_lvm and lvm_rpath: + rpath = lvm_rpath + bname = os.path.basename(rpath) syspath = "/sys/class/block/%s" % bname @@ -244,7 +303,7 @@ def device_part_info(devpath): # 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) + return diskdevpath, ptnum def devent2dev(devent): @@ -294,8 +353,9 @@ def resize_devices(resizer, devices): "device '%s' not a block device" % blockdev,)) continue + is_lvm = is_lvm_lv(blockdev) try: - (disk, ptnum) = device_part_info(blockdev) + disk, ptnum = device_part_info(blockdev, is_lvm) except (TypeError, ValueError) as e: info.append((devent, RESIZE.SKIPPED, "device_part_info(%s) failed: %s" % (blockdev, e),)) @@ -316,6 +376,23 @@ def resize_devices(resizer, devices): "failed to resize: disk=%s, ptnum=%s: %s" % (disk, ptnum, e),)) + if is_lvm and isinstance(resizer, ResizeGrowPart): + try: + if len(devices) == 1: + (_out, _err) = subp.subp( + ["lvm", "lvextend", "--extents=100%FREE", blockdev], + update_env={'LANG': 'C'}) + info.append((devent, RESIZE.CHANGED, + "Logical Volume %s extended" % devices[0],)) + else: + LOG.info("Exactly one device should be configured to be " + "resized when using LVM. More than one configured" + ": %s", devices) + except (subp.ProcessExecutionError, ValueError) as e: + info.append((devent, RESIZE.NOCHANGE, + "Logical Volume %s resize failed: %s" % + (blockdev, e),)) + return info diff --git a/doc/examples/cloud-config-growpart.txt b/doc/examples/cloud-config-growpart.txt index 393d5164..09268117 100644 --- a/doc/examples/cloud-config-growpart.txt +++ b/doc/examples/cloud-config-growpart.txt @@ -13,6 +13,8 @@ # # devices: # a list of things to resize. +# if the devices are under LVM, the list should be a single entry, +# cloud-init will then extend the single entry, otherwise it will fail. # items can be filesystem paths or devices (in /dev) # examples: # devices: [/, /dev/vdb1] diff --git a/tests/unittests/test_handler/test_handler_growpart.py b/tests/unittests/test_handler/test_handler_growpart.py index 7f039b79..cc0a9248 100644 --- a/tests/unittests/test_handler/test_handler_growpart.py +++ b/tests/unittests/test_handler/test_handler_growpart.py @@ -172,6 +172,53 @@ class TestResize(unittest.TestCase): self.name = "growpart" self.log = logging.getLogger("TestResize") + def test_lvm_resize(self): + # LVM resize should work only if a single device is configured. More + # than one device should fail. + lvm_pass = ["/dev/XXdm-0"] + lvm_fail = ["/dev/XXdm-1", "/dev/YYdm-1"] + devstat_ret = Bunch(st_mode=25008, st_ino=6078, st_dev=5, + st_nlink=1, st_uid=0, st_gid=6, st_size=0, + st_atime=0, st_mtime=0, st_ctime=0) + real_stat = os.stat + resize_calls = [] + + class myresizer(object): + def resize(self, diskdev, partnum, partdev): + resize_calls.append((diskdev, partnum, partdev)) + if partdev == "/dev/XXdm-0": + return (1024, 2048) + return (1024, 1024) # old size, new size + + def mystat(path): + if path in lvm_pass or path in lvm_fail: + return devstat_ret + return real_stat(path) + + try: + opinfo = cc_growpart.device_part_info + cc_growpart.device_part_info = simple_device_part_info_lvm + os.stat = mystat + + resized = cc_growpart.resize_devices(myresizer(), lvm_pass) + not_resized = cc_growpart.resize_devices(myresizer(), lvm_fail) + + def find(name, res): + for f in res: + if f[0] == name: + return f + return None + + self.assertEqual(cc_growpart.RESIZE.CHANGED, + find("/dev/XXdm-0", resized)[1]) + self.assertEqual(cc_growpart.RESIZE.NOCHANGE, + find("/dev/XXdm-1", not_resized)[1]) + self.assertEqual(cc_growpart.RESIZE.NOCHANGE, + find("/dev/YYdm-1", not_resized)[1]) + finally: + cc_growpart.device_part_info = opinfo + os.stat = real_stat + def test_simple_devices(self): # test simple device list # this patches out devent2dev, os.stat, and device_part_info @@ -227,7 +274,14 @@ class TestResize(unittest.TestCase): os.stat = real_stat -def simple_device_part_info(devpath): +def simple_device_part_info_lvm(devpath, is_lvm): + # 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 + + +def simple_device_part_info(devpath, is_lvm): # simple stupid return (/dev/vda, 1) for /dev/vda ret = re.search("([^0-9]*)([0-9]*)$", devpath) x = (ret.group(1), ret.group(2)) |