From 98473539103a68c45a1adf39a9c94e2d082172c0 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Sat, 18 Jul 2026 12:58:35 -0400 Subject: [PATCH 1/7] [VM] Add per-kernel VM image URL pinning to avoid pulling latest qcow2 Adds optional vm_image_url field to KernelInfo so each kernel entry in kernels.yaml can pin to a specific Rocky Linux qcow2 image. The cache filename is derived from the pinned URL basename, so changing a pin automatically triggers a re-download instead of serving a stale image. Pins lts-8.6, lts-9.2, and lts-9.6 to date-stamped vault images. Co-Authored-By: Claude Opus 4.6 --- kt/data/kernels.yaml | 3 ++ kt/ktlib/kernels.py | 2 + kt/ktlib/vm.py | 30 +++++++++++--- tests/kt/ktlib/test_kernels.py | 26 ++++++++++++ tests/kt/ktlib/test_vm.py | 72 ++++++++++++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 tests/kt/ktlib/test_vm.py diff --git a/kt/data/kernels.yaml b/kt/data/kernels.yaml index c13c057..45e4e00 100644 --- a/kt/data/kernels.yaml +++ b/kt/data/kernels.yaml @@ -19,6 +19,7 @@ kernels: dist_git_branch: lts86-8 mock_config: rocky-lts86 automated: true + vm_image_url: https://dl.rockylinux.org/vault/rocky/8.6/images/Rocky-8-GenericCloud-8.6.20220702.0.x86_64.qcow2 lts-9.2: src_tree_root: kernel-src-tree @@ -27,6 +28,7 @@ kernels: dist_git_branch: lts92-9 mock_config: rocky-lts92 automated: true + vm_image_url: https://dl.rockylinux.org/vault/rocky/9.2/images/x86_64/Rocky-9-GenericCloud-Base-9.2-20230513.0.x86_64.qcow2 lts-9.6: src_tree_root: kernel-src-tree @@ -35,6 +37,7 @@ kernels: dist_git_branch: lts96-9 mock_config: rocky-lts96 automated: true + vm_image_url: https://dl.rockylinux.org/vault/rocky/9.6/images/x86_64/Rocky-9-GenericCloud-Base-9.6-20250531.0.x86_64.qcow2 fipslegacy-8.6: src_tree_root: kernel-src-tree diff --git a/kt/ktlib/kernels.py b/kt/ktlib/kernels.py index 08dab17..ae78b70 100644 --- a/kt/ktlib/kernels.py +++ b/kt/ktlib/kernels.py @@ -39,6 +39,8 @@ class KernelInfo: automated: bool + vm_image_url: str | None = None + @dataclass class KernelsInfo: diff --git a/kt/ktlib/vm.py b/kt/ktlib/vm.py index bf54fc5..447df91 100644 --- a/kt/ktlib/vm.py +++ b/kt/ktlib/vm.py @@ -10,8 +10,11 @@ from git import Repo from pathlib3x import Path +from urllib.parse import urlparse + from kt.ktlib.config import Config from kt.ktlib.kernel_workspace import KernelWorkspace +from kt.ktlib.kernels import KernelsInfo from kt.ktlib.local import LocalCommand from kt.ktlib.ssh import SshCommand from kt.ktlib.util import Constants @@ -43,18 +46,22 @@ class Vm: cloud_init_path: Path name: str kernel_workspace: KernelWorkspace + vm_image_url: str | None = None @classmethod - def load(cls, config: Config, kernel_workspace: KernelWorkspace): + def load(cls, config: Config, kernel_workspace: KernelWorkspace, vm_image_url: str | None = None): kernel_workspace_str = kernel_workspace.folder.name kernel_name = cls._extract_kernel_name(kernel_workspace_str) vm_major_version = cls._extract_major(kernel_name) vm_major_minor_version = cls._extract_major_minor(kernel_name) - # Image source paths construction - qcow2_source_path = config.images_source_dir / Path( - cls._qcow2_name(vm_major_minor_version=vm_major_minor_version) - ) + # Image source paths construction — use pinned URL basename if set, else default + if vm_image_url: + source_image_name = Path(urlparse(vm_image_url).path).name + else: + source_image_name = cls._qcow2_name(vm_major_minor_version=vm_major_minor_version) + + qcow2_source_path = config.images_source_dir / Path(source_image_name) # Actual current image paths construction work_dir = config.images_dir / Path(kernel_workspace_str) @@ -69,6 +76,7 @@ def load(cls, config: Config, kernel_workspace: KernelWorkspace): cloud_init_path=cloud_init_path, name=kernel_workspace_str, kernel_workspace=kernel_workspace, + vm_image_url=vm_image_url, ) @classmethod @@ -104,7 +112,15 @@ def load_from_workspace(cls, kernel_workspace_name: str): config = Config.load() kernel_workpath = config.kernels_dir / kernel_workspace_name kernel_workspace = KernelWorkspace.load_from_filepath(folder=kernel_workpath) - return cls.load(config=config, kernel_workspace=kernel_workspace) + + vm_image_url = None + kernel_name = cls._extract_kernel_name(kernel_workspace_name) + kernels_info = KernelsInfo.from_yaml(config=config) + kernel_info = kernels_info.kernels.get(kernel_name) + if kernel_info: + vm_image_url = kernel_info.vm_image_url + + return cls.load(config=config, kernel_workspace=kernel_workspace, vm_image_url=vm_image_url) @classmethod def setup_and_spinup( @@ -140,6 +156,8 @@ def setup_and_spinup( return vm_instance def _get_vm_url(self): + if self.vm_image_url: + return self.vm_image_url return f"{Constants.BASE_URL}/{self.vm_major_minor_version}/images/x86_64/{Constants.DEFAULT_VM_BASE}-{self.vm_major_version}-{Constants.QCOW2_TRAIL}" def _download_source_image(self, override_base: bool = False): diff --git a/tests/kt/ktlib/test_kernels.py b/tests/kt/ktlib/test_kernels.py index 1b9c2b3..4b6b4b0 100644 --- a/tests/kt/ktlib/test_kernels.py +++ b/tests/kt/ktlib/test_kernels.py @@ -100,3 +100,29 @@ def test_kernels_from_dict_check_src_root(): kernels_info = KernelsInfo.from_dict(data=data, private_data={}, config=config) kernel_info = list(kernels_info.kernels.values())[0] assert kernel_info.src_tree_root.folder == config.base_path / Path("kernel-src-tree") + + +def test_kernels_vm_image_url_default_none(): + config = Config.from_str_dict(Config.DEFAULT) + + kernels_info = KernelsInfo.from_dict(data=data, private_data={}, config=config) + kernel_info = list(kernels_info.kernels.values())[0] + assert kernel_info.vm_image_url is None + + +def test_kernels_vm_image_url_present(): + pinned_url = ( + "https://dl.rockylinux.org/vault/rocky/9.2/images/x86_64/Rocky-9-GenericCloud-Base-9.2-20230513.0.x86_64.qcow2" + ) + kernels_with_pin = { + "kernel1": { + **kernels["kernel1"], + "vm_image_url": pinned_url, + } + } + data_with_pin = {"common_repos": common_repos, "kernels": kernels_with_pin} + config = Config.from_str_dict(Config.DEFAULT) + + kernels_info = KernelsInfo.from_dict(data=data_with_pin, private_data={}, config=config) + kernel_info = list(kernels_info.kernels.values())[0] + assert kernel_info.vm_image_url == pinned_url diff --git a/tests/kt/ktlib/test_vm.py b/tests/kt/ktlib/test_vm.py new file mode 100644 index 0000000..47abf0a --- /dev/null +++ b/tests/kt/ktlib/test_vm.py @@ -0,0 +1,72 @@ +from unittest.mock import MagicMock + +from pathlib3x import Path + +from kt.ktlib.util import Constants +from kt.ktlib.vm import Vm + + +def _make_vm(vm_image_url=None, major="9", major_minor="9.2"): + vm = Vm.__new__(Vm) + vm.vm_major_version = major + vm.vm_major_minor_version = major_minor + vm.vm_image_url = vm_image_url + return vm + + +def test_get_vm_url_default(): + vm = _make_vm() + url = vm._get_vm_url() + expected = f"{Constants.BASE_URL}/9.2/images/x86_64/{Constants.DEFAULT_VM_BASE}-9-{Constants.QCOW2_TRAIL}" + assert url == expected + + +def test_get_vm_url_pinned(): + pin = ( + "https://dl.rockylinux.org/vault/rocky/9.2/images/x86_64/Rocky-9-GenericCloud-Base-9.2-20230513.0.x86_64.qcow2" + ) + vm = _make_vm(vm_image_url=pin) + assert vm._get_vm_url() == pin + + +def test_load_cache_filename_default(): + config = MagicMock() + config.images_source_dir = Path("/tmp/images") + config.images_dir = Path("/tmp/virt-images") + + workspace = MagicMock() + workspace.folder.name = "lts-9.2" + + vm = Vm.load(config=config, kernel_workspace=workspace) + assert vm.qcow2_source_path.name == "Rocky-9.2-GenericCloud.latest.x86_64.qcow2" + + +def test_load_cache_filename_pinned(): + config = MagicMock() + config.images_source_dir = Path("/tmp/images") + config.images_dir = Path("/tmp/virt-images") + + workspace = MagicMock() + workspace.folder.name = "lts-9.2" + + pin = ( + "https://dl.rockylinux.org/vault/rocky/9.2/images/x86_64/Rocky-9-GenericCloud-Base-9.2-20230513.0.x86_64.qcow2" + ) + vm = Vm.load(config=config, kernel_workspace=workspace, vm_image_url=pin) + assert vm.qcow2_source_path.name == "Rocky-9-GenericCloud-Base-9.2-20230513.0.x86_64.qcow2" + + +def test_load_pinned_url_stored(): + config = MagicMock() + config.images_source_dir = Path("/tmp/images") + config.images_dir = Path("/tmp/virt-images") + + workspace = MagicMock() + workspace.folder.name = "lts-9.6" + + pin = ( + "https://dl.rockylinux.org/vault/rocky/9.6/images/x86_64/Rocky-9-GenericCloud-Base-9.6-20250531.0.x86_64.qcow2" + ) + vm = Vm.load(config=config, kernel_workspace=workspace, vm_image_url=pin) + assert vm.vm_image_url == pin + assert vm._get_vm_url() == pin From 25cccb4bcd85c01297b1257d586b94bcf6500f04 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Sat, 18 Jul 2026 13:44:09 -0400 Subject: [PATCH 2/7] [VM] Add vault pinning and depot support to cloud-init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin dnf to the Rocky Linux vault during cloud-init so package installs resolve against the correct minor version instead of rolling to latest. Also install the depot client and login/enable channels when DEPOT_USER and DEPOT_TOKEN are set on the host. Ordering: chown → vault-pin → depot → kernel_install_dep.sh Co-Authored-By: Claude Opus 4.6 --- kt/data/kernels.yaml | 6 ++ kt/ktlib/kernels.py | 1 + kt/ktlib/vm.py | 35 +++++++++- tests/kt/ktlib/test_vm.py | 137 +++++++++++++++++++++++++++++++++++++- 4 files changed, 176 insertions(+), 3 deletions(-) diff --git a/kt/data/kernels.yaml b/kt/data/kernels.yaml index 45e4e00..2095ee6 100644 --- a/kt/data/kernels.yaml +++ b/kt/data/kernels.yaml @@ -20,6 +20,8 @@ kernels: mock_config: rocky-lts86 automated: true vm_image_url: https://dl.rockylinux.org/vault/rocky/8.6/images/Rocky-8-GenericCloud-8.6.20220702.0.x86_64.qcow2 + depot_channels: + - lts-8.6 lts-9.2: src_tree_root: kernel-src-tree @@ -29,6 +31,8 @@ kernels: mock_config: rocky-lts92 automated: true vm_image_url: https://dl.rockylinux.org/vault/rocky/9.2/images/x86_64/Rocky-9-GenericCloud-Base-9.2-20230513.0.x86_64.qcow2 + depot_channels: + - lts-9.2 lts-9.6: src_tree_root: kernel-src-tree @@ -38,6 +42,8 @@ kernels: mock_config: rocky-lts96 automated: true vm_image_url: https://dl.rockylinux.org/vault/rocky/9.6/images/x86_64/Rocky-9-GenericCloud-Base-9.6-20250531.0.x86_64.qcow2 + depot_channels: + - lts-9.6 fipslegacy-8.6: src_tree_root: kernel-src-tree diff --git a/kt/ktlib/kernels.py b/kt/ktlib/kernels.py index ae78b70..e64f192 100644 --- a/kt/ktlib/kernels.py +++ b/kt/ktlib/kernels.py @@ -40,6 +40,7 @@ class KernelInfo: automated: bool vm_image_url: str | None = None + depot_channels: list[str] | None = None @dataclass diff --git a/kt/ktlib/vm.py b/kt/ktlib/vm.py index 447df91..c4defeb 100644 --- a/kt/ktlib/vm.py +++ b/kt/ktlib/vm.py @@ -47,9 +47,16 @@ class Vm: name: str kernel_workspace: KernelWorkspace vm_image_url: str | None = None + depot_channels: list[str] | None = None @classmethod - def load(cls, config: Config, kernel_workspace: KernelWorkspace, vm_image_url: str | None = None): + def load( + cls, + config: Config, + kernel_workspace: KernelWorkspace, + vm_image_url: str | None = None, + depot_channels: list[str] | None = None, + ): kernel_workspace_str = kernel_workspace.folder.name kernel_name = cls._extract_kernel_name(kernel_workspace_str) vm_major_version = cls._extract_major(kernel_name) @@ -77,6 +84,7 @@ def load(cls, config: Config, kernel_workspace: KernelWorkspace, vm_image_url: s name=kernel_workspace_str, kernel_workspace=kernel_workspace, vm_image_url=vm_image_url, + depot_channels=depot_channels, ) @classmethod @@ -114,13 +122,17 @@ def load_from_workspace(cls, kernel_workspace_name: str): kernel_workspace = KernelWorkspace.load_from_filepath(folder=kernel_workpath) vm_image_url = None + depot_channels = None kernel_name = cls._extract_kernel_name(kernel_workspace_name) kernels_info = KernelsInfo.from_yaml(config=config) kernel_info = kernels_info.kernels.get(kernel_name) if kernel_info: vm_image_url = kernel_info.vm_image_url + depot_channels = kernel_info.depot_channels - return cls.load(config=config, kernel_workspace=kernel_workspace, vm_image_url=vm_image_url) + return cls.load( + config=config, kernel_workspace=kernel_workspace, vm_image_url=vm_image_url, depot_channels=depot_channels + ) @classmethod def setup_and_spinup( @@ -203,6 +215,25 @@ def _setup_cloud_init(self, config: Config): data["runcmd"][0][1] = f"{config.user}:{config.user}" data["runcmd"][0][2] = os.environ["HOME"] + # Pin dnf to vault for kernels with a pinned VM image (Rocky only, not CentOS/cbr) + if self.vm_image_url: + data["runcmd"].append(f'echo "{self.vm_major_minor_version}" > /etc/dnf/vars/releasever') + data["runcmd"].append('echo "vault/rocky" > /etc/dnf/vars/contentdir') + data["runcmd"].append( + "cd /etc/yum.repos.d/ && for f in *.repo; do " + 'sed -i -e "s/^mirrorlist=/#mirrorlist=/" -e "s/#baseurl=/baseurl=/" "$f"; done' + ) + data["runcmd"].append("dnf clean all") + + # Depot: always install the client, but only login+enable if credentials are present + data["runcmd"].append("dnf install -y https://depot.ciq.com/public/files/depot-client/depot/depot.x86_64.rpm") + depot_user = os.environ.get("DEPOT_USER") + depot_token = os.environ.get("DEPOT_TOKEN") + if depot_user and depot_token and self.depot_channels: + data["runcmd"].append(f"depot login -u {depot_user} -t {depot_token}") + for channel in self.depot_channels: + data["runcmd"].append(f"depot enable {channel} -y") + # Install packages needed later data["runcmd"].append([str(config.base_path / Path("kernel-src-tree-tools") / Path("kernel_install_dep.sh"))]) # Write this to image cloud_init diff --git a/tests/kt/ktlib/test_vm.py b/tests/kt/ktlib/test_vm.py index 47abf0a..2a4f187 100644 --- a/tests/kt/ktlib/test_vm.py +++ b/tests/kt/ktlib/test_vm.py @@ -1,10 +1,15 @@ -from unittest.mock import MagicMock +import os +from io import StringIO +from unittest.mock import MagicMock, patch +import oyaml as yaml from pathlib3x import Path from kt.ktlib.util import Constants from kt.ktlib.vm import Vm +_real_open = open + def _make_vm(vm_image_url=None, major="9", major_minor="9.2"): vm = Vm.__new__(Vm) @@ -70,3 +75,133 @@ def test_load_pinned_url_stored(): vm = Vm.load(config=config, kernel_workspace=workspace, vm_image_url=pin) assert vm.vm_image_url == pin assert vm._get_vm_url() == pin + + +def _make_config(): + config = MagicMock() + config.user = "testuser" + config.ssh_key = "/tmp/fake_key.pub" + base = MagicMock() + base.__str__ = lambda self: "/home/testuser/ciq" + base.__truediv__ = lambda self, other: Path("/home/testuser/ciq") / other + base.absolute.return_value = Path("/home/testuser/ciq") + config.base_path = base + config.kernels_dir = Path("/home/testuser/ciq/kernels") + return config + + +def _build_vm_and_get_runcmd(vm_image_url=None, depot_channels=None, depot_env=None): + """Build a Vm, call _setup_cloud_init, and capture the generated runcmd.""" + config = _make_config() + + vm = Vm.__new__(Vm) + vm.vm_major_version = "8" + vm.vm_major_minor_version = "8.6" + vm.vm_image_url = vm_image_url + vm.depot_channels = depot_channels + vm.name = "lts-8.6" + vm.cloud_init_path = Path("/tmp/test_cloud_init.yaml") + + env = {"HOME": "/home/testuser"} + if depot_env: + env["DEPOT_USER"] = depot_env[0] + env["DEPOT_TOKEN"] = depot_env[1] + else: + env["DEPOT_USER"] = "" + env["DEPOT_TOKEN"] = "" + + captured_yaml = {} + + def fake_open(path, mode="r"): + path_str = str(path) + if mode == "w": + buf = StringIO() + + class WriteCapture: + def __enter__(self_inner): + return buf + + def __exit__(self_inner, *args): + captured_yaml["content"] = buf.getvalue() + + return WriteCapture() + if path_str == str(config.ssh_key): + buf = StringIO("ssh-ed25519 TESTKEY") + + class ReadCtx: + def __enter__(self_inner): + return buf + + def __exit__(self_inner, *args): + pass + + return ReadCtx() + return _real_open(path, mode) + + with patch("builtins.open", side_effect=fake_open), patch.dict(os.environ, env): + vm._setup_cloud_init(config=config) + + content = captured_yaml.get("content", "") + if content.startswith("#cloud-config\n"): + content = content[len("#cloud-config\n") :] + data = yaml.safe_load(content) + return data["runcmd"] + + +def test_cloud_init_vault_pin_present_when_image_pinned(): + pin = "https://dl.rockylinux.org/vault/rocky/8.6/images/Rocky-8-GenericCloud-8.6.20220702.0.x86_64.qcow2" + runcmd = _build_vm_and_get_runcmd(vm_image_url=pin) + + runcmd_strs = [str(c) for c in runcmd] + assert any("releasever" in s and "8.6" in s for s in runcmd_strs) + assert any("vault/rocky" in s for s in runcmd_strs) + assert any("mirrorlist" in s for s in runcmd_strs) + + +def test_cloud_init_no_vault_pin_when_no_image_url(): + runcmd = _build_vm_and_get_runcmd(vm_image_url=None) + + runcmd_strs = [str(c) for c in runcmd] + assert not any("releasever" in s for s in runcmd_strs) + assert not any("vault/rocky" in s for s in runcmd_strs) + + +def test_cloud_init_depot_install_always_present(): + runcmd = _build_vm_and_get_runcmd(vm_image_url=None) + runcmd_strs = [str(c) for c in runcmd] + assert any("depot.x86_64.rpm" in s for s in runcmd_strs) + + +def test_cloud_init_depot_login_when_creds_present(): + runcmd = _build_vm_and_get_runcmd( + depot_channels=["lts-8.6"], + depot_env=("myuser", "mytoken"), + ) + runcmd_strs = [str(c) for c in runcmd] + assert any("depot login -u myuser -t mytoken" in s for s in runcmd_strs) + assert any("depot enable lts-8.6 -y" in s for s in runcmd_strs) + + +def test_cloud_init_no_depot_login_without_creds(): + runcmd = _build_vm_and_get_runcmd(depot_channels=["lts-8.6"]) + runcmd_strs = [str(c) for c in runcmd] + assert not any("depot login" in s for s in runcmd_strs) + assert not any("depot enable" in s for s in runcmd_strs) + + +def test_cloud_init_ordering(): + """Vault pin, then depot, then kernel_install_dep.sh — in that order.""" + pin = "https://dl.rockylinux.org/vault/rocky/8.6/images/Rocky-8-GenericCloud-8.6.20220702.0.x86_64.qcow2" + runcmd = _build_vm_and_get_runcmd( + vm_image_url=pin, + depot_channels=["lts-8.6"], + depot_env=("myuser", "mytoken"), + ) + runcmd_strs = [str(c) for c in runcmd] + + vault_idx = next(i for i, s in enumerate(runcmd_strs) if "releasever" in s) + depot_install_idx = next(i for i, s in enumerate(runcmd_strs) if "depot.x86_64.rpm" in s) + depot_login_idx = next(i for i, s in enumerate(runcmd_strs) if "depot login" in s) + dep_script_idx = next(i for i, s in enumerate(runcmd_strs) if "kernel_install_dep.sh" in s) + + assert vault_idx < depot_install_idx < depot_login_idx < dep_script_idx From 4b422b105438529db56e4a31fe1896f0a63e918d Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Sat, 18 Jul 2026 15:19:27 -0400 Subject: [PATCH 3/7] kernel_install_dep.sh: Add python3, openssl, and powertools for EL8 Rocky 8.6 GenericCloud images lack python3 (only platform-python) and openssl, both needed for kernel builds. Also enable the powertools repo which provides dwarves/pahole required for BTF generation. Co-Authored-By: Claude Opus 4.6 --- kernel_install_dep.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kernel_install_dep.sh b/kernel_install_dep.sh index cf7122a..6de6b9c 100755 --- a/kernel_install_dep.sh +++ b/kernel_install_dep.sh @@ -10,6 +10,7 @@ install_kselftest_deps_8() { echo sudo dnf -y groupinstall 'Development Tools' sudo dnf -y install epel-release + sudo dnf config-manager --set-enabled powertools sudo dnf -y install --enablerepo=devel \ VirtualGL \ alsa-lib-devel \ @@ -43,9 +44,11 @@ install_kselftest_deps_8() { nftables \ nmap-ncat \ numactl-devel \ + openssl \ openssl-devel \ perf \ popt-devel \ + python3 \ python3-pip \ rsync \ socat \ From c8cf5e6bcefcbb698435179ffa4eb7b628dc6aab Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Wed, 22 Jul 2026 16:39:34 -0400 Subject: [PATCH 4/7] [VM] Harden test VM provisioning for vault-pinned minor releases Fix EPEL $releasever resolution on vault-pinned VMs, move packages unavailable on all 9.x minors to a soft-fail install, add dnf update and sshd restart post-install, and auto-reboot after cloud-init. Co-Authored-By: Claude Opus 4.6 --- kernel_install_dep.sh | 23 ++++++++++++++++++++--- kt/data/cloud_init.yaml | 6 ++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/kernel_install_dep.sh b/kernel_install_dep.sh index 6de6b9c..e65764c 100755 --- a/kernel_install_dep.sh +++ b/kernel_install_dep.sh @@ -10,6 +10,8 @@ install_kselftest_deps_8() { echo sudo dnf -y groupinstall 'Development Tools' sudo dnf -y install epel-release + # EPEL repos use $releasever which breaks when vault-pinned to a minor version (e.g. 8.6) + sudo sed -i 's/\$releasever/8/g' /etc/yum.repos.d/epel*.repo sudo dnf config-manager --set-enabled powertools sudo dnf -y install --enablerepo=devel \ VirtualGL \ @@ -74,6 +76,9 @@ install_kselftest_deps_8() { pyyaml \ scapy \ tftpy + + sudo dnf -y update + sudo systemctl restart sshd } install_kselftest_deps_9() { @@ -82,8 +87,9 @@ install_kselftest_deps_9() { echo sudo dnf -y groupinstall 'Development Tools' sudo dnf -y install epel-release + # EPEL repos use $releasever which breaks when vault-pinned to a minor version (e.g. 9.2) + sudo sed -i 's/\$releasever/9/g' /etc/yum.repos.d/epel*.repo sudo dnf -y install --enablerepo=crb,devel \ - VirtualGL \ alsa-lib-devel \ bc \ clang \ @@ -95,7 +101,6 @@ install_kselftest_deps_9() { ethtool \ fuse \ fuse-devel \ - gcc-toolset-13-libasan-devel \ glibc \ glibc-static \ iperf3 \ @@ -129,9 +134,15 @@ install_kselftest_deps_9() { tcpdump \ teamd \ traceroute \ - virtme-ng \ wget + # Not available on all 9.x minor releases + sudo dnf -y install --enablerepo=crb,devel \ + VirtualGL \ + gcc-toolset-13-libasan-devel \ + virtme-ng \ + || echo "Optional packages not available or install failed; continuing." + pip3 install --user \ jsonschema \ netaddr \ @@ -143,6 +154,9 @@ install_kselftest_deps_9() { scapy \ tftpy \ wheel + + sudo dnf -y update + sudo systemctl restart sshd } install_kselftest_deps_10() { @@ -211,6 +225,9 @@ install_kselftest_deps_10() { scapy \ tftpy \ wheel + + sudo dnf -y update + sudo systemctl restart sshd } case "${VERSION_ID%%.*}" in diff --git a/kt/data/cloud_init.yaml b/kt/data/cloud_init.yaml index 77101c4..cb39475 100644 --- a/kt/data/cloud_init.yaml +++ b/kt/data/cloud_init.yaml @@ -33,3 +33,9 @@ write_files: # workaround to change the owner to user runcmd: - [chown, USER_PLACEHOLDER:USER_PLACEHOLDER, HOMEDIR_PLACEHOLDER] + +power_state: + mode: reboot + message: "Rebooting after install" + timeout: 30 + condition: true From 7b4a662da044074c8271f4ce94c78533c973a443 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Thu, 23 Jul 2026 14:30:48 -0400 Subject: [PATCH 5/7] kt vm: Pin Rocky repos to vault to prevent minor version drift Without this, dnf resolves packages from the latest minor release instead of the one the image was built with. - Disable mirrorlist to stop dnf from redirecting to the latest minor version - Point baseurl to the Rocky vault where old minor versions are permanently hosted - Replace $releasever with the actual VERSION_ID from /etc/os-release to pin to the exact minor version - Clear dnf cache to force metadata refresh from the new vault URLs All of these were added in kt/data/cloud-init.yaml that is the base for all vms. When kt vm is run the first time for a kernel workspace, a copy of cloud-init.yaml is created. Use ruamel instead so that comments and the original formatting stays the same when the yaml file is read and then dump in python. Signed-off-by: Roxana Nicolescu add to a8edde3 --- kt/data/cloud_init.yaml | 14 +++++++++++--- kt/ktlib/vm.py | 11 ++++++++--- pyproject.toml | 3 ++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/kt/data/cloud_init.yaml b/kt/data/cloud_init.yaml index cb39475..2f983ab 100644 --- a/kt/data/cloud_init.yaml +++ b/kt/data/cloud_init.yaml @@ -15,8 +15,14 @@ ssh_pwauth: true package_upgrade: False mounts: - - ["mount_tag_mock_scratch", "SHARED_DIR_PLACEHOLDER", "virtiofs", "rw,relatime,context=unconfined_u:object_r:mock_var_lib_t:s0"] - - ["SHARED_DIR_PLACEHOLDER", "/var/lib/mock", "bind", "defaults,bind"] + - - mount_tag_mock_scratch + - SHARED_DIR_PLACEHOLDER + - virtiofs + - rw,relatime,context=unconfined_u:object_r:mock_var_lib_t:s0 + - - SHARED_DIR_PLACEHOLDER + - /var/lib/mock + - bind + - defaults,bind # Change working directory after boot # Setting up homedir does not work without tricks, because @@ -32,7 +38,9 @@ write_files: # and then root is the owner # workaround to change the owner to user runcmd: - - [chown, USER_PLACEHOLDER:USER_PLACEHOLDER, HOMEDIR_PLACEHOLDER] + - - chown + - USER_PLACEHOLDER:USER_PLACEHOLDER + - HOMEDIR_PLACEHOLDER power_state: mode: reboot diff --git a/kt/ktlib/vm.py b/kt/ktlib/vm.py index c4defeb..7ce2030 100644 --- a/kt/ktlib/vm.py +++ b/kt/ktlib/vm.py @@ -5,10 +5,10 @@ import time from dataclasses import dataclass -import oyaml as yaml import wget from git import Repo from pathlib3x import Path +from ruamel.yaml import YAML from urllib.parse import urlparse @@ -187,9 +187,15 @@ def _download_source_image(self, override_base: bool = False): wget.download(self._get_vm_url(), out=str(self.qcow2_source_path)) def _setup_cloud_init(self, config: Config): + yaml = YAML() + yaml.preserve_quotes = True + yaml.width = 4096 + yaml.default_flow_style = False + yaml.best_sequence_indent = 2 + data = None with open(CLOUD_INIT_BASE_PATH) as f: - data = yaml.safe_load(f) + data = yaml.load(f) # replace placeholders with user data data["users"][0]["name"] = config.user @@ -238,7 +244,6 @@ def _setup_cloud_init(self, config: Config): data["runcmd"].append([str(config.base_path / Path("kernel-src-tree-tools") / Path("kernel_install_dep.sh"))]) # Write this to image cloud_init with open(self.cloud_init_path, "w") as f: - f.write("#cloud-config\n") yaml.dump(data, f) def _create_image(self, config: Config, vcpus: int = 12, memory: int = 32768): diff --git a/pyproject.toml b/pyproject.toml index 5d73931..ac138d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,8 @@ dependencies = [ "click", "pathlib3x", "python3-wget", - "oyaml", + "ruamel.yaml", + "pyyaml", "pexpect", "jira", "requests", From e91cbb0027c95d63c8fb15c570e9ef1e982e3a35 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Wed, 22 Jul 2026 18:20:38 -0400 Subject: [PATCH 6/7] [VM] No Depot switch For those with Depot access defined in their OS environment there is no way to get a true vault pin of a VM. This is an unusual use case so add a negative flag to prevent installation of depot and setting the parameters. --- kt/commands/vm/command.py | 4 +++- kt/commands/vm/impl.py | 8 +++++++- kt/ktlib/vm.py | 33 +++++++++++++++++++-------------- tests/kt/ktlib/test_vm.py | 26 +++++++++++++++++++++++--- 4 files changed, 52 insertions(+), 19 deletions(-) diff --git a/kt/commands/vm/command.py b/kt/commands/vm/command.py index 0715b59..2b76d5a 100644 --- a/kt/commands/vm/command.py +++ b/kt/commands/vm/command.py @@ -71,8 +71,9 @@ @click.option("--test", is_flag=True, help="Build the kernel and run kselftests") @click.option("--vcpus", type=int, default=12, help="Number of virtual CPUs (default: 12)") @click.option("--memory", type=int, default=32768, help="Memory in MiB (default: 32768)") +@click.option("--no-depot", is_flag=True, help="Skip depot client installation and channel setup") @click.argument("kernel_workspace", required=False, shell_complete=ShellCompletion.show_kernel_workspaces) -def vm(kernel_workspace, console, destroy, override, override_base, list_all, test, vcpus, memory): +def vm(kernel_workspace, console, destroy, override, override_base, list_all, test, vcpus, memory, no_depot): if not list_all and not kernel_workspace: raise click.UsageError("kernel_workspace is required unless --list-all is specified") @@ -86,4 +87,5 @@ def vm(kernel_workspace, console, destroy, override, override_base, list_all, te test=test, vcpus=vcpus, memory=memory, + no_depot=no_depot, ) diff --git a/kt/commands/vm/impl.py b/kt/commands/vm/impl.py index 2c125fc..cd92475 100644 --- a/kt/commands/vm/impl.py +++ b/kt/commands/vm/impl.py @@ -16,6 +16,7 @@ def main( test: bool = False, vcpus: int = 12, memory: int = 32768, + no_depot: bool = False, ): if list_all: VmCommand.list_all() @@ -31,7 +32,12 @@ def main( return vm_instance = Vm.setup_and_spinup( - kernel_workspace_name=name, override=override, override_base=override_base, vcpus=vcpus, memory=memory + kernel_workspace_name=name, + override=override, + override_base=override_base, + vcpus=vcpus, + memory=memory, + no_depot=no_depot, ) config = Config.load() diff --git a/kt/ktlib/vm.py b/kt/ktlib/vm.py index 7ce2030..d9125d8 100644 --- a/kt/ktlib/vm.py +++ b/kt/ktlib/vm.py @@ -142,6 +142,7 @@ def setup_and_spinup( override_base: bool = False, vcpus: int = 12, memory: int = 32768, + no_depot: bool = False, ): """ Setup and spin up a VM from a kernel workspace name. @@ -152,6 +153,7 @@ def setup_and_spinup( override_base: If True, destroy and recreate the VM but override the base image as well vcpus: Number of virtual CPUs memory: Memory in MiB + no_depot: If True, skip depot client installation and channel setup Returns: VmInstance: The running VM instance @@ -163,7 +165,7 @@ def setup_and_spinup( vm.destroy() vm.setup(override_base=override_base) - vm_instance = vm.spin_up(config=config, vcpus=vcpus, memory=memory) + vm_instance = vm.spin_up(config=config, vcpus=vcpus, memory=memory, no_depot=no_depot) return vm_instance @@ -186,7 +188,7 @@ def _download_source_image(self, override_base: bool = False): logging.info(f"Downloading image from {self._get_vm_url()}") wget.download(self._get_vm_url(), out=str(self.qcow2_source_path)) - def _setup_cloud_init(self, config: Config): + def _setup_cloud_init(self, config: Config, no_depot: bool = False): yaml = YAML() yaml.preserve_quotes = True yaml.width = 4096 @@ -231,14 +233,17 @@ def _setup_cloud_init(self, config: Config): ) data["runcmd"].append("dnf clean all") - # Depot: always install the client, but only login+enable if credentials are present - data["runcmd"].append("dnf install -y https://depot.ciq.com/public/files/depot-client/depot/depot.x86_64.rpm") - depot_user = os.environ.get("DEPOT_USER") - depot_token = os.environ.get("DEPOT_TOKEN") - if depot_user and depot_token and self.depot_channels: - data["runcmd"].append(f"depot login -u {depot_user} -t {depot_token}") - for channel in self.depot_channels: - data["runcmd"].append(f"depot enable {channel} -y") + # Depot: install the client and login+enable if credentials are present + if not no_depot: + data["runcmd"].append( + "dnf install -y https://depot.ciq.com/public/files/depot-client/depot/depot.x86_64.rpm" + ) + depot_user = os.environ.get("DEPOT_USER") + depot_token = os.environ.get("DEPOT_TOKEN") + if depot_user and depot_token and self.depot_channels: + data["runcmd"].append(f"depot login -u {depot_user} -t {depot_token}") + for channel in self.depot_channels: + data["runcmd"].append(f"depot enable {channel} -y") # Install packages needed later data["runcmd"].append([str(config.base_path / Path("kernel-src-tree-tools") / Path("kernel_install_dep.sh"))]) @@ -246,11 +251,11 @@ def _setup_cloud_init(self, config: Config): with open(self.cloud_init_path, "w") as f: yaml.dump(data, f) - def _create_image(self, config: Config, vcpus: int = 12, memory: int = 32768): + def _create_image(self, config: Config, vcpus: int = 12, memory: int = 32768, no_depot: bool = False): # Make sure the dir exists self.qcow2_path.parent.mkdir(parents=True, exist_ok=True) - self._setup_cloud_init(config=config) + self._setup_cloud_init(config=config, no_depot=no_depot) # Copy qcow2 image to work dir self.qcow2_source_path.copy(self.qcow2_path) @@ -282,11 +287,11 @@ def _resize_disk(self): def setup(self, override_base: bool = False): self._download_source_image(override_base=override_base) - def spin_up(self, config: Config, vcpus: int = 12, memory: int = 32768) -> VmInstance: + def spin_up(self, config: Config, vcpus: int = 12, memory: int = 32768, no_depot: bool = False) -> VmInstance: if not VirtHelper.exists(vm_name=self.name): logging.info(f"VM {self.name} does not exist, creating from scratch...") - self._create_image(config=config, vcpus=vcpus, memory=memory) + self._create_image(config=config, vcpus=vcpus, memory=memory, no_depot=no_depot) return VmInstance(name=self.name, kernel_workspace=self.kernel_workspace) logging.info(f"Vm {self.name} already exists") diff --git a/tests/kt/ktlib/test_vm.py b/tests/kt/ktlib/test_vm.py index 2a4f187..d76ee20 100644 --- a/tests/kt/ktlib/test_vm.py +++ b/tests/kt/ktlib/test_vm.py @@ -90,7 +90,7 @@ def _make_config(): return config -def _build_vm_and_get_runcmd(vm_image_url=None, depot_channels=None, depot_env=None): +def _build_vm_and_get_runcmd(vm_image_url=None, depot_channels=None, depot_env=None, no_depot=False): """Build a Vm, call _setup_cloud_init, and capture the generated runcmd.""" config = _make_config() @@ -139,7 +139,7 @@ def __exit__(self_inner, *args): return _real_open(path, mode) with patch("builtins.open", side_effect=fake_open), patch.dict(os.environ, env): - vm._setup_cloud_init(config=config) + vm._setup_cloud_init(config=config, no_depot=no_depot) content = captured_yaml.get("content", "") if content.startswith("#cloud-config\n"): @@ -166,7 +166,7 @@ def test_cloud_init_no_vault_pin_when_no_image_url(): assert not any("vault/rocky" in s for s in runcmd_strs) -def test_cloud_init_depot_install_always_present(): +def test_cloud_init_depot_install_present_by_default(): runcmd = _build_vm_and_get_runcmd(vm_image_url=None) runcmd_strs = [str(c) for c in runcmd] assert any("depot.x86_64.rpm" in s for s in runcmd_strs) @@ -205,3 +205,23 @@ def test_cloud_init_ordering(): dep_script_idx = next(i for i, s in enumerate(runcmd_strs) if "kernel_install_dep.sh" in s) assert vault_idx < depot_install_idx < depot_login_idx < dep_script_idx + + +def test_cloud_init_no_depot_skips_all_depot_commands(): + """no_depot=True skips depot even when creds and channels are present.""" + runcmd = _build_vm_and_get_runcmd( + depot_channels=["lts-8.6"], + depot_env=("myuser", "mytoken"), + no_depot=True, + ) + runcmd_strs = [str(c) for c in runcmd] + assert not any("depot" in s for s in runcmd_strs) + assert any("kernel_install_dep.sh" in s for s in runcmd_strs) + + +def test_cloud_init_no_depot_skips_depot_install(): + """no_depot=True omits the depot RPM install (no creds variant).""" + runcmd = _build_vm_and_get_runcmd(no_depot=True) + runcmd_strs = [str(c) for c in runcmd] + assert not any("depot.x86_64.rpm" in s for s in runcmd_strs) + assert any("kernel_install_dep.sh" in s for s in runcmd_strs) From 55c1cfff5dd090c0181acb08e740e7e21d36efcb Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Wed, 22 Jul 2026 18:41:20 -0400 Subject: [PATCH 7/7] [TESTS] Fix vm tests with ruamel update --- tests/kt/ktlib/test_vm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/kt/ktlib/test_vm.py b/tests/kt/ktlib/test_vm.py index d76ee20..990bb0e 100644 --- a/tests/kt/ktlib/test_vm.py +++ b/tests/kt/ktlib/test_vm.py @@ -2,7 +2,7 @@ from io import StringIO from unittest.mock import MagicMock, patch -import oyaml as yaml +from ruamel.yaml import YAML from pathlib3x import Path from kt.ktlib.util import Constants @@ -144,7 +144,7 @@ def __exit__(self_inner, *args): content = captured_yaml.get("content", "") if content.startswith("#cloud-config\n"): content = content[len("#cloud-config\n") :] - data = yaml.safe_load(content) + data = YAML(typ="safe").load(content) return data["runcmd"]