@@ -118,7 +118,7 @@ pub struct ResourceLimits {
118118 pub nice_level : Option < i8 > ,
119119}
120120
121- /// VM boot configuration: direct kernel boot .
121+ /// VM boot configuration.
122122#[ derive( Debug ) ]
123123pub enum BootMode {
124124 /// Direct kernel boot (fast, testing-focused).
@@ -139,6 +139,15 @@ pub enum BootMode {
139139 /// VirtIO-FS socket for root filesystem.
140140 virtiofs_socket : Utf8PathBuf ,
141141 } ,
142+ /// Boot from an ISO image (e.g. for Anaconda installer testing).
143+ ///
144+ /// The ISO is attached as a CDROM device. Unlike DirectBoot, there is no
145+ /// root virtiofs socket — the installer boots from the ISO and installs
146+ /// to a disk device added via [`QemuConfig::add_virtio_blk_device`].
147+ IsoBoot {
148+ /// Path to the ISO image file.
149+ iso_path : String ,
150+ } ,
142151}
143152
144153/// Complete QEMU VM configuration with builder pattern.
@@ -171,6 +180,11 @@ pub struct QemuConfig {
171180 pub enable_console : bool ,
172181 /// SMBIOS credentials for systemd.
173182 smbios_credentials : Vec < String > ,
183+ /// Path to write serial console output (if set, `-serial file:<path>`
184+ /// is used instead of `-serial none`).
185+ pub serial_log : Option < String > ,
186+ /// Prevent automatic reboot (useful for debugging or post-install inspection).
187+ pub no_reboot : bool ,
174188
175189 /// Write systemd notifications to this file.
176190 pub systemd_notify : Option < File > ,
@@ -200,6 +214,16 @@ impl QemuConfig {
200214 }
201215 }
202216
217+ /// Create a new config for ISO boot (e.g. Anaconda installer).
218+ pub fn new_iso_boot ( memory_mb : u32 , vcpus : u32 , iso_path : String ) -> Self {
219+ Self {
220+ memory_mb,
221+ vcpus,
222+ boot_mode : Some ( BootMode :: IsoBoot { iso_path } ) ,
223+ ..Default :: default ( )
224+ }
225+ }
226+
203227 /// Enable vsock support.
204228 pub fn enable_vsock ( & mut self ) -> Result < ( ) > {
205229 let fd = OpenOptions :: new ( )
@@ -249,6 +273,39 @@ impl QemuConfig {
249273 return Err ( eyre ! ( "vCPU count too high: {} (maximum 256)" , self . vcpus) ) ;
250274 }
251275
276+ // Validate boot mode specifics
277+ match & self . boot_mode {
278+ Some ( BootMode :: IsoBoot { iso_path } ) => {
279+ if iso_path. is_empty ( ) {
280+ return Err ( eyre ! ( "ISO path cannot be empty" ) ) ;
281+ }
282+ if !std:: path:: Path :: new ( iso_path) . exists ( ) {
283+ return Err ( eyre ! ( "ISO image not found: {}" , iso_path) ) ;
284+ }
285+ // main_virtiofs_config is for the root filesystem in DirectBoot;
286+ // it has no meaning for ISO boot.
287+ if self . main_virtiofs_config . is_some ( ) {
288+ return Err ( eyre ! (
289+ "main_virtiofs_config is not supported with ISO boot \
290+ (the root filesystem comes from the ISO)"
291+ ) ) ;
292+ }
293+ }
294+ Some ( BootMode :: DirectBoot {
295+ kernel_path,
296+ initramfs_path,
297+ ..
298+ } ) => {
299+ if kernel_path. is_empty ( ) {
300+ return Err ( eyre ! ( "Kernel path cannot be empty" ) ) ;
301+ }
302+ if initramfs_path. is_empty ( ) {
303+ return Err ( eyre ! ( "Initramfs path cannot be empty" ) ) ;
304+ }
305+ }
306+ None => { }
307+ }
308+
252309 // Validate virtiofs mounts
253310 for mount in & self . additional_mounts {
254311 if mount. tag . is_empty ( ) {
@@ -430,6 +487,7 @@ fn spawn(
430487 . map_err ( Into :: into)
431488 } ) ;
432489 }
490+
433491 cmd. args ( [
434492 "-m" ,
435493 & memory_arg,
@@ -446,6 +504,10 @@ fn spawn(
446504 "node,memdev=mem" ,
447505 ] ) ;
448506
507+ if config. no_reboot {
508+ cmd. arg ( "-no-reboot" ) ;
509+ }
510+
449511 for ( idx, fd) in config. fdset . iter ( ) . enumerate ( ) {
450512 let fd_id = 100 + idx as u32 ; // Start at 100 to avoid conflicts
451513 let set_id = idx + 1 ; // fdset starts at 1
@@ -498,6 +560,9 @@ fn spawn(
498560 let append_str = kernel_cmdline. join ( " " ) ;
499561 cmd. args ( [ "-append" , & append_str] ) ;
500562 }
563+ Some ( BootMode :: IsoBoot { iso_path } ) => {
564+ cmd. args ( [ "-cdrom" , iso_path] ) ;
565+ }
501566 None => { }
502567 }
503568
@@ -562,8 +627,13 @@ fn spawn(
562627 }
563628 }
564629
565- // No GUI, and no emulated serial ports by default.
566- cmd. args ( [ "-serial" , "none" , "-nographic" , "-display" , "none" ] ) ;
630+ // No GUI; serial console either to a log file or disabled.
631+ if let Some ( ref serial_path) = config. serial_log {
632+ cmd. args ( [ "-serial" , & format ! ( "file:{}" , serial_path) ] ) ;
633+ } else {
634+ cmd. args ( [ "-serial" , "none" ] ) ;
635+ }
636+ cmd. args ( [ "-nographic" , "-display" , "none" ] ) ;
567637
568638 match & config. display_mode {
569639 DisplayMode :: None => {
@@ -868,6 +938,17 @@ mod tests {
868938 ) ;
869939 }
870940
941+ #[ test]
942+ fn test_iso_boot_config ( ) {
943+ let config = QemuConfig :: new_iso_boot ( 2048 , 2 , "/test/image.iso" . to_string ( ) ) ;
944+ assert_eq ! ( config. memory_mb, 2048 ) ;
945+ assert_eq ! ( config. vcpus, 2 ) ;
946+ assert ! ( matches!(
947+ & config. boot_mode,
948+ Some ( BootMode :: IsoBoot { iso_path } ) if iso_path == "/test/image.iso"
949+ ) ) ;
950+ }
951+
871952 #[ test]
872953 fn test_disk_format ( ) {
873954 assert_eq ! ( DiskFormat :: Raw . as_str( ) , "raw" ) ;
0 commit comments