Skip to content

Conversation

@xiachen-rh
Copy link
Contributor

@xiachen-rh xiachen-rh commented Nov 25, 2025

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
- Enumerates all PVs in the VG using new helper functions
- Intelligently handles pvresize:
* Skips pvresize for single-PV VGs when using growpart (growpart
already handles it via maybe_lvm_resize)
* Resizes all PVs for multi-PV VGs (growpart only resizes the
partition's PV)
- Conditionally extends LV based on resize_lv config option
* When resize_lv: true (default): lvextend +100%FREE on the LV
* When resize_lv: false: Only resize PVs, leaving LV unchanged
(useful for multi-LV setups where free space should be preserved)

The resize_lv option is particularly useful when multiple LVs exist
in the same VG (e.g., separate LVs for /home, /var/log, etc.), allowing
users to preserve free space for other LVs.

New helper functions:
- _get_vg_for_lv(): Returns VG name for a given LV device
- _get_pvs_for_vg(): Returns list of PV device paths for a VG

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
- resize_lv=False behavior
- Single-PV VG with growpart (skip pvresize)
- Multi-PV VG with growpart (resize all PVs)

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 #887, however I did not test it because that I failed to set up the local integration test environment, I will run it later.

@dermotbradley
Copy link
Contributor

dermotbradley commented Nov 27, 2025

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.

@github-actions
Copy link

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.)

@github-actions github-actions bot added the stale-pr Pull request is stale; will be auto-closed soon label Dec 12, 2025
@github-actions github-actions bot closed this Dec 19, 2025
@xiachen-rh
Copy link
Contributor Author

@dermotbradley thanks a lot for your comment, let me check how to update the code.

@xiachen-rh
Copy link
Contributor Author

Hello @blackboxsw could you help me re-open it please? I will continue working on it.

@holmanb holmanb reopened this Jan 5, 2026
@github-actions github-actions bot removed the stale-pr Pull request is stale; will be auto-closed soon label Jan 6, 2026
@holmanb holmanb added the incomplete Action required by submitter label Jan 6, 2026
@xiachen-rh
Copy link
Contributor Author

@dermotbradley
I reviewed the growpart of cloud-utils, it indeed has some pvresize functionality "maybe_lvm_resize()", and the issue is that it only resizes the PV corresponding to the resized partition. If a Volume Group has multiple Physical Volumes, only one gets resized, leaving other PVs unhandled.

So the solution for cloud-init could be,
Detects when using ResizeGrowPart (growpart mode) and checks if the VG has multiple PVs,
Single PV: skips pvresize (growpart already handled it) — avoids duplication
Multiple PVs: runs pvresize on all PVs in the VG — ensures complete coverage

Or making a fix solution for cloud-utils,
that is fixing maybe_lvm_resize() in growpart to handle all PVs, that could simplifies cloud-init code.

What's your opinion?

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.

@ani-sinha
Copy link
Contributor

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.

If we can have a more complete solution within cloud-init without depending on other external utilities, I think that might be more preferable.
(also python is so much better than cloud-utils bash :-) )

@ani-sinha
Copy link
Contributor

@holmanb @TheRealFalcon can you provide your inputs as well please? thanks.

@xiachen-rh
Copy link
Contributor Author

Another point is, with LVM we may not even need to grow an existing partition, we can add a new partition and use pvcreate+vgextend to add it to the existing volume group without any disruption. It should in theory be easier/safer than doing this without LVM.

Why the current approach uses resize
Simpler partition layout (one partition per disk)
Existing growpart tool support
Works well for single-disk scenarios
Well-tested approach

With LVM, we may not need to resize partitions because
VGs can span multiple PVs
Adding a new PV to a VG is non-disruptive
The VG automatically sees the new space
LVs can use space from any PV in the VG
This is one of LVM's advantages: flexibility in how you add storage

What do you think of it? Which solution is better?

@github-actions github-actions bot added the documentation This Pull Request changes documentation label Jan 20, 2026
@xiachen-rh xiachen-rh force-pushed the growpart-lvm-resize branch 3 times, most recently from 9e773a2 to 59ee3b0 Compare January 20, 2026 17:50
Fixes canonicalGH-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
 - Enumerates all PVs in the VG using new helper functions
 - Intelligently handles pvresize:
   * Skips pvresize for single-PV VGs when using growpart (growpart
     already handles it via maybe_lvm_resize)
   * Resizes all PVs for multi-PV VGs (growpart only resizes the
     partition's PV)
 - Conditionally extends LV based on `resize_lv` config option
   * When `resize_lv: true` (default): lvextend +100%FREE on the LV
   * When `resize_lv: false`: Only resize PVs, leaving LV unchanged
     (useful for multi-LV setups where free space should be preserved)

The `resize_lv` option is particularly useful when multiple LVs exist
in the same VG (e.g., separate LVs for /home, /var/log, etc.), allowing
users to preserve free space for other LVs.

New helper functions:
 - `_get_vg_for_lv()`: Returns VG name for a given LV device
 - `_get_pvs_for_vg()`: Returns list of PV device paths for a VG

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
 - resize_lv=False behavior
 - Single-PV VG with growpart (skip pvresize)
 - Multi-PV VG with growpart (resize all PVs)

This change does not affect existing non-LVM growpart behaviour.

Signed-off-by: Amy Chen <xiachen@redhat.com>
@xiachen-rh
Copy link
Contributor Author

I updated the code, the main change is,
cc_growpart: enhance LVM resize with resize_lv and multi-PV support

Enhance the LVM resize functionality with:

  • resize_lv config option to control LV extension (default: true)
  • Smart pvresize handling: skip for single-PV VGs with growpart, resize all PVs for multi-PV VGs
  • Helper functions _get_vg_for_lv() and _get_pvs_for_vg() for VG introspection

The resize_lv option is useful for multi-LV setups where free space should be preserved for other LVs. When resize_lv: false, only PVs are resized, leaving LV size unchanged.

Update schema, documentation, and add comprehensive tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation This Pull Request changes documentation incomplete Action required by submitter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

growpart does not work for lvm

4 participants