diff options
Diffstat (limited to 'crates/hule-vmm/src/backend/qemu/mod.rs')
| -rw-r--r-- | crates/hule-vmm/src/backend/qemu/mod.rs | 120 | +103 −17 |
1 files changed, 103 insertions, 17 deletions
diff --git a/crates/hule-vmm/src/backend/qemu/mod.rs b/crates/hule-vmm/src/backend/qemu/mod.rs index 75f51c3..0cd47cb 100644 --- a/crates/hule-vmm/src/backend/qemu/mod.rs +++ b/crates/hule-vmm/src/backend/qemu/mod.rs @@ -1,16 +1,19 @@ // SPDX-FileCopyrightText: 2026 Nikolay Govorov // SPDX-License-Identifier: Apache-2.0 -//! QMP is an unconditional implementation detail of this backend and backs -//! lifecycle operations that cannot be expressed through process signals. -//! Guest access and console channels are separate concerns and are not wired -//! up here yet. +//! QMP is an unconditional implementation detail of this backend. Guest +//! access is independent: an image may be a black box, or may promise QGA; +//! a promised QGA channel must become ready for `start()` to succeed. +//! +//! `logs`/`attach` use a dedicated serial console; `exec` uses QGA. +//! Console resizing and `reattach` are not implemented yet. //! //! `cli` is the typed qemu CLI wrapper; `supervise` gets an exit code out of //! a process even if we stop being its parent; this module only translates //! Hule's image/machine types into both. mod cli; +mod qga; mod qmp; mod supervise; @@ -19,7 +22,7 @@ use std::process::{ExitStatus, Output}; use std::time::Duration; use async_trait::async_trait; -use hule_image::{Architecture, Boot, BootLinux, Disk, MachineImage}; +use hule_image::{Access, Architecture, Boot, BootLinux, Disk, MachineImage}; use tokio::process::Child; use tokio::sync::Mutex; @@ -53,7 +56,9 @@ pub struct QemuMachine { settings: Settings, disk: Disk, boot: QemuBoot, + qga: bool, child: Mutex<ChildState>, + qga_lock: Mutex<()>, qmp_client: Mutex<Option<qmp::Qmp>>, } @@ -76,11 +81,35 @@ impl QemuMachine { self.dir.join("qemu.qmp-port") } - async fn qmp_port(&self) -> R<u16> { - let data = tokio::fs::read_to_string(self.qmp_port_path()).await?; + fn qga_port_path(&self) -> PathBuf { + self.dir.join("qemu.qga-port") + } + + fn console_port_path(&self) -> PathBuf { + self.dir.join("qemu.console-port") + } + + fn console_log_path(&self) -> PathBuf { + self.dir.join("console.log") + } + + async fn read_port(&self, path: &Path, name: &str) -> R<u16> { + let data = tokio::fs::read_to_string(path).await?; data.trim() .parse() - .map_err(|_| Error::InvalidState("invalid qemu.qmp-port contents".into())) + .map_err(|_| Error::InvalidState(format!("invalid {name} port contents"))) + } + + async fn qmp_port(&self) -> R<u16> { + self.read_port(&self.qmp_port_path(), "qmp").await + } + + async fn qga_port(&self) -> R<u16> { + self.read_port(&self.qga_port_path(), "qga").await + } + + async fn console_port(&self) -> R<u16> { + self.read_port(&self.console_port_path(), "console").await } /// Connects on first use (there's a short window after spawn before @@ -194,11 +223,20 @@ impl Machine for QemuMachine { } async fn stats(&self) -> R<Stats> { - todo!("needs /proc or QMP query-blockstats wiring") + let pid = self.qemu_pid().await?; + let (cpu_time_ns, memory_bytes) = supervise::process_stats(pid).await?; + Ok(Stats { + cpu_time_ns, + memory_bytes, + }) } async fn logs(&self) -> R<Vec<u8>> { - todo!("needs a captured serial console, not wired up yet") + match tokio::fs::read(self.console_log_path()).await { + Ok(logs) => Ok(logs), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()), + Err(error) => Err(error.into()), + } } async fn start(&mut self) -> R<()> { @@ -209,10 +247,28 @@ impl Machine for QemuMachine { let _ = tokio::fs::remove_file(self.pid_path()).await; let _ = tokio::fs::remove_file(self.exit_code_path()).await; let _ = tokio::fs::remove_file(self.qmp_port_path()).await; + let _ = tokio::fs::remove_file(self.qga_port_path()).await; + let _ = tokio::fs::remove_file(self.console_port_path()).await; + let _ = tokio::fs::remove_file(self.console_log_path()).await; *self.qmp_client.lock().await = None; let qmp_port = qmp::free_port().await?; tokio::fs::write(self.qmp_port_path(), qmp_port.to_string()).await?; + let qga_port = if self.qga { + let mut port = qmp::free_port().await?; + while port == qmp_port { + port = qmp::free_port().await?; + } + tokio::fs::write(self.qga_port_path(), port.to_string()).await?; + Some(port) + } else { + None + }; + let mut console_port = qmp::free_port().await?; + while console_port == qmp_port || Some(console_port) == qga_port { + console_port = qmp::free_port().await?; + } + tokio::fs::write(self.console_port_path(), console_port.to_string()).await?; let kvm = Architecture::host() == Some(self.arch) && cli::kvm_available().await; let sys_arch = match self.arch { @@ -274,10 +330,20 @@ impl Machine for QemuMachine { } cmd.pidfile = Some(self.pid_path()); cmd.qmp_port = Some(qmp_port); + cmd.qga_port = qga_port; + cmd.console = Some((console_port, self.console_log_path())); let argv = cmd.to_argv(); let wrapper = supervise::spawn(&argv, &self.exit_code_path())?; *child = ChildState::Running(wrapper); + drop(child); + + if let Some(port) = qga_port + && let Err(error) = qga::wait_until_ready(port, Duration::from_secs(60)).await + { + let _ = self.kill().await; + return Err(error); + } Ok(()) } @@ -321,6 +387,9 @@ impl Machine for QemuMachine { let _ = tokio::fs::remove_file(self.pid_path()).await; let _ = tokio::fs::remove_file(self.exit_code_path()).await; let _ = tokio::fs::remove_file(self.qmp_port_path()).await; + let _ = tokio::fs::remove_file(self.qga_port_path()).await; + let _ = tokio::fs::remove_file(self.console_port_path()).await; + let _ = tokio::fs::remove_file(self.console_log_path()).await; Ok(()) } @@ -391,16 +460,27 @@ impl Machine for QemuMachine { } } - async fn exec(&self, _cmd: &[String]) -> R<Output> { - todo!("needs the access channel (ssh today) wired up here") + async fn exec(&self, cmd: &[String]) -> R<Output> { + if !self.qga { + return Err(Error::Unsupported( + "this image does not declare qga access".into(), + )); + } + let _guard = self.qga_lock.lock().await; + qga::exec(self.qga_port().await?, cmd).await } async fn attach(&self) -> R<Box<dyn Console>> { - todo!("needs a serial console channel, not wired up yet") - } - - async fn resize(&self, _cols: u16, _rows: u16) -> R<()> { - todo!("depends on attach") + let port = self.console_port().await?; + for _ in 0..50 { + match tokio::net::TcpStream::connect(("127.0.0.1", port)).await { + Ok(stream) => return Ok(Box::new(stream)), + Err(_) => tokio::time::sleep(Duration::from_millis(20)).await, + } + } + Err(Error::InvalidState( + "qemu serial console did not become available".into(), + )) } } @@ -460,7 +540,13 @@ impl Hypervisor for QemuHypervisor { settings: settings.clone(), disk, boot: qemu_boot, + qga: image + .machine + .access + .iter() + .any(|access| matches!(access, Access::Qga { .. })), child: Mutex::new(ChildState::NotStarted), + qga_lock: Mutex::new(()), qmp_client: Mutex::new(None), })) } |
