Skip to content

Commit e7cceca

Browse files
committed
test(qemu): replace Fedora Cloud image with Alpine Linux tiny image
## Motivation The Fedora Cloud qcow2 images used in QEMU driver tests weigh ~556 MB (x86_64) + ~519 MB (aarch64) = ~1.1 GB total, taking ~1 minute to download on every CI cache miss and consuming significant bandwidth. ## Image replacement Replace with Alpine Linux 3.22.4 UEFI tiny cloud images from dl-cdn.alpinelinux.org: x86_64: 556 MB → 127 MB (77% reduction) aarch64: 519 MB → 151 MB (71% reduction) Total: ~1076 MB → ~278 MB (74% reduction) Alpine's nocloud tiny images support UEFI boot on both architectures and use tiny-cloud (a minimal cloud-init alternative) that reads the standard NoCloud CIDATA vfat volume the QEMU driver already generates. ## Driver changes (driver.py) tiny-cloud has two behavioural differences from full cloud-init: 1. Hostname: tiny-cloud reads @hostname from the YAML key 'hostname' in meta-data, not 'local-hostname'. Add 'hostname' alongside 'local-hostname' so both cloud-init and tiny-cloud set it correctly. 2. Password: tiny-cloud does not support plain_text_passwd in the users stanza. Add a runcmd entry ('echo user:pass | chpasswd') which tiny-cloud does execute. Full cloud-init images are unaffected. ## Test changes (driver_test.py) Alpine has no SELinux and uses a different default shell prompt: - Remove 'sudo setenforce 0' (no SELinux on Alpine). - Update post-login prompt match from bash '[user@host ~]$' to Alpine ash 'host:~$'. - Relax 'uname -r' assertion from the Fedora-specific kernel string to a non-empty check. - Press Enter when the GRUB countdown appears to skip the 10 s wait (detected via 'automatically in '; if the image already booted past GRUB the code falls through directly to the login prompt). ## SSH: TCP hostfwd instead of vsock Fedora cloud images ship systemd-ssh-generator (part of systemd ≥ 256) which automatically binds sshd to AF_VSOCK port 22 when booting inside a VM. Alpine uses OpenRC — no systemd, no generator — so sshd only listens on TCP. The fix: allocate a free host port at test start and pass it as hostfwd={'ssh': {protocol, hostaddr, hostport, guestport=22}}. Because hostfwd entries are written to children['ssh'] after the VsockNetwork, they override it, so qemu.shell() transparently uses TCP without any other code changes.
1 parent ada5fa8 commit e7cceca

3 files changed

Lines changed: 33 additions & 14 deletions

File tree

.github/workflows/python-tests.yaml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,19 +120,19 @@ jobs:
120120
run: |
121121
brew install renode/tap/renode
122122
123-
- name: Cache Fedora Cloud images
124-
id: cache-fedora-cloud-images
123+
- name: Cache Alpine cloud images
124+
id: cache-alpine-cloud-images
125125
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
126126
with:
127127
path: python/packages/jumpstarter-driver-qemu/images
128-
key: fedora-cloud-43-1.6
128+
key: alpine-3.22.4-uefi-tiny
129129

130-
- name: Download Fedora Cloud images
131-
if: steps.cache-fedora-cloud-images.outputs.cache-hit != 'true'
130+
- name: Download Alpine cloud images
131+
if: steps.cache-alpine-cloud-images.outputs.cache-hit != 'true'
132132
run: |
133133
for arch in aarch64 x86_64; do
134-
curl -L --fail --output "python/packages/jumpstarter-driver-qemu/images/Fedora-Cloud-Base-Generic-43-1.6.${arch}.qcow2" \
135-
"https://iad.mirror.rackspace.com/fedora/releases/43/Cloud/${arch}/images/Fedora-Cloud-Base-Generic-43-1.6.${arch}.qcow2"
134+
curl -L --fail --output "python/packages/jumpstarter-driver-qemu/images/nocloud_alpine-3.22.4-${arch}-uefi-tiny-r0.qcow2" \
135+
"https://dl-cdn.alpinelinux.org/alpine/v3.22/releases/cloud/nocloud_alpine-3.22.4-${arch}-uefi-tiny-r0.qcow2"
136136
done
137137
138138
- name: Run pytest

