feat(growpart): add LVM resize support
Fixes GH-3265
This change integrates LVM logical volume resizing into the existing growpart flow. When the target block device is an LVM LV, cloud-init now performs:
- lvs lookup to determine the volume group
- enumeration of PVs in the VG
- pvresize on each PV
- lvextend +100%FREE on the LV
Filesystem resizing is intentionally omitted because resizefs is handled by a separate module.
Unit tests have been added under test_cc_growpart.py to validate:
- successful pvresize and lvextend calls
- error propagation for failed operations
This change does not affect existing non-LVM growpart behaviour.
Redhat Jira ticket: [RFE][Azure]growpart fails to resize a root partition on LVM https://issues.redhat.com/browse/RHEL-107485
Test Steps
I tested it on Azure and AWS with rhel image, for example, Boot up a VM on Azure and change the os disk to bigger than 64G Check the rootlv partition size, the root partition size is extended after VM boot up.
[Before changes] The root partition size is not extended.
2025-07-25 10:04:34,238 - cc_growpart.py[DEBUG]: growpart found fs=xfs
2025-07-25 10:04:34,238 - distros[DEBUG]: /dev/mapper/rootvg-rootlv is a mapped device pointing to /dev/dm-1
2025-07-25 10:04:34,238 - subp.py[DEBUG]: Running command ['dmsetup', 'deps', '--options=devname', '/dev/mapper/rootvg-rootlv'] with allowed return codes [0] (shell=False, capture=True)
2025-07-25 10:04:34,246 - subp.py[DEBUG]: Running command ['cryptsetup', 'status', '/dev/dm-1'] with allowed return codes [0] (shell=False, capture=True)
2025-07-25 10:04:34,395 - performance.py[DEBUG]: Running ['cryptsetup', 'status', '/dev/dm-1'] took 0.149 seconds
2025-07-25 10:04:34,395 - subp.py[DEBUG]: Running command ['cryptsetup', 'isLuks', '/dev/sda4'] with allowed return codes [0] (shell=False, capture=True)
2025-07-25 10:04:34,469 - performance.py[DEBUG]: Running ['cryptsetup', 'isLuks', '/dev/sda4'] took 0.073 seconds
2025-07-25 10:04:34,469 - performance.py[DEBUG]: Resizing devices took 0.234 seconds
2025-07-25 10:04:34,469 - cc_growpart.py[DEBUG]: '/' SKIPPED: Resizing mapped device (/dev/mapper/rootvg-rootlv) skipped as it is not encrypted.
[After changes] The root partition size is extended.
2025-11-25 05:55:00,763 - cc_growpart.py[DEBUG]: growpart found fs=xfs
2025-11-25 05:55:00,763 - distros[DEBUG]: /dev/mapper/rootvg-rootlv is a mapped device pointing to /dev/dm-1
2025-11-25 05:55:00,763 - subp.py[DEBUG]: Running command ['dmsetup', 'deps', '--options=devname', '/dev/mapper/rootvg-rootlv'] with allowed return codes [0] (shell=False, capture=True)
2025-11-25 05:55:00,766 - subp.py[DEBUG]: Running command ['cryptsetup', 'status', '/dev/dm-1'] with allowed return codes [0] (shell=False, capture=True)
2025-11-25 05:55:00,929 - performance.py[DEBUG]: Running ['cryptsetup', 'status', '/dev/dm-1'] took 0.164 seconds
2025-11-25 05:55:00,930 - subp.py[DEBUG]: Running command ['cryptsetup', 'isLuks', '/dev/sda4'] with allowed return codes [0] (shell=False, capture=True)
2025-11-25 05:55:01,001 - performance.py[DEBUG]: Running ['cryptsetup', 'isLuks', '/dev/sda4'] took 0.071 seconds
2025-11-25 05:55:01,001 - subp.py[DEBUG]: Running command ['lsblk', '-n', '-o', 'TYPE', '/dev/mapper/rootvg-rootlv'] with allowed return codes [0] (shell=False, capture=True)
2025-11-25 05:55:01,055 - performance.py[DEBUG]: Running ['lsblk', '-n', '-o', 'TYPE', '/dev/mapper/rootvg-rootlv'] took 0.053 seconds
2025-11-25 05:55:01,055 - util.py[DEBUG]: Reading from /sys/class/block/sda4/partition (quiet=False)
2025-11-25 05:55:01,055 - util.py[DEBUG]: Reading 2 bytes from /sys/class/block/sda4/partition
2025-11-25 05:55:01,055 - util.py[DEBUG]: Reading from /sys/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07/VMBUS:01/00000000-0000-8899-0000-000000000000/host0/target0:0:0/0:0:0:0/block/sda/dev (quiet=False)
2025-11-25 05:55:01,055 - util.py[DEBUG]: Reading 4 bytes from /sys/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07/VMBUS:01/00000000-0000-8899-0000-000000000000/host0/target0:0:0/0:0:0:0/block/sda/dev
2025-11-25 05:55:01,055 - util.py[DEBUG]: Reading from /proc/1239/mountinfo (quiet=False)
2025-11-25 05:55:01,055 - util.py[DEBUG]: Reading 3132 bytes from /proc/1239/mountinfo
2025-11-25 05:55:01,056 - util.py[DEBUG]: Reading from /proc/1239/mountinfo (quiet=False)
2025-11-25 05:55:01,056 - util.py[DEBUG]: Reading 3132 bytes from /proc/1239/mountinfo
2025-11-25 05:55:01,056 - subp.py[DEBUG]: Running command ['growpart', '--dry-run', '/dev/sda', '4'] with allowed return codes [0] (shell=False, capture=True)
2025-11-25 05:55:01,247 - performance.py[DEBUG]: Running ['growpart', '--dry-run', '/dev/sda', '4'] took 0.191 seconds
2025-11-25 05:55:01,247 - subp.py[DEBUG]: Running command ['growpart', '/dev/sda', '4'] with allowed return codes [0] (shell=False, capture=True)
2025-11-25 05:55:02,440 - performance.py[DEBUG]: Running ['growpart', '/dev/sda', '4'] took 1.193 seconds
2025-11-25 05:55:02,442 - cc_growpart.py[INFO]: starting LVM resize flow for /dev/mapper/rootvg-rootlv
2025-11-25 05:55:02,442 - subp.py[DEBUG]: Running command ['lvs', '--noheadings', '-o', 'vg_name', '/dev/mapper/rootvg-rootlv'] with allowed return codes [0] (shell=False, capture=True)
2025-11-25 05:55:02,467 - performance.py[DEBUG]: Running ['lvs', '--noheadings', '-o', 'vg_name', '/dev/mapper/rootvg-rootlv'] took 0.025 seconds
2025-11-25 05:55:02,467 - cc_growpart.py[DEBUG]: lv /dev/mapper/rootvg-rootlv belongs to vg rootvg
2025-11-25 05:55:02,467 - subp.py[DEBUG]: Running command ['vgs', '--noheadings', '-o', 'pv_name', '--separator', ' ', 'rootvg'] with allowed return codes [0] (shell=False, capture=True)
2025-11-25 05:55:02,486 - performance.py[DEBUG]: Running ['vgs', '--noheadings', '-o', 'pv_name', '--separator', ' ', 'rootvg'] took 0.019 seconds
2025-11-25 05:55:02,487 - subp.py[DEBUG]: Running command ['pvresize', '/dev/sda4'] with allowed return codes [0] (shell=False, capture=True)
2025-11-25 05:55:02,702 - performance.py[DEBUG]: Running ['pvresize', '/dev/sda4'] took 0.215 seconds
2025-11-25 05:55:02,702 - cc_growpart.py[INFO]: pvresize succeeded for /dev/sda4
2025-11-25 05:55:02,702 - subp.py[DEBUG]: Running command ['lvextend', '-l', '+100%FREE', '/dev/mapper/rootvg-rootlv'] with allowed return codes [0] (shell=False, capture=True)
2025-11-25 05:55:02,961 - performance.py[DEBUG]: Running ['lvextend', '-l', '+100%FREE', '/dev/mapper/rootvg-rootlv'] took 0.259 seconds
2025-11-25 05:55:02,961 - cc_growpart.py[INFO]: lvextend +100%FREE succeeded for /dev/mapper/rootvg-rootlv
2025-11-25 05:55:02,961 - performance.py[DEBUG]: Resizing devices took 2.198 seconds
2025-11-25 05:55:02,961 - cc_growpart.py[INFO]: '/' resized: changed (/dev/sda4) from 116510408192 to 127247826432
2025-11-25 05:55:02,961 - cc_growpart.py[INFO]: '/' resized: Successfully resized LVM device '/dev/mapper/rootvg-rootlv'
2025-11-25 05:55:02,961 - handlers.py[DEBUG]: finish: init-network/config-growpart: SUCCESS: config-growpart ran successfully and took 2.260 seconds
2025-11-25 05:55:02,963 - handlers.py[DEBUG]: start: init-network/config-resizefs: running config-resizefs with frequency always
2025-11-25 05:55:02,963 - helpers.py[DEBUG]: Running config-resizefs using lock (<cloudinit.helpers.DummyLock object at 0x7fa0730692e0>)
2025-11-25 05:55:02,964 - util.py[DEBUG]: Reading from /proc/1239/mountinfo (quiet=False)
2025-11-25 05:55:02,964 - util.py[DEBUG]: Reading 3132 bytes from /proc/1239/mountinfo
2025-11-25 05:55:02,964 - cc_resizefs.py[DEBUG]: resize_info: dev=/dev/mapper/rootvg-rootlv mnt_point=/ path=/
2025-11-25 05:55:02,964 - cc_resizefs.py[DEBUG]: Resizing / (xfs) using xfs_growfs /
2025-11-25 05:55:02,964 - subp.py[DEBUG]: Running command ('xfs_growfs', '/') with allowed return codes [0] (shell=False, capture=True)
2025-11-25 05:55:03,336 - performance.py[DEBUG]: Running ('xfs_growfs', '/') took 0.372 seconds
2025-11-25 05:55:03,336 - cc_resizefs.py[DEBUG]: Resized root filesystem (type=xfs, val=True)
2025-11-25 05:55:03,336 - handlers.py[DEBUG]: finish: init-network/config-resizefs: SUCCESS: config-resizefs ran successfully and took 0.373 seconds
I’ve added three unit tests. Please let me know if further coverage is needed.
The integration test case is copied from https://github.com/canonical/cloud-init/pull/887, however I did not test it because that I failed to set up the local integration test environment, I will run it later.
I think there should be a configuration switch to control whether or not the LV is resized (the PV resize seems fine) - often when LVM is used there will be multiple LVs (i.e. for /home, for /var/log, etc) and so having the LV for rootfs ("/") be grown to use up the whole of the VG is likely to be undesirable.
Also the growpart script (from cloud-utils) has some pvresize functionality (I forget any issues with it) and so it doesn't make sense to have LVM functionality in both cloud-util's growpart and in cloud-init's growpart module.
Hello! Thank you for this proposed change to cloud-init. This pull request is now marked as stale as it has not seen any activity in 14 days. If no activity occurs within the next 7 days, this pull request will automatically close.
If you are waiting for code review and you are seeing this message, apologies! Please reply, tagging blackboxsw, and he will ensure that someone takes a look soon.
(If the pull request is closed and you would like to continue working on it, please do tag blackboxsw to reopen it.)