From 573875bf77a192c75b5d56b2927fb544b86c350f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Wed, 20 May 2026 09:55:46 +0200 Subject: [PATCH 1/5] Move root partition resizing logic to VM.grow_root_partition() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit And make it a bit more robust, by: - fetching the actual partition mounted on / - checking if the filesystem is ext* Signed-off-by: Gaëtan Lehmann --- lib/vm.py | 26 ++++++++++++++++++++++++++ tests/storage/storage.py | 14 +++----------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/lib/vm.py b/lib/vm.py index c334500ba..0800db2cb 100644 --- a/lib/vm.py +++ b/lib/vm.py @@ -4,6 +4,7 @@ import logging import os +import re import subprocess import tempfile import uuid @@ -12,6 +13,7 @@ import lib.efi as efi from lib.basevm import BaseVM from lib.common import ( + KiB, PackageManagerEnum, expand_scope_relative_nodeid, parse_xe_dict, @@ -523,6 +525,30 @@ def detect_package_manager(self) -> PackageManagerEnum: return PackageManagerEnum.ZYPPER return PackageManagerEnum.UNKNOWN + def grow_root_partition(self) -> int | None: + if self.detect_package_manager() == PackageManagerEnum.APK: + # growpart is not available in alpine 3.12 + # vm.ssh('apk add cloud-utils-growpart e2fsprogs-extra') + self.ssh('apk add gawk util-linux e2fsprogs-extra') + self.ssh( + 'wget https://raw.githubusercontent.com/canonical/cloud-utils/main/bin/growpart -O /usr/bin/growpart' + ) + self.ssh('chmod +x /usr/bin/growpart') + else: + return None + mount_output = self.ssh('mount').strip() + root_match = re.search(r'/dev/(\w+?)(p?)(\d+) on / type (\w+)', mount_output) + assert root_match is not None + disk, p, partition, fs_type = root_match.groups() + if not fs_type.startswith('ext'): + logging.debug(f"Unsupported filesystem: {fs_type}") + return None + growpart_returncode = self.ssh_with_result(f'growpart /dev/{disk} {partition}').returncode + assert growpart_returncode in [0, 1] # growpart returns 1 if the size is already the expected one + self.ssh(f'resize2fs /dev/{disk}{p}{partition}') + df_output = self.ssh('df /') + return int(df_output.splitlines()[-1].split()[3]) * KiB + def insert_cd(self, vdi_name: str) -> None: logging.info("Insert CD %r in VM %s", vdi_name, self.uuid) self.host.xe('vm-cd-insert', {'uuid': self.uuid, 'cd-name': vdi_name}) diff --git a/tests/storage/storage.py b/tests/storage/storage.py index bddd157f4..8d07acd8f 100644 --- a/tests/storage/storage.py +++ b/tests/storage/storage.py @@ -250,17 +250,9 @@ def xva_export_import(source_vm: VM, compression: XVACompression, temp_large_dir vm.wait_for_vm_running_and_ssh_up() install_randstream(vm) - if vm.detect_package_manager() == PackageManagerEnum.APK: - # growpart is not available in alpine 3.12 - # vm.ssh('apk add cloud-utils-growpart e2fsprogs-extra') - vm.ssh('apk add gawk util-linux e2fsprogs-extra') - vm.ssh('wget https://raw.githubusercontent.com/canonical/cloud-utils/main/bin/growpart -O /usr/bin/growpart') - vm.ssh('chmod +x /usr/bin/growpart') - # TODO: maybe use `findmnt -no SOURCE /` from util-linux to get the blockdevice mounted on / - growpart_returncode = vm.ssh_with_result('growpart /dev/xvda 3').returncode - assert growpart_returncode in [0, 1] # growpart returns 1 if the size is already the expected one - vm.ssh('resize2fs /dev/xvda3') - stream_size = min(volume_size // 2, config.write_volume_cap) + root_partition_size = vm.grow_root_partition() + if root_partition_size is not None: + stream_size = min(root_partition_size // 2, config.write_volume_cap) else: stream_size = 500 * MiB From f2b107771efa20ce96fb2e7188685e041f08db2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Thu, 21 May 2026 22:50:43 +0200 Subject: [PATCH 2/5] Avoid downloading growpart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit github is not exactly reliable these days. use sfdisk, which is also used by growpart internally Signed-off-by: Gaëtan Lehmann --- lib/vm.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/vm.py b/lib/vm.py index 0800db2cb..7ffca8148 100644 --- a/lib/vm.py +++ b/lib/vm.py @@ -527,13 +527,7 @@ def detect_package_manager(self) -> PackageManagerEnum: def grow_root_partition(self) -> int | None: if self.detect_package_manager() == PackageManagerEnum.APK: - # growpart is not available in alpine 3.12 - # vm.ssh('apk add cloud-utils-growpart e2fsprogs-extra') - self.ssh('apk add gawk util-linux e2fsprogs-extra') - self.ssh( - 'wget https://raw.githubusercontent.com/canonical/cloud-utils/main/bin/growpart -O /usr/bin/growpart' - ) - self.ssh('chmod +x /usr/bin/growpart') + self.ssh('apk add util-linux e2fsprogs-extra') else: return None mount_output = self.ssh('mount').strip() @@ -543,8 +537,8 @@ def grow_root_partition(self) -> int | None: if not fs_type.startswith('ext'): logging.debug(f"Unsupported filesystem: {fs_type}") return None - growpart_returncode = self.ssh_with_result(f'growpart /dev/{disk} {partition}').returncode - assert growpart_returncode in [0, 1] # growpart returns 1 if the size is already the expected one + self.ssh(f'echo ", +" | sfdisk --no-reread --force -N {partition} /dev/{disk}') + self.ssh(f'partx -u -n {partition}:{partition} /dev/{disk}') self.ssh(f'resize2fs /dev/{disk}{p}{partition}') df_output = self.ssh('df /') return int(df_output.splitlines()[-1].split()[3]) * KiB From 0063384c3c764b9f9883ae86d21f886095db0861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Thu, 21 May 2026 23:22:19 +0200 Subject: [PATCH 3/5] Implement grow_root_partition for debian-based systems MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gaëtan Lehmann --- lib/vm.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/vm.py b/lib/vm.py index 7ffca8148..7e321e006 100644 --- a/lib/vm.py +++ b/lib/vm.py @@ -526,8 +526,11 @@ def detect_package_manager(self) -> PackageManagerEnum: return PackageManagerEnum.UNKNOWN def grow_root_partition(self) -> int | None: - if self.detect_package_manager() == PackageManagerEnum.APK: + pkg_manager = self.detect_package_manager() + if pkg_manager == PackageManagerEnum.APK: self.ssh('apk add util-linux e2fsprogs-extra') + elif pkg_manager == PackageManagerEnum.APT_GET: + self.ssh('apt-get update && apt-get install -y -qq util-linux e2fsprogs') else: return None mount_output = self.ssh('mount').strip() From ebfb4f50e3f40208ab04b62b5453f0521fe6e197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Thu, 21 May 2026 23:24:56 +0200 Subject: [PATCH 4/5] Implement grow_root_partition for redhat-based systems MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gaëtan Lehmann --- lib/vm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/vm.py b/lib/vm.py index 7e321e006..9e342401c 100644 --- a/lib/vm.py +++ b/lib/vm.py @@ -531,6 +531,8 @@ def grow_root_partition(self) -> int | None: self.ssh('apk add util-linux e2fsprogs-extra') elif pkg_manager == PackageManagerEnum.APT_GET: self.ssh('apt-get update && apt-get install -y -qq util-linux e2fsprogs') + elif pkg_manager == PackageManagerEnum.RPM: + self.ssh('yum install -y util-linux e2fsprogs') else: return None mount_output = self.ssh('mount').strip() From 686b74902eb27cadc85170be41d11781688fd153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Lehmann?= Date: Tue, 7 Jul 2026 18:56:20 +0200 Subject: [PATCH 5/5] Match package manager with a python match in grow_root_partition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit and implement missing command for opensuse Signed-off-by: Gaëtan Lehmann --- lib/vm.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/vm.py b/lib/vm.py index 9e342401c..69fd6135c 100644 --- a/lib/vm.py +++ b/lib/vm.py @@ -29,7 +29,7 @@ from lib.vdi import VDI from lib.vif import VIF -from typing import TYPE_CHECKING, Iterable, List, Literal, overload +from typing import TYPE_CHECKING, Iterable, List, Literal, assert_never, overload if TYPE_CHECKING: from lib.host import Host @@ -527,14 +527,21 @@ def detect_package_manager(self) -> PackageManagerEnum: def grow_root_partition(self) -> int | None: pkg_manager = self.detect_package_manager() - if pkg_manager == PackageManagerEnum.APK: - self.ssh('apk add util-linux e2fsprogs-extra') - elif pkg_manager == PackageManagerEnum.APT_GET: - self.ssh('apt-get update && apt-get install -y -qq util-linux e2fsprogs') - elif pkg_manager == PackageManagerEnum.RPM: - self.ssh('yum install -y util-linux e2fsprogs') - else: - return None + match pkg_manager: + case PackageManagerEnum.APK: + self.ssh('apk add util-linux e2fsprogs-extra') + case PackageManagerEnum.APT_GET: + self.ssh('apt-get update && apt-get install -y -qq util-linux e2fsprogs') + case PackageManagerEnum.DNF: + self.ssh('dnf install -y util-linux e2fsprogs') + case PackageManagerEnum.YUM: + self.ssh('yum install -y util-linux e2fsprogs') + case PackageManagerEnum.ZYPPER: + self.ssh('zypper --non-interactive install util-linux e2fsprogs') + case PackageManagerEnum.UNKNOWN: + return None + case _: + assert_never(pkg_manager) mount_output = self.ssh('mount').strip() root_match = re.search(r'/dev/(\w+?)(p?)(\d+) on / type (\w+)', mount_output) assert root_match is not None