diff --git a/crates/kit/src/libvirt/domain.rs b/crates/kit/src/libvirt/domain.rs index 1d6909b..98d8ba0 100644 --- a/crates/kit/src/libvirt/domain.rs +++ b/crates/kit/src/libvirt/domain.rs @@ -43,7 +43,7 @@ pub struct DomainBuilder { disk_path: Option, transient_disk: bool, // Use transient disk with temporary overlay network: Option, - vnc_port: Option, + graphical_console: bool, kernel_args: Option, metadata: HashMap, qemu_args: Vec, @@ -76,7 +76,7 @@ impl DomainBuilder { disk_path: None, transient_disk: false, network: None, - vnc_port: None, + graphical_console: false, kernel_args: None, metadata: HashMap::new(), qemu_args: Vec::new(), @@ -129,10 +129,9 @@ impl DomainBuilder { self } - /// Enable VNC on specified port - #[allow(dead_code)] - pub fn with_vnc(mut self, port: u16) -> Self { - self.vnc_port = Some(port); + /// Enable graphical console (SPICE) for virt-manager access + pub fn with_graphical_console(mut self) -> Self { + self.graphical_console = true; self } @@ -474,19 +473,23 @@ impl DomainBuilder { } } - // VNC graphics if enabled - if let Some(vnc_port) = self.vnc_port { + // Graphical console (SPICE) for virt-manager access + if self.graphical_console { + writer.start_element("graphics", &[("type", "spice"), ("autoport", "yes")])?; + writer.write_empty_element("listen", &[("type", "address")])?; + writer.end_element("graphics")?; + writer.start_element("video", &[])?; writer.write_empty_element( - "graphics", - &[ - ("type", "vnc"), - ("port", &vnc_port.to_string()), - ("listen", "127.0.0.1"), - ], + "model", + &[("type", "virtio"), ("heads", "1"), ("primary", "yes")], )?; - writer.start_element("video", &[])?; - writer.write_empty_element("model", &[("type", "vga")])?; writer.end_element("video")?; + writer.start_element("channel", &[("type", "spicevmc")])?; + writer.write_empty_element( + "target", + &[("type", "virtio"), ("name", "com.redhat.spice.0")], + )?; + writer.end_element("channel")?; } // Virtiofs filesystems @@ -633,15 +636,28 @@ mod tests { } #[test] - fn test_vnc_configuration() { + fn test_graphical_console_configuration() { + // Test with graphical console enabled let xml = DomainBuilder::new() .with_name("test") - .with_vnc(5901) + .with_graphical_console() + .build_xml() + .unwrap(); + + assert!(xml.contains("graphics type=\"spice\" autoport=\"yes\"")); + assert!(xml.contains("model type=\"virtio\" heads=\"1\" primary=\"yes\"")); + assert!(xml.contains("channel type=\"spicevmc\"")); + assert!(xml.contains("target type=\"virtio\" name=\"com.redhat.spice.0\"")); + + // Test without graphical console (default) + let xml_no_graphics = DomainBuilder::new() + .with_name("test-no-graphics") .build_xml() .unwrap(); - assert!(xml.contains("graphics type=\"vnc\" port=\"5901\"")); - assert!(xml.contains("model type=\"vga\"")); + assert!(!xml_no_graphics.contains(", + /// Enable graphical console (SPICE) for virt-manager access + #[clap(long)] + pub graphical_console: bool, + /// Create a transient VM that disappears on shutdown/reboot #[clap(long)] pub transient: bool, @@ -473,6 +477,12 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, mut opts: LibvirtRunOpt opts.install.target_transport = Some(UPDATE_FROM_HOST_TRANSPORT.to_owned()); } + // Add console=tty1 kernel argument for graphical console support + if opts.graphical_console { + opts.install.karg.push("console=tty1".to_string()); + debug!("Added console=tty1 kernel argument for graphical console"); + } + // Add Ignition kernel argument to install options if Ignition config is specified // This ensures the kernel arg is baked into the installed system's GRUB configuration if opts.ignition_config.is_some() { @@ -1193,6 +1203,9 @@ fn create_libvirt_domain_from_disk( domain_builder = domain_builder.with_firmware_log(crate::libvirt::domain::FirmwareLogOutput::Console); } + if opts.graphical_console { + domain_builder = domain_builder.with_graphical_console(); + } domain_builder = domain_builder .with_metadata("bootc:source-image", &opts.image) .with_metadata("bootc:memory-mb", &memory.to_string())