summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cloudinit/config/cc_growpart.py83
-rw-r--r--doc/examples/cloud-config-growpart.txt2
-rw-r--r--tests/unittests/test_handler/test_handler_growpart.py56
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))