python/packages/jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ def cidata(self) -> TemporaryDirectory:
512512
{
513513
"instance-id": str(self.uuid),
514514
"local-hostname": self.hostname,
515+
"hostname": self.hostname,
515516
}
516517
)
517518
)
@@ -528,6 +529,13 @@ def cidata(self) -> TemporaryDirectory:
528529
"sudo": "ALL=(ALL) NOPASSWD:ALL",
529530
}
530531
],
532+
# runcmd sets the password explicitly for cloud-init implementations
533+
# that do not support plain_text_passwd (e.g. Alpine's tiny-cloud).
534+
# cloud-init ignores runcmd entries it doesn't understand, so this
535+
# is safe to include unconditionally.
536+
"runcmd": [
537+
f"echo '{self.username}:{self.password}' | chpasswd",
538+
],
531539
}
532540
)
533541
)

python/packages/jumpstarter-driver-qemu/jumpstarter_driver_qemu/driver_test.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
import os
44
import platform
5+
import socket
56
import sys
67
import tarfile
78
from pathlib import Path
@@ -59,26 +60,34 @@ def get_native_arch_config():
5960
def test_driver_qemu(tmp_path, ovmf):
6061
arch, ovmf_arch = get_native_arch_config()
6162

63+
# Alpine uses OpenRC (not systemd), so systemd-ssh-generator does not run
64+
# and sshd never binds to AF_VSOCK. Use a TCP hostfwd instead so that
65+
# qemu.shell() connects over standard TCP SSH, which always works.
66+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as _s:
67+
_s.bind(("127.0.0.1", 0))
68+
ssh_host_port = _s.getsockname()[1]
69+
6270
with serve(
6371
Qemu(
6472
arch=arch,
6573
default_partitions={
6674
"OVMF_CODE.fd": ovmf / ovmf_arch / "code.fd",
6775
"OVMF_VARS.fd": ovmf / ovmf_arch / "vars.fd",
6876
},
77+
hostfwd={"ssh": {"protocol": "tcp", "hostaddr": "127.0.0.1", "hostport": ssh_host_port, "guestport": 22}},
6978
)
7079
) as qemu:
7180
hostname = qemu.hostname
7281
username = qemu.username
7382
password = qemu.password
7483

75-
cached_image = Path(__file__).parent.parent / "images" / f"Fedora-Cloud-Base-Generic-43-1.6.{arch}.qcow2"
84+
cached_image = Path(__file__).parent.parent / "images" / f"nocloud_alpine-3.22.4-{arch}-uefi-tiny-r0.qcow2"
7685

7786
if cached_image.exists():
7887
qemu.flasher.flash(cached_image.resolve())
7988
else:
8089
qemu.flasher.flash(
81-
f"https://download.fedoraproject.org/pub/fedora/linux/releases/43/Cloud/{arch}/images/Fedora-Cloud-Base-Generic-43-1.6.{arch}.qcow2",
90+
f"https://dl-cdn.alpinelinux.org/alpine/v3.22/releases/cloud/nocloud_alpine-3.22.4-{arch}-uefi-tiny-r0.qcow2",
8291
)
8392

8493
qemu.power.on()
@@ -88,16 +97,18 @@ def test_driver_qemu(tmp_path, ovmf):
8897

8998
with qemu.console.pexpect() as p:
9099
p.logfile = sys.stdout.buffer
91-
p.expect_exact(f"{hostname} login:", timeout=600)
100+
# Press Enter if GRUB is waiting; if it already booted, go straight to login
101+
idx = p.expect_exact(["automatically in ", f"{hostname} login:"], timeout=60)
102+
if idx == 0:
103+
p.sendline("")
104+
p.expect_exact(f"{hostname} login:", timeout=600)
92105
p.sendline(username)
93106
p.expect_exact("Password:")
94107
p.sendline(password)
95-
p.expect_exact(f"[{username}@{hostname} ~]$")
96-
p.sendline("sudo setenforce 0")
97-
p.expect_exact(f"[{username}@{hostname} ~]$")
108+
p.expect_exact(f"{hostname}:~$")
98109

99110
with qemu.shell() as s:
100-
assert s.run("uname -r").stdout.strip() == f"6.17.1-300.fc43.{arch}"
111+
assert s.run("uname -r").stdout.strip() != ""
101112

102113
qemu.power.off()
103114

0 commit comments

Comments
 (0)