aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNikolay Govorov <me@govorov.online>2026-07-12 22:52:18 +0100
committerNikolay Govorov <me@govorov.online>2026-07-12 22:55:50 +0100
commita0167823ba254acdda4c29c64fbbad1ca2f8a1af (patch)
treecb338e5c73b2458df50be0a2099509bf5d6b8d69
parent9dede6532c0130dab8dc7d8d924655811e10ea60 (diff)
downloadtar
tar.gz
tar.bz2
tar.lz
tar.xz
tar.zst
zip
Add qemu-guest-agent to golden images
Diffstat
-rw-r--r--crates/hule-image/src/lib.rs41+37 −4
-rw-r--r--crates/hule/src/main.rs2+1 −1
-rwxr-xr-ximages/alpine/genimg20+17 −3
-rwxr-xr-ximages/debian/genimg6+4 −2
-rwxr-xr-ximages/fedora/genimg7+5 −2
-rwxr-xr-ximages/freebsd/genimg6+4 −2
-rwxr-xr-ximages/ubuntu/genimg7+5 −2
7 files changed, 73 insertions, 16 deletions
diff --git a/crates/hule-image/src/lib.rs b/crates/hule-image/src/lib.rs
index 6974882..6165294 100644
--- a/crates/hule-image/src/lib.rs
+++ b/crates/hule-image/src/lib.rs
@@ -326,6 +326,12 @@ pub enum Access {
user: String,
auth: String,
},
+ /// qemu-guest-agent. The hypervisor backend chooses and configures a
+ /// compatible transport; the image only promises that the agent runs.
+ Qga {
+ #[serde(rename = "type")]
+ kind: String,
+ },
/// Unrecognized access surface, preserved verbatim.
Unknown(serde_json::Value),
}
@@ -353,6 +359,7 @@ impl<'de> Deserialize<'de> for Access {
auth: raw.auth,
})
}
+ "qga" => Ok(Self::Qga { kind: "qga".into() }),
_ => Ok(Self::Unknown(value)),
}
}
@@ -375,7 +382,7 @@ impl Access {
validation.required(user, &format!("{field}.user"));
validation.required(auth, &format!("{field}.auth"));
}
- Self::Unknown(_) => {}
+ Self::Qga { .. } | Self::Unknown(_) => {}
}
}
}
@@ -413,6 +420,7 @@ pub struct Machine {
pub cpu: ResourceRange,
pub ram: ResourceRange,
pub boot: Vec<Boot>,
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub access: Vec<Access>,
pub network: Network,
}
@@ -429,10 +437,9 @@ impl Machine {
for (index, boot) in self.boot.iter().enumerate() {
boot.validate_into(&format!("{field}.boot[{index}]"), validation);
}
- if !self.access.iter().any(Access::is_recognized) {
+ if !self.access.is_empty() && !self.access.iter().any(Access::is_recognized) {
validation.error(format!(
- "{field}.access must contain at least one supported access method \
- (a running machine must be reachable by something, if only to shut it down)"
+ "{field}.access must contain at least one supported access method"
));
}
for (index, access) in self.access.iter().enumerate() {
@@ -761,6 +768,32 @@ mod tests {
}
#[test]
+ fn parses_qga_access() {
+ let json = String::from_utf8(valid_json()).unwrap().replace(
+ r#"[{"type":"ssh","port":22,"user":"build","auth":"empty-password"}]"#,
+ r#"[{"type":"ssh","port":22,"user":"build","auth":"empty-password"},{"type":"qga"}]"#,
+ );
+ let image = MachineImage::from_json(json.as_bytes()).unwrap();
+ assert_eq!(image.machine.access.len(), 2);
+ assert!(matches!(image.machine.access[1], Access::Qga { .. }));
+ }
+
+ #[test]
+ fn access_may_be_omitted_for_a_black_box_image() {
+ let json = String::from_utf8(valid_json()).unwrap().replace(
+ r#","access":[{"type":"ssh","port":22,"user":"build","auth":"empty-password"}]"#,
+ "",
+ );
+ let image = MachineImage::from_json(json.as_bytes()).unwrap();
+ assert!(image.machine.access.is_empty());
+ assert!(
+ serde_json::to_value(image).unwrap()["machine"]
+ .get("access")
+ .is_none()
+ );
+ }
+
+ #[test]
fn parses_dhcp_network_without_static_parameters() {
let json = String::from_utf8(valid_json()).unwrap().replace(
r#"{"mode":"static","address":"10.0.2.15/24","gateway":"10.0.2.2"}"#,
diff --git a/crates/hule/src/main.rs b/crates/hule/src/main.rs
index 58ffd34..82ff039 100644
--- a/crates/hule/src/main.rs
+++ b/crates/hule/src/main.rs
@@ -506,7 +506,7 @@ async fn cmd_run(reference_str: &str, port: u16) -> R<()> {
.iter()
.find_map(|a| match a {
Access::Ssh { port, .. } => Some(*port),
- Access::Unknown(_) => None,
+ Access::Qga { .. } | Access::Unknown(_) => None,
})
.unwrap_or(22);
let settings = Settings {
diff --git a/images/alpine/genimg b/images/alpine/genimg
index 3bf3e2d..76405a4 100755
--- a/images/alpine/genimg
+++ b/images/alpine/genimg
@@ -90,7 +90,7 @@ apk add -U \
--initdb \
acct alpine-base alpine-conf alpine-sdk linux-firmware-none $linux \
git mercurial openssh sudo syslinux tzdata gnupg haveged bash curl \
- doas
+ doas qemu-guest-agent
mount --bind /dev /mnt/dev
mount --bind /dev/pts /mnt/dev/pts
@@ -135,7 +135,20 @@ fi
extlinux -i /mnt/boot
-for i in ntpd sshd crond haveged; do
+# Alpine uses mdev rather than udev, so create the stable, named virtio-port
+# symlink expected by qemu-ga without depending on controller/port numbers.
+mkdir -p /mnt/etc/mdev
+cat >/mnt/etc/mdev/virtio-port <<'EOF'
+#!/bin/sh
+name=$(cat "/sys/class/virtio-ports/$MDEV/name") || exit
+[ "$name" = org.qemu.guest_agent.0 ] || exit
+mkdir -p /dev/virtio-ports
+ln -sf "/dev/$MDEV" "/dev/virtio-ports/$name"
+EOF
+chmod +x /mnt/etc/mdev/virtio-port
+echo 'vport.* root:root 0600 @/etc/mdev/virtio-port' >>/mnt/etc/mdev.conf
+
+for i in ntpd sshd crond haveged qemu-guest-agent; do
run_root rc-update add $i default
done
for i in hwclock modules sysctl hostname bootmisc loadkmap networking seedrng syslog swap; do
@@ -228,7 +241,8 @@ cat >"$out/config.json" <<EOF
}
],
"access": [
- { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" }
+ { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" },
+ { "type": "qga" }
],
"network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" }
},
diff --git a/images/debian/genimg b/images/debian/genimg
index 116fab2..85e363a 100755
--- a/images/debian/genimg
+++ b/images/debian/genimg
@@ -135,7 +135,7 @@ run_root apt-get -y install locales
run_root apt-get -y install $kpkg
run_root apt-get -y install build-essential git mercurial ssh sudo \
gnupg dirmngr ca-certificates apt-transport-https curl dbus \
- systemd-timesyncd ncurses-term
+ systemd-timesyncd ncurses-term qemu-guest-agent
run_root ln -sf /usr/share/zoneinfo/UTC /etc/localtime
run_root systemctl enable systemd-timesyncd.service
@@ -147,6 +147,7 @@ echo '%sudo ALL=(ALL) NOPASSWD: ALL' >>/mnt/etc/sudoers
echo "PermitEmptyPasswords yes" >>/mnt/etc/ssh/sshd_config
echo ssh >>/mnt/etc/securetty
run_root systemctl enable ssh
+run_root systemctl enable qemu-guest-agent
# Prevent docker from mucking up networking
mkdir -p /mnt/etc/docker
@@ -275,7 +276,8 @@ cat >"$out/config.json" <<EOF
$protocols
],
"access": [
- { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" }
+ { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" },
+ { "type": "qga" }
],
"network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" }
},
diff --git a/images/fedora/genimg b/images/fedora/genimg
index 35c2695..56e2a5d 100755
--- a/images/fedora/genimg
+++ b/images/fedora/genimg
@@ -141,7 +141,8 @@ run_root systemctl enable systemd-timesyncd.service
run_root dnf -y \
--releasever="$release" \
install \
- @development-tools git mercurial openssh-server sudo kernel grub2
+ @development-tools git mercurial openssh-server sudo kernel grub2 \
+ qemu-guest-agent
run_root dnf clean all
@@ -166,6 +167,7 @@ echo '%sudo ALL=(ALL) NOPASSWD: ALL' >>/mnt/etc/sudoers
echo "PermitEmptyPasswords yes" >>/mnt/etc/ssh/sshd_config
echo ssh >>/mnt/etc/securetty
run_root systemctl enable sshd
+run_root systemctl enable qemu-guest-agent
boot_uuid=$(blkid --match-tag UUID --output value ${NBD_DEVICE}p1)
swap_uuid=$(blkid --match-tag UUID --output value ${NBD_DEVICE}p2)
@@ -213,7 +215,8 @@ cat >"$out/config.json" <<EOF
}
],
"access": [
- { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" }
+ { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" },
+ { "type": "qga" }
],
"network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" }
},
diff --git a/images/freebsd/genimg b/images/freebsd/genimg
index e360436..4d40076 100755
--- a/images/freebsd/genimg
+++ b/images/freebsd/genimg
@@ -141,7 +141,7 @@ esac
# resolver; the guest resolv.conf written below is purely for runtime.
pkg -C /dev/null -R "$reposdir" -r "$rootfs" update
pkg -C /dev/null -R "$reposdir" -r "$rootfs" install -y \
- pkg git-lite mercurial sudo curl
+ pkg git-lite mercurial sudo curl qemu-guest-agent
pkg -C /dev/null -R "$reposdir" -r "$rootfs" clean -ay
if [ "$with_ports" = true ]; then
@@ -157,6 +157,7 @@ cat >>"$rootfs"/etc/rc.conf <<EOF
ntpd_enable=YES
sshd_enable=YES
growfs_enable=YES
+qemu_guest_agent_enable=YES
hostname="build"
ifconfig_DEFAULT="inet 10.0.2.15 netmask 255.255.255.0"
defaultrouter="10.0.2.2"
@@ -229,7 +230,8 @@ cat >"$out/config.json" <<CONFIG
}
],
"access": [
- { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" }
+ { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" },
+ { "type": "qga" }
],
"network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" }
},
diff --git a/images/ubuntu/genimg b/images/ubuntu/genimg
index 114438c..28d6e08 100755
--- a/images/ubuntu/genimg
+++ b/images/ubuntu/genimg
@@ -142,7 +142,8 @@ run_root apt-get -y install locales
run_root apt-get -y install linux-base
run_root apt-get -y install linux-image-generic
run_root apt-get -y install \
- build-essential git mercurial ssh sudo dirmngr curl ca-certificates
+ build-essential git mercurial ssh sudo dirmngr curl ca-certificates \
+ qemu-guest-agent
if [ "$arch" = "amd64" ]; then
extlinux -i /mnt/boot
@@ -159,6 +160,7 @@ echo '%sudo ALL=(ALL) NOPASSWD: ALL' >>/mnt/etc/sudoers
echo "PermitEmptyPasswords yes" >>/mnt/etc/ssh/sshd_config
echo ssh >>/mnt/etc/securetty
run_root systemctl enable ssh
+run_root systemctl enable qemu-guest-agent
# Prevent docker from mucking up networking
mkdir -p /mnt/etc/docker
@@ -272,7 +274,8 @@ cat >"$out/config.json" <<EOF
$protocols
],
"access": [
- { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" }
+ { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" },
+ { "type": "qga" }
],
"network": {
"mode": "$net_mode",