diff --git a/Cargo.lock b/Cargo.lock
index 32d2489cb..f71acbd32 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -179,6 +179,7 @@ dependencies = [
"libc",
"nix",
"notify",
+ "quick-xml",
"rand 0.9.1",
"regex",
"reqwest",
@@ -1629,6 +1630,15 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "quick-xml"
+version = "0.36.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "quote"
version = "1.0.40"
@@ -2693,7 +2703,7 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22"
dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
]
[[package]]
diff --git a/crates/kit/Cargo.toml b/crates/kit/Cargo.toml
index 67de2c7ee..e00338cfc 100644
--- a/crates/kit/Cargo.toml
+++ b/crates/kit/Cargo.toml
@@ -44,6 +44,7 @@ libc = "0.2"
camino = "1.1.12"
comfy-table = "7.1"
strum = { version = "0.26", features = ["derive"] }
+quick-xml = "0.36"
[dev-dependencies]
similar-asserts = "1.5"
diff --git a/crates/kit/src/arch.rs b/crates/kit/src/arch.rs
index 6119d612b..9d37bdf98 100644
--- a/crates/kit/src/arch.rs
+++ b/crates/kit/src/arch.rs
@@ -3,6 +3,7 @@
//! This module provides cross-architecture support for libvirt domain creation
//! and QEMU emulator selection, avoiding hardcoded architecture assumptions.
+use crate::xml_utils::XmlWriter;
use color_eyre::Result;
/// Architecture configuration for libvirt domains and QEMU
@@ -44,52 +45,33 @@ impl ArchConfig {
}
}
- /// Get architecture-specific XML features for libvirt
- pub fn xml_features(&self) -> &'static str {
- match self.arch {
- "x86_64" => {
- r#"
-
-
-
-
- "#
- }
- "aarch64" => {
- r#"
-
-
-
- "#
- }
- _ => {
- r#"
-
-
-
- "#
- }
+ /// Generate architecture-specific XML features for libvirt
+ pub fn write_features(&self, writer: &mut XmlWriter) -> Result<()> {
+ writer.start_element("features", &[])?;
+ writer.write_empty_element("acpi", &[])?;
+ writer.write_empty_element("apic", &[])?;
+
+ // Add x86_64-specific features
+ if self.arch == "x86_64" {
+ writer.write_empty_element("vmport", &[("state", "off")])?;
}
+
+ writer.end_element("features")?;
+ Ok(())
}
- /// Get architecture-specific timer configuration
- pub fn xml_timers(&self) -> &'static str {
- match self.arch {
- "x86_64" => {
- r#"
-
-
- "#
- }
- "aarch64" => {
- r#"
- "#
- }
- _ => {
- r#"
- "#
- }
+ /// Generate architecture-specific timer configuration
+ pub fn write_timers(&self, writer: &mut XmlWriter) -> Result<()> {
+ // RTC timer is common to all architectures
+ writer.write_empty_element("timer", &[("name", "rtc"), ("tickpolicy", "catchup")])?;
+
+ // Add x86_64-specific timers
+ if self.arch == "x86_64" {
+ writer.write_empty_element("timer", &[("name", "pit"), ("tickpolicy", "delay")])?;
+ writer.write_empty_element("timer", &[("name", "hpet"), ("present", "no")])?;
}
+
+ Ok(())
}
/// Check if this architecture supports VMport (x86_64 specific feature)
@@ -146,9 +128,20 @@ mod tests {
fn test_arch_specific_features() {
let arch_config = ArchConfig::detect().unwrap();
- // All architectures should have some features
- assert!(!arch_config.xml_features().is_empty());
- assert!(!arch_config.xml_timers().is_empty());
+ // Test that we can generate features XML without errors
+ let mut writer = XmlWriter::new();
+ arch_config.write_features(&mut writer).unwrap();
+ let features_xml = writer.into_string().unwrap();
+ assert!(features_xml.contains(""));
+ assert!(features_xml.contains(""));
+ assert!(features_xml.contains(""));
+
+ // Test that we can generate timers XML without errors
+ let mut writer = XmlWriter::new();
+ arch_config.write_timers(&mut writer).unwrap();
+ let timers_xml = writer.into_string().unwrap();
+ assert!(timers_xml.contains("timer"));
+ assert!(timers_xml.contains("rtc"));
// CPU mode should be valid
assert!(!arch_config.cpu_mode().is_empty());
@@ -177,4 +170,60 @@ mod tests {
// Should be mutually exclusive
assert!(!(is_x86_64() && is_aarch64()));
}
+
+ /// Helper function to generate XML for testing
+ fn generate_xml(config: &ArchConfig, writer_fn: F) -> String
+ where
+ F: FnOnce(&ArchConfig, &mut XmlWriter) -> Result<()>,
+ {
+ let mut writer = XmlWriter::new();
+ writer_fn(config, &mut writer).unwrap();
+ writer.into_string().unwrap()
+ }
+
+ #[test]
+ fn test_x86_64_specific_features() {
+ // Test x86_64 configuration specifically
+ let x86_config = ArchConfig {
+ arch: "x86_64",
+ machine: "q35",
+ os_type: "hvm",
+ };
+
+ let features_xml = generate_xml(&x86_config, |cfg, w| cfg.write_features(w));
+
+ // Should have x86_64-specific vmport feature
+ assert!(features_xml.contains("vmport"));
+ assert!(features_xml.contains("state=\"off\""));
+
+ let timers_xml = generate_xml(&x86_config, |cfg, w| cfg.write_timers(w));
+
+ // Should have x86_64-specific timers
+ assert!(timers_xml.contains("pit"));
+ assert!(timers_xml.contains("hpet"));
+ assert!(timers_xml.contains("present=\"no\""));
+ }
+
+ #[test]
+ fn test_aarch64_specific_features() {
+ // Test aarch64 configuration specifically
+ let arm_config = ArchConfig {
+ arch: "aarch64",
+ machine: "virt",
+ os_type: "hvm",
+ };
+
+ let features_xml = generate_xml(&arm_config, |cfg, w| cfg.write_features(w));
+
+ // Should NOT have x86_64-specific vmport feature
+ assert!(!features_xml.contains("vmport"));
+
+ let timers_xml = generate_xml(&arm_config, |cfg, w| cfg.write_timers(w));
+
+ // Should NOT have x86_64-specific timers
+ assert!(!timers_xml.contains("pit"));
+ assert!(!timers_xml.contains("hpet"));
+ // But should still have RTC
+ assert!(timers_xml.contains("rtc"));
+ }
}
diff --git a/crates/kit/src/domain_list.rs b/crates/kit/src/domain_list.rs
index 67ef5ad57..2d1b80ebf 100644
--- a/crates/kit/src/domain_list.rs
+++ b/crates/kit/src/domain_list.rs
@@ -3,6 +3,7 @@
//! This module provides functionality to list libvirt domains created by bcvk libvirt,
//! using libvirt as the source of truth instead of the VmRegistry cache.
+use crate::xml_utils;
use color_eyre::{eyre::Context, Result};
use serde::{Deserialize, Serialize};
use std::process::Command;
@@ -130,8 +131,8 @@ impl DomainLister {
Ok(String::from_utf8(output.stdout)?.trim().to_string())
}
- /// Get domain XML metadata
- pub fn get_domain_xml(&self, domain_name: &str) -> Result {
+ /// Get domain XML metadata as parsed DOM
+ pub fn get_domain_xml(&self, domain_name: &str) -> Result {
let output = self
.virsh_command()
.args(&["dumpxml", domain_name])
@@ -147,11 +148,16 @@ impl DomainLister {
));
}
- Ok(String::from_utf8(output.stdout)?)
+ let xml = String::from_utf8(output.stdout)?;
+ xml_utils::parse_xml_dom(&xml)
+ .with_context(|| format!("Failed to parse XML for domain '{}'", domain_name))
}
- /// Extract podman-bootc metadata from domain XML
- fn extract_podman_bootc_metadata(&self, xml: &str) -> Option {
+ /// Extract podman-bootc metadata from parsed domain XML
+ fn extract_podman_bootc_metadata(
+ &self,
+ dom: &xml_utils::XmlNode,
+ ) -> Result