From d17357067eab1dbea2f06fc3bb3f3b8d8b57ca43 Mon Sep 17 00:00:00 2001 From: Changyuan Lyu Date: Tue, 23 Dec 2025 13:57:06 -0800 Subject: [PATCH 1/2] refactor(vm): separate argument parsing and device creation Introduce a unified VM config structure, let CLI focuse on argument parsing only, and move device creation into core crate. Signed-off-by: Changyuan Lyu --- alioth-cli/src/boot.rs | 428 ++++++------------ alioth/src/board/board.rs | 1 + alioth/src/device/device.rs | 1 + .../dev/net/mac_addr.rs => device/net.rs} | 4 +- .../mac_addr_test.rs => device/net_test.rs} | 0 alioth/src/hv/hv.rs | 21 + alioth/src/lib.rs | 1 + alioth/src/loader/loader.rs | 5 +- alioth/src/virtio/dev/net/net.rs | 4 +- alioth/src/virtio/dev/net/tap.rs | 2 +- alioth/src/virtio/dev/net/vmnet.rs | 2 +- alioth/src/vm/config.rs | 118 +++++ alioth/src/{ => vm}/vm.rs | 125 ++++- 13 files changed, 408 insertions(+), 304 deletions(-) rename alioth/src/{virtio/dev/net/mac_addr.rs => device/net.rs} (97%) rename alioth/src/{virtio/dev/net/mac_addr_test.rs => device/net_test.rs} (100%) create mode 100644 alioth/src/vm/config.rs rename alioth/src/{ => vm}/vm.rs (78%) diff --git a/alioth-cli/src/boot.rs b/alioth-cli/src/boot.rs index 8204bb8b..2306eef5 100644 --- a/alioth-cli/src/boot.rs +++ b/alioth-cli/src/boot.rs @@ -14,6 +14,7 @@ use std::collections::HashMap; use std::ffi::CString; +use std::mem; use std::path::{Path, PathBuf}; use alioth::board::{BoardConfig, CpuConfig}; @@ -22,35 +23,21 @@ use alioth::device::fw_cfg::FwCfgItemParam; use alioth::errors::{DebugTrace, trace_error}; #[cfg(target_os = "macos")] use alioth::hv::Hvf; -use alioth::hv::{self, Coco}; #[cfg(target_os = "linux")] -use alioth::hv::{Kvm, KvmConfig}; +use alioth::hv::Kvm; +use alioth::hv::{Coco, HvConfig}; use alioth::loader::{Executable, Payload}; use alioth::mem::{MemBackend, MemConfig}; #[cfg(target_os = "linux")] use alioth::vfio::{CdevParam, ContainerParam, GroupParam, IoasParam}; -#[cfg(target_os = "linux")] -use alioth::virtio::DeviceId; use alioth::virtio::dev::balloon::BalloonParam; use alioth::virtio::dev::blk::BlkFileParam; use alioth::virtio::dev::entropy::EntropyParam; -use alioth::virtio::dev::fs::shared_dir::SharedDirParam; -#[cfg(target_os = "linux")] -use alioth::virtio::dev::fs::vu::VuFsParam; -#[cfg(target_os = "linux")] -use alioth::virtio::dev::net::tap::NetTapParam; -#[cfg(target_os = "macos")] -use alioth::virtio::dev::net::vmnet::NetVmnetParam; -use alioth::virtio::dev::vsock::UdsVsockParam; -#[cfg(target_os = "linux")] -use alioth::virtio::dev::vsock::VhostVsockParam; -#[cfg(target_os = "linux")] -use alioth::virtio::vu::frontend::VuFrontendParam; use alioth::virtio::worker::WorkerApi; use alioth::vm::Machine; +use alioth::vm::config::{BlkParam, Config, FsParam, NetParam, VsockParam}; use clap::Args; -use serde::Deserialize; -use serde_aco::{Help, help_text}; +use serde_aco::help_text; use snafu::{ResultExt, Snafu}; use crate::objects::{DOC_OBJECTS, parse_objects}; @@ -71,95 +58,17 @@ pub enum Error { Hypervisor { source: alioth::hv::Error }, #[snafu(display("Failed to create a VM"))] CreateVm { source: alioth::vm::Error }, - #[snafu(display("Failed to create a device"))] - CreateDevice { source: alioth::vm::Error }, #[snafu(display("Failed to boot a VM"))] BootVm { source: alioth::vm::Error }, #[snafu(display("VM did not shutdown peacefully"))] WaitVm { source: alioth::vm::Error }, } -#[derive(Debug, Deserialize, Clone, Help)] -#[cfg_attr(target_os = "macos", derive(Default))] -enum Hypervisor { - /// KVM backed by the Linux kernel. - #[cfg(target_os = "linux")] - #[serde(alias = "kvm")] - Kvm(KvmConfig), - /// macOS Hypervisor Framework. - #[cfg(target_os = "macos")] - #[serde(alias = "hvf")] - #[default] - Hvf, -} - -#[cfg(target_os = "linux")] -impl Default for Hypervisor { - fn default() -> Self { - Hypervisor::Kvm(KvmConfig::default()) - } -} - -#[derive(Debug, Deserialize, Clone, Help)] -enum FsParam { - /// VirtIO FS device backed by a shared directory. - #[serde(alias = "dir")] - Dir(SharedDirParam), - #[cfg(target_os = "linux")] - /// VirtIO FS device backed by a vhost-user process, e.g. virtiofsd. - #[serde(alias = "vu")] - Vu(VuFsParam), -} - -#[derive(Debug, Deserialize, Clone, Help)] -enum VsockParam { - #[cfg(target_os = "linux")] - /// Vsock device backed by host kernel vhost-vsock module. - #[serde(alias = "vhost")] - Vhost(VhostVsockParam), - /// Vsock device mapped to a Unix domain socket. - #[serde(alias = "uds")] - Uds(UdsVsockParam), -} - -#[cfg(target_os = "linux")] -#[derive(Deserialize, Help)] -struct VuSocket { - socket: Box, -} - -#[derive(Deserialize, Help)] -enum NetParam { - /// VirtIO net device backed by TUN/TAP, MacVTap, or IPVTap. - #[cfg(target_os = "linux")] - #[serde(alias = "tap")] - Tap(NetTapParam), - /// VirtIO net device backed by vmnet framework. - #[cfg(target_os = "macos")] - #[serde(alias = "vmnet")] - Vmnet(NetVmnetParam), - /// vhost-user net device over a Unix domain socket. - #[cfg(target_os = "linux")] - #[serde(alias = "vu")] - Vu(VuSocket), -} - -#[derive(Deserialize, Help)] -enum BlkParam { - /// VirtIO block device backed a disk image file. - #[serde(alias = "file")] - File(BlkFileParam), - #[cfg(target_os = "linux")] - #[serde(alias = "vu")] - /// vhost-user block device over a Unix domain socket. - Vu(VuSocket), -} - #[derive(Args, Debug, Clone)] #[command(arg_required_else_help = true, alias("run"))] pub struct BootArgs { #[arg(long, help( - help_text::("Specify the Hypervisor to run on.") + help_text::("Specify the Hypervisor to run on.") ), value_name = "HV")] hypervisor: Option, @@ -207,10 +116,10 @@ pub struct BootArgs { pvpanic: bool, #[cfg(target_arch = "x86_64")] - #[arg(long = "fw-cfg", help( + #[arg(long, help( help_text::("Add an extra item to the fw_cfg device.") ), value_name = "ITEM")] - fw_cfgs: Vec, + fw_cfg: Vec, /// Add a VirtIO entropy device. #[arg(long)] @@ -269,114 +178,47 @@ pub struct BootArgs { objects: Vec, } -fn add_net( - vm: &Machine, - args: Vec, - objects: &HashMap<&str, &str>, -) -> Result<(), Error> -where - H: hv::Hypervisor + 'static, -{ - for (index, arg) in args.into_iter().enumerate() { - #[cfg(target_os = "linux")] - let param: NetParam = match serde_aco::from_args(&arg, objects) { - Ok(p) => p, - Err(_) => { - let tap_param = serde_aco::from_args::(&arg, objects) - .context(error::ParseArg { arg })?; - NetParam::Tap(tap_param) - } - }; - #[cfg(target_os = "macos")] - let param: NetParam = - serde_aco::from_args(&arg, objects).context(error::ParseArg { arg })?; - match param { - #[cfg(target_os = "linux")] - NetParam::Tap(tap_param) => vm.add_virtio_dev(format!("virtio-net-{index}"), tap_param), - #[cfg(target_os = "linux")] - NetParam::Vu(sock) => { - let param = VuFrontendParam { - id: DeviceId::Net, - socket: sock.socket, - }; - vm.add_virtio_dev(format!("vu-net-{index}"), param) - } - #[cfg(target_os = "macos")] - NetParam::Vmnet(p) => vm.add_virtio_dev(format!("virtio-net-{index}"), p), - } - .context(error::CreateDevice)?; +fn parse_net_arg(arg: &str, objects: &HashMap<&str, &str>) -> serde_aco::Result { + #[cfg(target_os = "linux")] + if let Ok(param) = serde_aco::from_args(arg, objects) { + Ok(param) + } else { + let param = serde_aco::from_args(arg, objects)?; + Ok(NetParam::Tap(param)) } - Ok(()) + + #[cfg(target_os = "macos")] + serde_aco::from_args(arg, objects) } -fn add_blk( - vm: &Machine, - args: Vec, - objects: &HashMap<&str, &str>, -) -> Result<(), Error> -where - H: hv::Hypervisor + 'static, -{ - for (index, opt) in args.into_iter().enumerate() { - let param: BlkParam = match serde_aco::from_args(&opt, objects) { - Ok(param) => param, - Err(_) => match serde_aco::from_args(&opt, objects) { - Ok(param) => BlkParam::File(param), - Err(_) => { - eprintln!("Please update the cmd line to --blk file,path={opt}"); - BlkParam::File(BlkFileParam { - path: PathBuf::from(opt).into(), - readonly: false, - api: WorkerApi::Mio, - }) - } - }, - }; - match param { - BlkParam::File(p) => vm.add_virtio_dev(format!("virtio-blk-{index}"), p), - #[cfg(target_os = "linux")] - BlkParam::Vu(s) => { - let p = VuFrontendParam { - id: DeviceId::Block, - socket: s.socket, - }; - vm.add_virtio_dev(format!("vu-net-{index}"), p) - } - } - .context(error::CreateDevice)?; +fn parse_blk_arg(arg: &str, objects: &HashMap<&str, &str>) -> BlkParam { + if let Ok(param) = serde_aco::from_args(arg, objects) { + param + } else if let Ok(param) = serde_aco::from_args(arg, objects) { + BlkParam::File(param) + } else { + eprintln!("Please update the cmd line to --blk file,path={arg}"); + BlkParam::File(BlkFileParam { + path: PathBuf::from(arg).into(), + readonly: false, + api: WorkerApi::Mio, + }) } - Ok(()) } -pub fn boot(args: BootArgs) -> Result<(), Error> { - let objects = parse_objects(&args.objects)?; - let hv_config = if let Some(hv_cfg_opt) = args.hypervisor { - serde_aco::from_args(&hv_cfg_opt, &objects).context(error::ParseArg { arg: hv_cfg_opt })? - } else { - Hypervisor::default() - }; - let hypervisor = match hv_config { - #[cfg(target_os = "linux")] - Hypervisor::Kvm(kvm_config) => Kvm::new(kvm_config).context(error::Hypervisor)?, - #[cfg(target_os = "macos")] - Hypervisor::Hvf => Hvf {}, - }; - let coco = match args.coco { - None => None, - Some(c) => Some(serde_aco::from_args(&c, &objects).context(error::ParseArg { arg: c })?), - }; - let mem_config = if let Some(s) = args.memory { - serde_aco::from_args(&s, &objects).context(error::ParseArg { arg: s })? +fn parse_mem_arg( + arg: Option, + mem_size: String, + objects: &HashMap<&str, &str>, +) -> Result { + let config = if let Some(arg) = arg { + serde_aco::from_args(&arg, objects).context(error::ParseArg { arg })? } else { #[cfg(target_os = "linux")] - eprintln!( - "Please update the cmd line to --memory size={},backend=memfd", - args.mem_size - ); - let size = serde_aco::from_args(&args.mem_size, &objects) - .context(error::ParseArg { arg: args.mem_size })?; + eprintln!("Please update the cmd line to --memory size={mem_size},backend=memfd"); MemConfig { - size, + size: serde_aco::from_args(&mem_size, objects) + .context(error::ParseArg { arg: mem_size })?, #[cfg(target_os = "linux")] backend: MemBackend::Memfd, #[cfg(not(target_os = "linux"))] @@ -384,119 +226,137 @@ pub fn boot(args: BootArgs) -> Result<(), Error> { ..Default::default() } }; - let cpu_config = if let Some(s) = args.cpu { - serde_aco::from_args(&s, &objects).context(error::ParseArg { arg: s })? + Ok(config) +} + +fn parse_cpu_arg( + arg: Option>, + num_cpu: u16, + objects: &HashMap<&str, &str>, +) -> Result { + let config = if let Some(arg) = arg { + serde_aco::from_args(&arg, objects).context(error::ParseArg { arg })? } else { - eprintln!("Please update the cmd line to --cpu count={}", args.num_cpu); + eprintln!("Please update the cmd line to --cpu count={num_cpu}"); CpuConfig { - count: args.num_cpu, + count: num_cpu, ..Default::default() } }; - let board_config = BoardConfig { - mem: mem_config, - cpu: cpu_config, - coco, + Ok(config) +} + +fn parse_payload_arg(args: &mut BootArgs) -> Payload { + let mut payload = Payload { + firmware: args.firmware.take(), + initramfs: args.initramfs.take(), + cmdline: args.cmdline.take(), + ..Default::default() }; - let vm = Machine::new(hypervisor, board_config).context(error::CreateVm)?; + payload.executable = args.kernel.take().map(Executable::Linux); #[cfg(target_arch = "x86_64")] - vm.add_com1().context(error::CreateDevice)?; - #[cfg(target_arch = "aarch64")] - vm.add_pl011().context(error::CreateDevice)?; - #[cfg(target_arch = "aarch64")] - vm.add_pl031(); - - if args.pvpanic { - vm.add_pvpanic().context(error::CreateDevice)?; + if payload.executable.is_none() { + payload.executable = args.pvh.take().map(Executable::Pvh); } + payload +} - #[cfg(target_arch = "x86_64")] - if args.firmware.is_some() || !args.fw_cfgs.is_empty() { - let params = args - .fw_cfgs - .into_iter() - .map(|s| serde_aco::from_args(&s, &objects).context(error::ParseArg { arg: s })) - .collect::, _>>()?; - vm.add_fw_cfg(params.into_iter()) - .context(error::CreateDevice)?; +fn parse_args(mut args: BootArgs, objects: HashMap<&str, &str>) -> Result { + let payload = parse_payload_arg(&mut args); + + let mut board_config = BoardConfig::default(); + if let Some(arg) = args.coco { + let param = serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })?; + board_config.coco = Some(param); + }; + board_config.mem = parse_mem_arg(args.memory, args.mem_size, &objects)?; + board_config.cpu = parse_cpu_arg(args.cpu, args.num_cpu, &objects)?; + + let mut config = Config { + board: board_config, + pvpanic: args.pvpanic, + payload, + ..Default::default() }; + #[cfg(target_arch = "x86_64")] + for arg in args.fw_cfg { + let param = serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })?; + config.fw_cfg.push(param); + } + if args.entropy { - vm.add_virtio_dev("virtio-entropy", EntropyParam::default()) - .context(error::CreateDevice)?; + config.entropy = Some(EntropyParam::default()); } - add_net(&vm, args.net, &objects)?; - add_blk(&vm, args.blk, &objects)?; - for (index, fs) in args.fs.into_iter().enumerate() { - let param: FsParam = - serde_aco::from_args(&fs, &objects).context(error::ParseArg { arg: fs })?; - match param { - FsParam::Dir(p) => vm.add_virtio_dev(format!("virtio-fs-{index}"), p), - #[cfg(target_os = "linux")] - FsParam::Vu(p) => vm.add_virtio_dev(format!("vu-fs-{index}"), p), - } - .context(error::CreateDevice)?; + + for arg in args.net { + let param = parse_net_arg(&arg, &objects).context(error::ParseArg { arg })?; + config.net.push(param); } - if let Some(vsock) = args.vsock { - let param = - serde_aco::from_args(&vsock, &objects).context(error::ParseArg { arg: vsock })?; - match param { - #[cfg(target_os = "linux")] - VsockParam::Vhost(p) => vm - .add_virtio_dev("vhost-vsock", p) - .context(error::CreateDevice)?, - VsockParam::Uds(p) => vm - .add_virtio_dev("uds-vsock", p) - .context(error::CreateDevice)?, - }; + + for arg in args.blk { + let param = parse_blk_arg(&arg, &objects); + config.blk.push(param); } - if let Some(balloon) = args.balloon { - let param: BalloonParam = - serde_aco::from_args(&balloon, &objects).context(error::ParseArg { arg: balloon })?; - vm.add_virtio_dev("virtio-balloon", param) - .context(error::CreateDevice)?; + + for arg in args.fs { + let param = serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })?; + config.fs.push(param); + } + + if let Some(arg) = args.vsock { + let param = serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })?; + config.vsock = Some(param); + } + + if let Some(arg) = args.balloon { + let param = serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })?; + config.balloon = Some(param); } #[cfg(target_os = "linux")] - for ioas in args.vfio_ioas.into_iter() { - let param: IoasParam = - serde_aco::from_args(&ioas, &objects).context(error::ParseArg { arg: ioas })?; - vm.add_vfio_ioas(param).context(error::CreateDevice)?; + for arg in args.vfio_ioas { + let param = serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })?; + config.vfio_ioas.push(param); } #[cfg(target_os = "linux")] - for (index, vfio) in args.vfio_cdev.into_iter().enumerate() { - let param: CdevParam = - serde_aco::from_args(&vfio, &objects).context(error::ParseArg { arg: vfio })?; - vm.add_vfio_cdev(format!("vfio-{index}").into(), param) - .context(error::CreateDevice)?; + for arg in args.vfio_cdev { + let param = serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })?; + config.vfio_cdev.push(param); } - #[cfg(target_os = "linux")] - for container in args.vfio_container.into_iter() { - let param: ContainerParam = serde_aco::from_args(&container, &objects) - .context(error::ParseArg { arg: container })?; - vm.add_vfio_container(param).context(error::CreateDevice)?; + for arg in args.vfio_container { + let param = serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })?; + config.vfio_container.push(param); } #[cfg(target_os = "linux")] - for (index, group) in args.vfio_group.into_iter().enumerate() { - let param: GroupParam = - serde_aco::from_args(&group, &objects).context(error::ParseArg { arg: group })?; - vm.add_vfio_devs_in_group(&index.to_string(), param) - .context(error::CreateDevice)?; + for arg in args.vfio_group { + let param = serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })?; + config.vfio_group.push(param); } - let mut payload = Payload { - firmware: args.firmware, - initramfs: args.initramfs, - cmdline: args.cmdline, - ..Default::default() + Ok(config) +} + +pub fn boot(mut args: BootArgs) -> Result<(), Error> { + let object_args = mem::take(&mut args.objects); + let objects = parse_objects(&object_args)?; + + let hv_config = if let Some(arg) = args.hypervisor.take() { + serde_aco::from_args(&arg, &objects).context(error::ParseArg { arg })? + } else { + HvConfig::default() }; - payload.executable = args.kernel.map(Executable::Linux); - #[cfg(target_arch = "x86_64")] - if payload.executable.is_none() { - payload.executable = args.pvh.map(Executable::Pvh); - } - vm.add_payload(payload); + let hypervisor = match hv_config { + #[cfg(target_os = "linux")] + HvConfig::Kvm(kvm_config) => Kvm::new(kvm_config).context(error::Hypervisor)?, + #[cfg(target_os = "macos")] + HvConfig::Hvf => Hvf {}, + }; + + let config = parse_args(args, objects)?; + + let vm = Machine::new(hypervisor, config).context(error::CreateVm)?; vm.boot().context(error::BootVm)?; vm.wait().context(error::WaitVm)?; diff --git a/alioth/src/board/board.rs b/alioth/src/board/board.rs index bea1809b..dd9ba856 100644 --- a/alioth/src/board/board.rs +++ b/alioth/src/board/board.rs @@ -175,6 +175,7 @@ struct MpSync { pub const PCIE_MMIO_64_SIZE: u64 = 1 << 40; +#[derive(Debug, Deserialize, Default)] pub struct BoardConfig { pub mem: MemConfig, pub cpu: CpuConfig, diff --git a/alioth/src/device/device.rs b/alioth/src/device/device.rs index 3fed2fce..ecd309e2 100644 --- a/alioth/src/device/device.rs +++ b/alioth/src/device/device.rs @@ -16,6 +16,7 @@ pub mod console; #[cfg(target_arch = "x86_64")] #[path = "fw_cfg/fw_cfg.rs"] pub mod fw_cfg; +pub mod net; #[cfg(target_arch = "aarch64")] pub mod pl011; #[cfg(target_arch = "aarch64")] diff --git a/alioth/src/virtio/dev/net/mac_addr.rs b/alioth/src/device/net.rs similarity index 97% rename from alioth/src/virtio/dev/net/mac_addr.rs rename to alioth/src/device/net.rs index cfa0785d..e1999a8f 100644 --- a/alioth/src/virtio/dev/net/mac_addr.rs +++ b/alioth/src/device/net.rs @@ -1,4 +1,4 @@ -// Copyright 2024 Google LLC +// Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -92,5 +92,5 @@ impl<'de> Deserialize<'de> for MacAddr { } #[cfg(test)] -#[path = "mac_addr_test.rs"] +#[path = "net_test.rs"] mod tests; diff --git a/alioth/src/virtio/dev/net/mac_addr_test.rs b/alioth/src/device/net_test.rs similarity index 100% rename from alioth/src/virtio/dev/net/mac_addr_test.rs rename to alioth/src/device/net_test.rs diff --git a/alioth/src/hv/hv.rs b/alioth/src/hv/hv.rs index 695bebd1..d3e29ce0 100644 --- a/alioth/src/hv/hv.rs +++ b/alioth/src/hv/hv.rs @@ -107,6 +107,27 @@ pub enum Error { pub type Result = std::result::Result; +#[derive(Debug, Deserialize, Clone, Help)] +#[cfg_attr(target_os = "macos", derive(Default))] +pub enum HvConfig { + /// KVM backed by the Linux kernel. + #[cfg(target_os = "linux")] + #[serde(alias = "kvm")] + Kvm(KvmConfig), + /// macOS Hypervisor Framework. + #[cfg(target_os = "macos")] + #[default] + #[serde(alias = "hvf")] + Hvf, +} + +#[cfg(target_os = "linux")] +impl Default for HvConfig { + fn default() -> Self { + HvConfig::Kvm(KvmConfig::default()) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct MemMapOption { pub read: bool, diff --git a/alioth/src/lib.rs b/alioth/src/lib.rs index 7487999d..46a7fdb8 100644 --- a/alioth/src/lib.rs +++ b/alioth/src/lib.rs @@ -42,6 +42,7 @@ pub(crate) mod utils; pub mod vfio; #[path = "virtio/virtio.rs"] pub mod virtio; +#[path = "vm/vm.rs"] pub mod vm; #[cfg(test)] diff --git a/alioth/src/loader/loader.rs b/alioth/src/loader/loader.rs index dee19d52..1f7e4abe 100644 --- a/alioth/src/loader/loader.rs +++ b/alioth/src/loader/loader.rs @@ -25,6 +25,7 @@ use std::ffi::CString; use std::ops::Range; use std::path::Path; +use serde::Deserialize; use snafu::Snafu; #[cfg(target_arch = "x86_64")] @@ -33,7 +34,7 @@ use crate::arch::reg::{Reg, SReg}; use crate::errors::{DebugTrace, trace_error}; use crate::mem::{MemRegionEntry, MemRegionType}; -#[derive(Debug, Default)] +#[derive(Debug, Default, Deserialize)] pub struct Payload { pub firmware: Option>, pub executable: Option, @@ -41,7 +42,7 @@ pub struct Payload { pub cmdline: Option, } -#[derive(Debug)] +#[derive(Debug, Deserialize)] pub enum Executable { Linux(Box), #[cfg(target_arch = "x86_64")] diff --git a/alioth/src/virtio/dev/net/net.rs b/alioth/src/virtio/dev/net/net.rs index f65bddb3..a46fa423 100644 --- a/alioth/src/virtio/dev/net/net.rs +++ b/alioth/src/virtio/dev/net/net.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod mac_addr; #[cfg(target_os = "linux")] pub mod tap; #[cfg(target_os = "macos")] @@ -23,10 +22,9 @@ use std::fmt::Debug; use bitflags::bitflags; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; +use crate::device::net::MacAddr; use crate::{c_enum, impl_mmio_for_zerocopy}; -use self::mac_addr::MacAddr; - #[repr(C, align(8))] #[derive(Debug, Default, FromBytes, Immutable, IntoBytes)] pub struct NetConfig { diff --git a/alioth/src/virtio/dev/net/tap.rs b/alioth/src/virtio/dev/net/tap.rs index 902dc914..ffea7c48 100644 --- a/alioth/src/virtio/dev/net/tap.rs +++ b/alioth/src/virtio/dev/net/tap.rs @@ -35,11 +35,11 @@ use serde::Deserialize; use serde_aco::Help; use zerocopy::{FromBytes, IntoBytes}; +use crate::device::net::MacAddr; use crate::hv::IoeventFd; use crate::mem::mapped::RamBus; use crate::sync::notifier::Notifier; use crate::sys::if_tun::{TunFeature, tun_set_iff, tun_set_offload, tun_set_vnet_hdr_sz}; -use crate::virtio::dev::net::mac_addr::MacAddr; use crate::virtio::dev::net::{ CtrlAck, CtrlClass, CtrlHdr, CtrlMq, CtrlMqParisSet, NetConfig, NetFeature, VirtioNetHdr, }; diff --git a/alioth/src/virtio/dev/net/vmnet.rs b/alioth/src/virtio/dev/net/vmnet.rs index eecb9392..77fffd66 100644 --- a/alioth/src/virtio/dev/net/vmnet.rs +++ b/alioth/src/virtio/dev/net/vmnet.rs @@ -29,6 +29,7 @@ use serde::Deserialize; use serde_aco::Help; use zerocopy::IntoBytes; +use crate::device::net::MacAddr; use crate::hv::IoeventFd; use crate::mem::mapped::RamBus; use crate::sync::notifier::Notifier; @@ -45,7 +46,6 @@ use crate::sys::xpc::{ XpcObject, xpc_bool_create, xpc_dictionary_create, xpc_dictionary_get_string, xpc_dictionary_get_uint64, xpc_uint64_create, }; -use crate::virtio::dev::net::mac_addr::MacAddr; use crate::virtio::dev::net::{NetConfig, NetFeature, VirtioNetHdr}; use crate::virtio::dev::{DevParam, DeviceId, Result, Virtio, WakeEvent}; use crate::virtio::queue::{DescChain, QueueReg, Status, VirtQueue}; diff --git a/alioth/src/vm/config.rs b/alioth/src/vm/config.rs new file mode 100644 index 00000000..9e630675 --- /dev/null +++ b/alioth/src/vm/config.rs @@ -0,0 +1,118 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[cfg(target_os = "linux")] +use std::path::Path; + +use serde::Deserialize; +use serde_aco::Help; + +use crate::board::BoardConfig; +#[cfg(target_arch = "x86_64")] +use crate::device::fw_cfg::FwCfgItemParam; +use crate::loader::Payload; +use crate::virtio::dev::balloon::BalloonParam; +use crate::virtio::dev::blk::BlkFileParam; +use crate::virtio::dev::entropy::EntropyParam; +use crate::virtio::dev::fs::shared_dir::SharedDirParam; +#[cfg(target_os = "macos")] +use crate::virtio::dev::net::vmnet::NetVmnetParam; +use crate::virtio::dev::vsock::UdsVsockParam; +#[cfg(target_os = "linux")] +use crate::{ + vfio::{CdevParam, ContainerParam, GroupParam, IoasParam}, + virtio::dev::{fs::vu::VuFsParam, net::tap::NetTapParam, vsock::VhostVsockParam}, +}; + +#[cfg(target_os = "linux")] +#[derive(Debug, Deserialize, Help)] +pub struct VuSocket { + pub socket: Box, +} + +#[derive(Debug, Deserialize, Help)] +pub enum NetParam { + /// VirtIO net device backed by TUN/TAP, MacVTap, or IPVTap. + #[cfg(target_os = "linux")] + #[serde(alias = "tap")] + Tap(NetTapParam), + /// VirtIO net device backed by vmnet framework. + #[cfg(target_os = "macos")] + #[serde(alias = "vmnet")] + Vmnet(NetVmnetParam), + /// vhost-user net device over a Unix domain socket. + #[cfg(target_os = "linux")] + #[serde(alias = "vu")] + Vu(VuSocket), +} + +#[derive(Debug, Deserialize, Help)] +pub enum BlkParam { + /// VirtIO block device backed a disk image file. + #[serde(alias = "file")] + File(BlkFileParam), + #[cfg(target_os = "linux")] + #[serde(alias = "vu")] + /// vhost-user block device over a Unix domain socket. + Vu(VuSocket), +} + +#[derive(Debug, Deserialize, Clone, Help)] +pub enum FsParam { + /// VirtIO FS device backed by a shared directory. + #[serde(alias = "dir")] + Dir(SharedDirParam), + #[cfg(target_os = "linux")] + /// VirtIO FS device backed by a vhost-user process, e.g. virtiofsd. + #[serde(alias = "vu")] + Vu(VuFsParam), +} + +#[derive(Debug, Deserialize, Clone, Help)] +pub enum VsockParam { + #[cfg(target_os = "linux")] + /// Vsock device backed by host kernel vhost-vsock module. + #[serde(alias = "vhost")] + Vhost(VhostVsockParam), + /// Vsock device mapped to a Unix domain socket. + #[serde(alias = "uds")] + Uds(UdsVsockParam), +} + +#[derive(Debug, Default, Deserialize)] +pub struct Config { + pub board: BoardConfig, + + pub payload: Payload, + + pub net: Vec, + pub blk: Vec, + pub fs: Vec, + pub vsock: Option, + pub entropy: Option, + pub balloon: Option, + pub pvpanic: bool, + + #[cfg(target_arch = "x86_64")] + pub fw_cfg: Vec, + + #[cfg(target_os = "linux")] + pub vfio_cdev: Vec, + #[cfg(target_os = "linux")] + pub vfio_ioas: Vec, + #[cfg(target_os = "linux")] + pub vfio_group: Vec, + #[cfg(target_os = "linux")] + pub vfio_container: Vec, +} diff --git a/alioth/src/vm.rs b/alioth/src/vm/vm.rs similarity index 78% rename from alioth/src/vm.rs rename to alioth/src/vm/vm.rs index af374795..36d618c4 100644 --- a/alioth/src/vm.rs +++ b/alioth/src/vm/vm.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod config; + #[cfg(target_os = "linux")] use std::path::Path; use std::sync::Arc; @@ -27,7 +29,7 @@ use snafu::{ResultExt, Snafu}; use crate::arch::layout::{PL011_START, PL031_START}; #[cfg(target_arch = "x86_64")] use crate::arch::layout::{PORT_COM1, PORT_FW_CFG_SELECTOR}; -use crate::board::{ArchBoard, Board, BoardConfig}; +use crate::board::{ArchBoard, Board}; #[cfg(target_arch = "x86_64")] use crate::device::fw_cfg::{FwCfg, FwCfgItemParam}; #[cfg(target_arch = "aarch64")] @@ -38,8 +40,6 @@ use crate::device::pvpanic::PvPanic; #[cfg(target_arch = "x86_64")] use crate::device::serial::Serial; use crate::errors::{DebugTrace, trace_error}; -#[cfg(target_os = "linux")] -use crate::hv::Kvm; use crate::hv::{Hypervisor, IoeventFdRegistry, Vm, VmConfig}; use crate::loader::Payload; use crate::mem::Memory; @@ -62,8 +62,14 @@ use crate::vfio::iommu::{Ioas, Iommu}; use crate::vfio::pci::VfioPciDev; #[cfg(target_os = "linux")] use crate::vfio::{CdevParam, ContainerParam, GroupParam, IoasParam}; +#[cfg(target_os = "linux")] +use crate::virtio::DeviceId; use crate::virtio::dev::{DevParam, Virtio, VirtioDevice}; use crate::virtio::pci::VirtioPciDevice; +#[cfg(target_os = "linux")] +use crate::virtio::vu::frontend::VuFrontendParam; + +use self::config::{BlkParam, Config, FsParam, NetParam, VsockParam}; #[trace_error] #[derive(Snafu, DebugTrace)] @@ -123,18 +129,19 @@ impl Machine where H: Hypervisor + 'static, { - pub fn new(hv: H, mut config: BoardConfig) -> Result { - config.config_fixup()?; + pub fn new(hv: H, config: Config) -> Result { + let mut board_config = config.board; + board_config.config_fixup()?; let vm_config = VmConfig { - coco: config.coco.clone(), + coco: board_config.coco.clone(), }; let mut vm = hv.create_vm(&vm_config)?; let vm_memory = vm.create_vm_memory()?; let memory = Memory::new(vm_memory); - let arch = ArchBoard::new(&hv, &vm, &config)?; + let arch = ArchBoard::new(&hv, &vm, &board_config)?; - let board = Arc::new(Board::new(vm, memory, arch, config)); + let board = Arc::new(Board::new(vm, memory, arch, board_config)); let (event_tx, event_rx) = mpsc::channel(); @@ -156,7 +163,7 @@ where board.arch_init()?; - let machine = Machine { + let vm = Machine { board, event_rx, _event_tx: event_tx, @@ -164,7 +171,100 @@ where iommu: Mutex::new(None), }; - Ok(machine) + #[cfg(target_arch = "x86_64")] + vm.add_com1()?; + #[cfg(target_arch = "aarch64")] + vm.add_pl011()?; + #[cfg(target_arch = "aarch64")] + vm.add_pl031(); + + if config.pvpanic { + vm.add_pvpanic()?; + } + + #[cfg(target_arch = "x86_64")] + if config.payload.firmware.is_some() || !config.fw_cfg.is_empty() { + vm.add_fw_cfg(config.fw_cfg.into_iter())?; + }; + + if let Some(param) = config.entropy { + vm.add_virtio_dev("virtio-entropy", param)?; + } + + for (index, param) in config.net.into_iter().enumerate() { + match param { + #[cfg(target_os = "linux")] + NetParam::Tap(tap_param) => { + vm.add_virtio_dev(format!("virtio-net-{index}"), tap_param) + } + #[cfg(target_os = "linux")] + NetParam::Vu(sock) => { + let param = VuFrontendParam { + id: DeviceId::Net, + socket: sock.socket, + }; + vm.add_virtio_dev(format!("vu-net-{index}"), param) + } + #[cfg(target_os = "macos")] + NetParam::Vmnet(p) => vm.add_virtio_dev(format!("virtio-net-{index}"), p), + }?; + } + + for (index, param) in config.blk.into_iter().enumerate() { + match param { + BlkParam::File(p) => vm.add_virtio_dev(format!("virtio-blk-{index}"), p), + #[cfg(target_os = "linux")] + BlkParam::Vu(s) => { + let p = VuFrontendParam { + id: DeviceId::Block, + socket: s.socket, + }; + vm.add_virtio_dev(format!("vu-net-{index}"), p) + } + }?; + } + + for (index, param) in config.fs.into_iter().enumerate() { + match param { + FsParam::Dir(p) => vm.add_virtio_dev(format!("virtio-fs-{index}"), p), + #[cfg(target_os = "linux")] + FsParam::Vu(p) => vm.add_virtio_dev(format!("vu-fs-{index}"), p), + }?; + } + + if let Some(param) = config.vsock { + match param { + #[cfg(target_os = "linux")] + VsockParam::Vhost(p) => vm.add_virtio_dev("vhost-vsock", p), + VsockParam::Uds(p) => vm.add_virtio_dev("uds-vsock", p), + }?; + } + + if let Some(param) = config.balloon { + vm.add_virtio_dev("virtio-balloon", param)?; + } + + #[cfg(target_os = "linux")] + for param in config.vfio_ioas.into_iter() { + vm.add_vfio_ioas(param)?; + } + #[cfg(target_os = "linux")] + for (index, param) in config.vfio_cdev.into_iter().enumerate() { + vm.add_vfio_cdev(format!("vfio-{index}").into(), param)?; + } + + #[cfg(target_os = "linux")] + for param in config.vfio_container.into_iter() { + vm.add_vfio_container(param)?; + } + #[cfg(target_os = "linux")] + for (index, param) in config.vfio_group.into_iter().enumerate() { + vm.add_vfio_devs_in_group(&index.to_string(), param)?; + } + + vm.add_payload(config.payload); + + Ok(vm) } #[cfg(target_arch = "x86_64")] @@ -309,7 +409,10 @@ where } #[cfg(target_os = "linux")] -impl Machine { +impl Machine +where + H: Hypervisor + 'static, +{ const DEFAULT_NAME: &str = "default"; pub fn add_vfio_ioas(&self, param: IoasParam) -> Result, Error> { From 2d22148ca3ce09ea9961ce42f014b3786e24ee1a Mon Sep 17 00:00:00 2001 From: Changyuan Lyu Date: Wed, 24 Dec 2025 16:59:16 -0800 Subject: [PATCH 2/2] test(cli): add tests for argument parsing Signed-off-by: Changyuan Lyu --- Cargo.lock | 26 ++ Cargo.toml | 1 + alioth-cli/Cargo.toml | 6 + alioth-cli/src/boot.rs | 6 +- alioth-cli/src/boot_test.rs | 366 +++++++++++++++++++++ alioth-cli/src/main.rs | 6 + alioth/src/arch/x86_64/sev.rs | 4 +- alioth/src/board/board.rs | 6 +- alioth/src/device/fw_cfg/fw_cfg.rs | 4 +- alioth/src/device/net.rs | 2 +- alioth/src/hv/hv.rs | 2 +- alioth/src/loader/loader.rs | 4 +- alioth/src/mem/mem.rs | 4 +- alioth/src/vfio/vfio.rs | 8 +- alioth/src/virtio/dev/balloon.rs | 2 +- alioth/src/virtio/dev/blk.rs | 2 +- alioth/src/virtio/dev/entropy.rs | 2 +- alioth/src/virtio/dev/fs/shared_dir.rs | 2 +- alioth/src/virtio/dev/fs/vu.rs | 2 +- alioth/src/virtio/dev/net/tap.rs | 10 +- alioth/src/virtio/dev/net/vmnet.rs | 2 +- alioth/src/virtio/dev/vsock/uds_vsock.rs | 2 +- alioth/src/virtio/dev/vsock/vhost_vsock.rs | 2 +- alioth/src/virtio/worker/worker.rs | 2 +- alioth/src/vm/config.rs | 12 +- 25 files changed, 447 insertions(+), 38 deletions(-) create mode 100644 alioth-cli/src/boot_test.rs diff --git a/Cargo.lock b/Cargo.lock index eae9328e..0e8d63a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,9 +39,13 @@ name = "alioth-cli" version = "0.10.0" dependencies = [ "alioth", + "assert_matches", "clap", + "ctor", "flexi_logger", "log", + "pretty_assertions", + "rstest", "serde", "serde-aco", "snafu", @@ -253,6 +257,12 @@ version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "dtor" version = "0.1.1" @@ -553,6 +563,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -1032,6 +1052,12 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.8.31" diff --git a/Cargo.toml b/Cargo.toml index ebfb63f1..4d8d6bb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ syn = { version = "2", features = ["full"] } quote = { version = "1" } rstest = "0.26" tempfile = "3" +pretty_assertions = "1" [profile.release] lto = true diff --git a/alioth-cli/Cargo.toml b/alioth-cli/Cargo.toml index 23d95d49..10d80a09 100644 --- a/alioth-cli/Cargo.toml +++ b/alioth-cli/Cargo.toml @@ -20,3 +20,9 @@ serde-aco.workspace = true [[bin]] path = "src/main.rs" name = "alioth" + +[dev-dependencies] +assert_matches.workspace = true +ctor.workspace = true +rstest.workspace = true +pretty_assertions.workspace = true diff --git a/alioth-cli/src/boot.rs b/alioth-cli/src/boot.rs index 2306eef5..b6e143ae 100644 --- a/alioth-cli/src/boot.rs +++ b/alioth-cli/src/boot.rs @@ -64,7 +64,7 @@ pub enum Error { WaitVm { source: alioth::vm::Error }, } -#[derive(Args, Debug, Clone)] +#[derive(Args, Debug, Clone, Default)] #[command(arg_required_else_help = true, alias("run"))] pub struct BootArgs { #[arg(long, help( @@ -362,3 +362,7 @@ pub fn boot(mut args: BootArgs) -> Result<(), Error> { vm.wait().context(error::WaitVm)?; Ok(()) } + +#[cfg(test)] +#[path = "boot_test.rs"] +mod tests; diff --git a/alioth-cli/src/boot_test.rs b/alioth-cli/src/boot_test.rs new file mode 100644 index 00000000..f73fb8a4 --- /dev/null +++ b/alioth-cli/src/boot_test.rs @@ -0,0 +1,366 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; +use std::path::Path; + +use alioth::board::{BoardConfig, CpuConfig, CpuTopology}; +#[cfg(target_arch = "x86_64")] +use alioth::device::fw_cfg::{FwCfgContentParam, FwCfgItemParam}; +use alioth::device::net::MacAddr; +use alioth::loader::{Executable, Payload}; +use alioth::mem::{MemBackend, MemConfig}; +#[cfg(target_os = "linux")] +use alioth::vfio::{CdevParam, ContainerParam, GroupParam, IoasParam}; +use alioth::virtio::dev::balloon::BalloonParam; +use alioth::virtio::dev::blk::BlkFileParam; +use alioth::virtio::dev::entropy::EntropyParam; +use alioth::virtio::dev::fs::shared_dir::SharedDirParam; +#[cfg(target_os = "linux")] +use alioth::virtio::dev::fs::vu::VuFsParam; +#[cfg(target_os = "linux")] +use alioth::virtio::dev::net::tap::NetTapParam; +#[cfg(target_os = "macos")] +use alioth::virtio::dev::net::vmnet::NetVmnetParam; +use alioth::virtio::dev::vsock::UdsVsockParam; +use alioth::virtio::worker::WorkerApi; +use alioth::vm::config::{BlkParam, Config, FsParam, NetParam, VsockParam}; +use pretty_assertions::assert_eq; +use rstest::rstest; + +use crate::boot::{ + BootArgs, parse_args, parse_blk_arg, parse_cpu_arg, parse_mem_arg, parse_net_arg, + parse_payload_arg, +}; + +#[test] +fn test_parse_args() { + let args = BootArgs { + firmware: Some(Path::new("stage0.bin").into()), + kernel: Some(Path::new("vmlinuz").into()), + cmdline: Some(c"console=ttyS0".into()), + initramfs: Some(Path::new("initramfs.cpio").into()), + cpu: Some("count=16,topology=id_topo".into()), + memory: Some("size=128G,backend=anon,shared=true".into()), + pvpanic: true, + #[cfg(target_arch = "x86_64")] + fw_cfg: vec![ + "name=item1,file=file1".into(), + "name=item2,string=string2".into(), + ], + entropy: true, + net: vec![ + #[cfg(target_os = "linux")] + "tap=/dev/tap86,mac=02:32:10:d0:00:01,mtu=1500".into(), + #[cfg(target_os = "linux")] + "if=tap1,mac=ea:c2:14:80:10:01,mtu=1500,queue_pairs=2,api=mio".into(), + #[cfg(target_os = "macos")] + "vmnet,mac=a0:d0:ea:8a:d3:37".into(), + ], + blk: vec![ + "file,path=ubuntu-25.04-server-cloudimg.raw".into(), + "file,path=cloudinit.img,readonly=true".into(), + ], + coco: None, + fs: vec![ + "dir,tag=home,path=/home,dax_window=1g".into(), + #[cfg(target_os = "linux")] + "vu,socket=fs.vsock,tag=vufs".into(), + ], + vsock: Some("uds,cid=3,path=vsock_3.sock".into()), + balloon: Some("free_page_reporting=true".into()), + #[cfg(target_os = "linux")] + vfio_cdev: vec!["path=/dev/vfio/devices/vfio0,ioas=default".into()], + #[cfg(target_os = "linux")] + vfio_ioas: vec!["name=default,dev_iommu=/dev/iommu".into()], + #[cfg(target_os = "linux")] + vfio_group: vec!["path=/dev/vfio/26,container=gpu_container,devices=id_gpus".into()], + #[cfg(target_os = "linux")] + vfio_container: vec!["name=gpu_container,dev_vfio=/dev/vfio/vfio".into()], + ..Default::default() + }; + let objects = HashMap::from([ + ("id_topo", "smt=true,sockets=1,cores=8"), + #[cfg(target_os = "linux")] + ("id_gpus", "0000:06:0d.0,0000:06:0d.1"), + ]); + let config = parse_args(args, objects).unwrap(); + let want = Config { + board: BoardConfig { + cpu: CpuConfig { + count: 16, + topology: CpuTopology { + smt: true, + cores: 8, + sockets: 1, + }, + }, + mem: MemConfig { + size: 128 << 30, + backend: MemBackend::Anonymous, + shared: true, + #[cfg(target_os = "linux")] + transparent_hugepage: false, + }, + coco: None, + }, + payload: Payload { + executable: Some(Executable::Linux(Path::new("vmlinuz").into())), + initramfs: Some(Path::new("initramfs.cpio").into()), + cmdline: Some(c"console=ttyS0".to_owned()), + firmware: Some(Path::new("stage0.bin").into()), + }, + net: vec![ + #[cfg(target_os = "linux")] + NetParam::Tap(NetTapParam { + mac: MacAddr([0x02, 0x32, 0x10, 0xd0, 0x00, 0x01]), + mtu: 1500, + tap: Some(Path::new("/dev/tap86").into()), + ..Default::default() + }), + #[cfg(target_os = "linux")] + NetParam::Tap(NetTapParam { + mac: MacAddr([0xea, 0xc2, 0x14, 0x80, 0x10, 0x01]), + mtu: 1500, + if_name: Some("tap1".into()), + queue_pairs: 2, + api: WorkerApi::Mio, + ..Default::default() + }), + #[cfg(target_os = "macos")] + NetParam::Vmnet(NetVmnetParam { + mac: Some(MacAddr([0xa0, 0xd0, 0xea, 0x8a, 0xd3, 0x37])), + }), + ], + blk: vec![ + BlkParam::File(BlkFileParam { + path: Path::new("ubuntu-25.04-server-cloudimg.raw").into(), + readonly: false, + api: WorkerApi::Mio, + }), + BlkParam::File(BlkFileParam { + path: Path::new("cloudinit.img").into(), + readonly: true, + api: WorkerApi::Mio, + }), + ], + fs: vec![ + FsParam::Dir(SharedDirParam { + path: Path::new("/home").into(), + tag: "home".into(), + dax_window: 1 << 30, + }), + #[cfg(target_os = "linux")] + FsParam::Vu(VuFsParam { + socket: Path::new("fs.vsock").into(), + tag: Some("vufs".into()), + dax_window: 0, + }), + ], + vsock: Some(VsockParam::Uds(UdsVsockParam { + cid: 3, + path: Path::new("vsock_3.sock").into(), + })), + entropy: Some(EntropyParam::default()), + balloon: Some(BalloonParam { + free_page_reporting: true, + }), + pvpanic: true, + #[cfg(target_arch = "x86_64")] + fw_cfg: vec![ + FwCfgItemParam { + name: "item1".into(), + content: FwCfgContentParam::File(Path::new("file1").into()), + }, + FwCfgItemParam { + name: "item2".into(), + content: FwCfgContentParam::String("string2".into()), + }, + ], + #[cfg(target_os = "linux")] + vfio_cdev: vec![CdevParam { + path: Path::new("/dev/vfio/devices/vfio0").into(), + ioas: Some("default".into()), + }], + #[cfg(target_os = "linux")] + vfio_ioas: vec![IoasParam { + name: "default".into(), + dev_iommu: Some(Path::new("/dev/iommu").into()), + }], + #[cfg(target_os = "linux")] + vfio_container: vec![ContainerParam { + name: "gpu_container".into(), + dev_vfio: Some(Path::new("/dev/vfio/vfio").into()), + }], + #[cfg(target_os = "linux")] + vfio_group: vec![GroupParam { + path: Path::new("/dev/vfio/26").into(), + container: Some("gpu_container".into()), + devices: vec!["0000:06:0d.0".into(), "0000:06:0d.1".into()], + }], + }; + assert_eq!(config, want); +} + +#[rstest] +#[cfg_attr(target_os = "macos", case( + "vmnet", + NetParam::Vmnet(NetVmnetParam { mac: None }) +))] +#[cfg_attr(target_os = "linux", case( + "tap,tap=/dev/tap86,mac=02:32:10:d0:00:01,mtu=1500,api=iouring", + NetParam::Tap(NetTapParam { + mac: MacAddr([0x02, 0x32, 0x10, 0xd0, 0x00, 0x01]), + mtu: 1500, + tap: Some(Path::new("/dev/tap86").into()), + api: WorkerApi::IoUring, + ..Default::default() + }), +))] +fn test_parse_net_arg(#[case] arg: &str, #[case] want: NetParam) { + let objects = HashMap::new(); + assert_eq!(parse_net_arg(arg, &objects).unwrap(), want); +} + +#[rstest] +#[case( + Some("count=16,topology=id_topo".into()), + 0, + HashMap::from([("id_topo", "cores=8,sockets=2,smt=false")]), + CpuConfig { + count: 16, + topology: CpuTopology { smt: false, cores: 8, sockets: 2 }, + } +)] +#[case( + None, + 16, + HashMap::new(), + CpuConfig { + count: 16, + topology: CpuTopology::default(), + } +)] +fn test_parse_cpu_arg( + #[case] arg: Option>, + #[case] num_cpu: u16, + #[case] objects: HashMap<&str, &str>, + #[case] want: CpuConfig, +) { + assert_eq!(parse_cpu_arg(arg, num_cpu, &objects).unwrap(), want,); +} + +#[rstest] +#[case( + None, + "32G", + MemConfig { + size: 32 << 30, + #[cfg(target_os = "linux")] + backend: MemBackend::Memfd, + #[cfg(target_os = "macos")] + backend: MemBackend::Anonymous, + ..Default::default() + } +)] +#[cfg_attr(target_os = "linux", case( + Some("size=32g,backend=memfd,shared=true,thp=true".into()), + "", + MemConfig { + size: 32 << 30, + backend: MemBackend::Memfd, + shared: true, + transparent_hugepage: true + } +))] +fn test_parse_mem_arg( + #[case] arg: Option, + #[case] mem_size: &str, + #[case] want: MemConfig, +) { + let objects = HashMap::new(); + let config = parse_mem_arg(arg, mem_size.to_owned(), &objects).unwrap(); + assert_eq!(config, want); +} + +#[rstest] +#[case( + BootArgs { + kernel: Some(Path::new("vmlinuz").into()), + cmdline: Some(c"console=ttyS0".into()), + initramfs: Some(Path::new("initramfs.cpio").into()), + ..Default::default() + }, + Payload { + firmware: None, + initramfs: Some(Path::new("initramfs.cpio").into()), + executable: Some(Executable::Linux(Path::new("vmlinuz").into())), + cmdline: Some(c"console=ttyS0".into()), + } +)] +#[cfg_attr(target_arch = "x86_64", case( + BootArgs { + pvh: Some(Path::new("vmlinux.bin").into()), + cmdline: Some(c"console=ttyS0".into()), + initramfs: Some(Path::new("initramfs.cpio").into()), + ..Default::default() + }, + Payload { + firmware: None, + initramfs: Some(Path::new("initramfs.cpio").into()), + executable: Some(Executable::Pvh(Path::new("vmlinux.bin").into())), + cmdline: Some(c"console=ttyS0".into()), + } +))] +fn test_parse_payload_arg(#[case] mut args: BootArgs, #[case] want: Payload) { + assert_eq!(parse_payload_arg(&mut args), want); +} + +#[rstest] +#[case( + "file,path=ubuntu-25.04-server-cloudimg.raw", + BlkParam::File(BlkFileParam { + path: Path::new("ubuntu-25.04-server-cloudimg.raw").into(), + readonly: false, + api: WorkerApi::Mio + }) +)] +#[case( + "path=cloudinit.img,readonly=true", + BlkParam::File(BlkFileParam { + path: Path::new("cloudinit.img").into(), + readonly: true, + api: WorkerApi::Mio + }) +)] +#[case( + "ubuntu-25.04-server-cloudimg.raw", + BlkParam::File(BlkFileParam { + path: Path::new("ubuntu-25.04-server-cloudimg.raw").into(), + readonly: false, + api: WorkerApi::Mio + }) +)] +#[cfg_attr(target_os = "linux", case( + "file,path=ubuntu-25.04-server-cloudimg.raw,api=io_uring", + BlkParam::File(BlkFileParam { + path: Path::new("ubuntu-25.04-server-cloudimg.raw").into(), + readonly: false, + api: WorkerApi::IoUring + }) +))] +fn test_parse_blk_arg(#[case] arg: &str, #[case] want: BlkParam) { + let objects = HashMap::new(); + assert_eq!(parse_blk_arg(arg, &objects), want); +} diff --git a/alioth-cli/src/main.rs b/alioth-cli/src/main.rs index ff7bb1ac..86061aaf 100644 --- a/alioth-cli/src/main.rs +++ b/alioth-cli/src/main.rs @@ -82,3 +82,9 @@ fn main() -> Result<(), Box> { } Ok(()) } + +#[cfg(test)] +#[ctor::ctor] +fn global_setup() { + flexi_logger::init(); +} diff --git a/alioth/src/arch/x86_64/sev.rs b/alioth/src/arch/x86_64/sev.rs index 3cb4a86b..a40142d9 100644 --- a/alioth/src/arch/x86_64/sev.rs +++ b/alioth/src/arch/x86_64/sev.rs @@ -53,7 +53,7 @@ bitfield! { /// AMD SEV guest policy /// /// From Secure Encrypted Virtualization API Version 0.24, Revision 3.24, Ch.2, Table 2. - #[derive(Copy, Clone, Default, Serialize, Deserialize, Help)] + #[derive(Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Help)] pub struct SevPolicy(u32); impl Debug; pub no_debug, set_no_debug: 0; @@ -70,7 +70,7 @@ bitfield! { /// AMD SEV-SNP guest policy /// /// From SEV SNP Firmware ABI Specification, Revision 1.55, Sec.4.3, Table 9. - #[derive(Copy, Clone, Default, Serialize, Deserialize, Help)] + #[derive(Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Help)] pub struct SnpPolicy(u64); impl Debug; pub api_minor, set_api_minor: 7,0; diff --git a/alioth/src/board/board.rs b/alioth/src/board/board.rs index dd9ba856..9c9fb9d0 100644 --- a/alioth/src/board/board.rs +++ b/alioth/src/board/board.rs @@ -103,7 +103,7 @@ pub enum Error { type Result = std::result::Result; -#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Default, Help)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Help)] pub struct CpuTopology { #[serde(default)] /// Enable SMT (Hyperthreading). @@ -130,7 +130,7 @@ const fn default_cpu_count() -> u16 { 1 } -#[derive(Debug, Deserialize, Default, Help)] +#[derive(Debug, Default, PartialEq, Eq, Deserialize, Help)] pub struct CpuConfig { /// Number of VCPUs assigned to the guest. [default: 1] #[serde(default = "default_cpu_count")] @@ -175,7 +175,7 @@ struct MpSync { pub const PCIE_MMIO_64_SIZE: u64 = 1 << 40; -#[derive(Debug, Deserialize, Default)] +#[derive(Debug, Default, PartialEq, Eq, Deserialize)] pub struct BoardConfig { pub mem: MemConfig, pub cpu: CpuConfig, diff --git a/alioth/src/device/fw_cfg/fw_cfg.rs b/alioth/src/device/fw_cfg/fw_cfg.rs index 6e0359a1..e742b849 100644 --- a/alioth/src/device/fw_cfg/fw_cfg.rs +++ b/alioth/src/device/fw_cfg/fw_cfg.rs @@ -512,7 +512,7 @@ impl Mmio for Mutex { } } -#[derive(Deserialize, Debug, Help)] +#[derive(Debug, PartialEq, Eq, Deserialize, Help)] pub enum FwCfgContentParam { /// Path to a file with binary contents. #[serde(alias = "file")] @@ -522,7 +522,7 @@ pub enum FwCfgContentParam { String(String), } -#[derive(Debug, Help)] +#[derive(Debug, PartialEq, Eq, Help)] pub struct FwCfgItemParam { /// Selector key of an item. pub name: String, diff --git a/alioth/src/device/net.rs b/alioth/src/device/net.rs index e1999a8f..bf0f2a37 100644 --- a/alioth/src/device/net.rs +++ b/alioth/src/device/net.rs @@ -21,7 +21,7 @@ use zerocopy::{FromBytes, Immutable, IntoBytes}; #[derive(Debug, Clone, Default, FromBytes, Immutable, IntoBytes, PartialEq, Eq)] #[repr(transparent)] -pub struct MacAddr([u8; 6]); +pub struct MacAddr(pub [u8; 6]); #[derive(Debug)] pub enum Error { diff --git a/alioth/src/hv/hv.rs b/alioth/src/hv/hv.rs index d3e29ce0..de5720f8 100644 --- a/alioth/src/hv/hv.rs +++ b/alioth/src/hv/hv.rs @@ -280,7 +280,7 @@ pub trait Its: Debug + Send + Sync + 'static { fn init(&self) -> Result<()>; } -#[derive(Debug, Clone, Deserialize, Help)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Help)] pub enum Coco { /// Enable AMD SEV or SEV-ES. #[cfg(target_arch = "x86_64")] diff --git a/alioth/src/loader/loader.rs b/alioth/src/loader/loader.rs index 1f7e4abe..3d87c3bc 100644 --- a/alioth/src/loader/loader.rs +++ b/alioth/src/loader/loader.rs @@ -34,7 +34,7 @@ use crate::arch::reg::{Reg, SReg}; use crate::errors::{DebugTrace, trace_error}; use crate::mem::{MemRegionEntry, MemRegionType}; -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Default, PartialEq, Eq, Deserialize)] pub struct Payload { pub firmware: Option>, pub executable: Option, @@ -42,7 +42,7 @@ pub struct Payload { pub cmdline: Option, } -#[derive(Debug, Deserialize)] +#[derive(Debug, PartialEq, Eq, Deserialize)] pub enum Executable { Linux(Box), #[cfg(target_arch = "x86_64")] diff --git a/alioth/src/mem/mem.rs b/alioth/src/mem/mem.rs index e41ec0a2..19aa79cc 100644 --- a/alioth/src/mem/mem.rs +++ b/alioth/src/mem/mem.rs @@ -81,7 +81,7 @@ fn default_memory_size() -> u64 { 1 << 30 } -#[derive(Debug, Deserialize, Default, Help)] +#[derive(Debug, Default, PartialEq, Eq, Deserialize, Help)] pub struct MemConfig { /// Total guest memory size in bytes. [default: 1G] #[serde(default = "default_memory_size")] @@ -99,7 +99,7 @@ pub struct MemConfig { pub transparent_hugepage: bool, } -#[derive(Debug, Deserialize, Default, Help)] +#[derive(Debug, Default, PartialEq, Eq, Deserialize, Help)] pub enum MemBackend { /// Anonymous memory by MAP_ANONYMOUS. #[default] diff --git a/alioth/src/vfio/vfio.rs b/alioth/src/vfio/vfio.rs index 81794a91..3e6a9db0 100644 --- a/alioth/src/vfio/vfio.rs +++ b/alioth/src/vfio/vfio.rs @@ -51,7 +51,7 @@ pub enum Error { pub type Result = std::result::Result; -#[derive(Debug, Deserialize, Help)] +#[derive(Debug, PartialEq, Eq, Deserialize, Help)] pub struct CdevParam { /// Path to a VFIO cdev, e.g. /dev/vfio/devices/vfio0. pub path: Box, @@ -59,7 +59,7 @@ pub struct CdevParam { pub ioas: Option>, } -#[derive(Debug, Deserialize, Help)] +#[derive(Debug, PartialEq, Eq, Deserialize, Help)] pub struct IoasParam { /// Name of the IO Address space. pub name: Box, @@ -67,7 +67,7 @@ pub struct IoasParam { pub dev_iommu: Option>, } -#[derive(Debug, Deserialize, Help)] +#[derive(Debug, PartialEq, Eq, Deserialize, Help)] pub struct GroupParam { /// Path to a VFIO group file, e.g. /dev/vfio/12. pub path: Box, @@ -78,7 +78,7 @@ pub struct GroupParam { pub container: Option>, } -#[derive(Debug, Deserialize, Help)] +#[derive(Debug, PartialEq, Eq, Deserialize, Help)] pub struct ContainerParam { /// Name of the Container. pub name: Box, diff --git a/alioth/src/virtio/dev/balloon.rs b/alioth/src/virtio/dev/balloon.rs index 7778d6db..5647bc2e 100644 --- a/alioth/src/virtio/dev/balloon.rs +++ b/alioth/src/virtio/dev/balloon.rs @@ -316,7 +316,7 @@ impl VirtioMio for Balloon { } } -#[derive(Debug, Clone, Deserialize, Help, Default)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Help)] pub struct BalloonParam { /// Enable free page reporting. [default: false] #[serde(default)] diff --git a/alioth/src/virtio/dev/blk.rs b/alioth/src/virtio/dev/blk.rs index df6f3535..e42f1bdf 100644 --- a/alioth/src/virtio/dev/blk.rs +++ b/alioth/src/virtio/dev/blk.rs @@ -140,7 +140,7 @@ pub struct BlockConfig { } impl_mmio_for_zerocopy!(BlockConfig); -#[derive(Debug, Clone, Deserialize, Help)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Help)] pub struct BlkFileParam { /// Path to a raw-formatted disk image. pub path: Box, diff --git a/alioth/src/virtio/dev/entropy.rs b/alioth/src/virtio/dev/entropy.rs index 618d954f..0516f07f 100644 --- a/alioth/src/virtio/dev/entropy.rs +++ b/alioth/src/virtio/dev/entropy.rs @@ -167,7 +167,7 @@ impl VirtioMio for Entropy { fn reset(&mut self, _registry: &Registry) {} } -#[derive(Debug, Default, Deserialize, Clone, Help)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Help)] pub struct EntropyParam { /// Source of entropy [default: /dev/urandom] pub source: Option>, diff --git a/alioth/src/virtio/dev/fs/shared_dir.rs b/alioth/src/virtio/dev/fs/shared_dir.rs index 38b8b2f2..ba0ba4dc 100644 --- a/alioth/src/virtio/dev/fs/shared_dir.rs +++ b/alioth/src/virtio/dev/fs/shared_dir.rs @@ -24,7 +24,7 @@ use crate::virtio::Result; use crate::virtio::dev::DevParam; use crate::virtio::dev::fs::{Fs, FsConfig}; -#[derive(Debug, Clone, Deserialize, Help)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Help)] pub struct SharedDirParam { /// Mount tag seen by the guest. pub tag: String, diff --git a/alioth/src/virtio/dev/fs/vu.rs b/alioth/src/virtio/dev/fs/vu.rs index 8995f020..67d12665 100644 --- a/alioth/src/virtio/dev/fs/vu.rs +++ b/alioth/src/virtio/dev/fs/vu.rs @@ -102,7 +102,7 @@ impl VuFs { } } -#[derive(Debug, Clone, Deserialize, Help)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Help)] pub struct VuFsParam { /// Path to the vhost-user UNIX domain socket. pub socket: Box, diff --git a/alioth/src/virtio/dev/net/tap.rs b/alioth/src/virtio/dev/net/tap.rs index ffea7c48..5649c411 100644 --- a/alioth/src/virtio/dev/net/tap.rs +++ b/alioth/src/virtio/dev/net/tap.rs @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::cmp::max; use std::fmt::Debug; use std::fs::{File, OpenOptions}; use std::io::{ErrorKind, IoSlice}; use std::mem::MaybeUninit; -use std::num::NonZeroU16; use std::os::fd::{AsFd, AsRawFd}; use std::os::unix::prelude::OpenOptionsExt; use std::path::Path; @@ -64,15 +64,15 @@ pub struct Net { api: WorkerApi, } -#[derive(Debug, Deserialize, Clone, Help)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Help)] pub struct NetTapParam { /// MAC address of the virtual NIC, e.g. 06:3a:76:53:da:3d. pub mac: MacAddr, /// Maximum transmission unit. pub mtu: u16, /// Number of pairs of transmit/receive queues. [default: 1] - #[serde(alias = "qp")] - pub queue_pairs: Option, + #[serde(alias = "qp", default)] + pub queue_pairs: u16, /// Path to the character device file of a tap interface. /// /// Required for MacVTap and IPVTap, e.g. /dev/tapX. @@ -113,7 +113,7 @@ impl Net { param.tap.as_deref(), matches!(param.api, WorkerApi::IoUring), )?; - let max_queue_pairs = param.queue_pairs.map(From::from).unwrap_or(1); + let max_queue_pairs = max(param.queue_pairs, 1); setup_socket(&mut socket, param.if_name.as_deref(), max_queue_pairs > 1)?; let mut dev_feat = NetFeature::MAC | NetFeature::MTU diff --git a/alioth/src/virtio/dev/net/vmnet.rs b/alioth/src/virtio/dev/net/vmnet.rs index 77fffd66..389426ef 100644 --- a/alioth/src/virtio/dev/net/vmnet.rs +++ b/alioth/src/virtio/dev/net/vmnet.rs @@ -495,7 +495,7 @@ fn write_to_vmnet(interface: *mut VmnetInterface) -> impl FnMut(&mut DescChain) } } -#[derive(Debug, Deserialize, Clone, Help)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Help)] pub struct NetVmnetParam { /// MAC address of the virtual NIC, e.g. 06:3a:76:53:da:3d. pub mac: Option, diff --git a/alioth/src/virtio/dev/vsock/uds_vsock.rs b/alioth/src/virtio/dev/vsock/uds_vsock.rs index a5330d10..ed613e4f 100644 --- a/alioth/src/virtio/dev/vsock/uds_vsock.rs +++ b/alioth/src/virtio/dev/vsock/uds_vsock.rs @@ -48,7 +48,7 @@ use zerocopy::{FromBytes, IntoBytes}; const HEADER_SIZE: usize = size_of::(); const SOCKET_TYPE: VsockType = VsockType::STREAM; -#[derive(Debug, Clone, Deserialize, Help)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Help)] pub struct UdsVsockParam { /// Vsock context id. pub cid: u32, diff --git a/alioth/src/virtio/dev/vsock/vhost_vsock.rs b/alioth/src/virtio/dev/vsock/vhost_vsock.rs index 9538641b..dc37a357 100644 --- a/alioth/src/virtio/dev/vsock/vhost_vsock.rs +++ b/alioth/src/virtio/dev/vsock/vhost_vsock.rs @@ -39,7 +39,7 @@ use crate::virtio::vhost::{UpdateVsockMem, VhostDev, error}; use crate::virtio::worker::mio::{ActiveMio, Mio, VirtioMio}; use crate::virtio::{IrqSender, Result, VirtioFeature}; -#[derive(Debug, Clone, Deserialize, Help)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Help)] pub struct VhostVsockParam { /// Vsock context id. pub cid: u32, diff --git a/alioth/src/virtio/worker/worker.rs b/alioth/src/virtio/worker/worker.rs index 2e3c4317..1bd786a6 100644 --- a/alioth/src/virtio/worker/worker.rs +++ b/alioth/src/virtio/worker/worker.rs @@ -19,7 +19,7 @@ pub mod mio; use serde::Deserialize; use serde_aco::Help; -#[derive(Debug, Clone, Copy, Default, Deserialize, Help)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Help)] pub enum WorkerApi { /// I/O event queue backed by epoll/kqeueu. #[default] diff --git a/alioth/src/vm/config.rs b/alioth/src/vm/config.rs index 9e630675..34efac81 100644 --- a/alioth/src/vm/config.rs +++ b/alioth/src/vm/config.rs @@ -36,12 +36,12 @@ use crate::{ }; #[cfg(target_os = "linux")] -#[derive(Debug, Deserialize, Help)] +#[derive(Debug, PartialEq, Eq, Deserialize, Help)] pub struct VuSocket { pub socket: Box, } -#[derive(Debug, Deserialize, Help)] +#[derive(Debug, PartialEq, Eq, Deserialize, Help)] pub enum NetParam { /// VirtIO net device backed by TUN/TAP, MacVTap, or IPVTap. #[cfg(target_os = "linux")] @@ -57,7 +57,7 @@ pub enum NetParam { Vu(VuSocket), } -#[derive(Debug, Deserialize, Help)] +#[derive(Debug, PartialEq, Eq, Deserialize, Help)] pub enum BlkParam { /// VirtIO block device backed a disk image file. #[serde(alias = "file")] @@ -68,7 +68,7 @@ pub enum BlkParam { Vu(VuSocket), } -#[derive(Debug, Deserialize, Clone, Help)] +#[derive(Debug, PartialEq, Eq, Deserialize, Clone, Help)] pub enum FsParam { /// VirtIO FS device backed by a shared directory. #[serde(alias = "dir")] @@ -79,7 +79,7 @@ pub enum FsParam { Vu(VuFsParam), } -#[derive(Debug, Deserialize, Clone, Help)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Help)] pub enum VsockParam { #[cfg(target_os = "linux")] /// Vsock device backed by host kernel vhost-vsock module. @@ -90,7 +90,7 @@ pub enum VsockParam { Uds(UdsVsockParam), } -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Default, PartialEq, Eq, Deserialize)] pub struct Config { pub board: BoardConfig,