7373//! quay.io/centos-bootc/centos-bootc:stream10 output.img
7474//! ```
7575
76+ use std:: io:: IsTerminal ;
77+
7678use crate :: install_options:: InstallOptions ;
77- use crate :: run_ephemeral:: { run_synchronous as run_ephemeral, CommonVmOpts , RunEphemeralOpts } ;
78- use crate :: { images, utils} ;
79+ use crate :: run_ephemeral:: { run_detached, CommonVmOpts , RunEphemeralOpts } ;
80+ use crate :: run_ephemeral_ssh:: wait_for_ssh_ready;
81+ use crate :: { images, ssh, utils} ;
7982use camino:: Utf8PathBuf ;
8083use clap:: { Parser , ValueEnum } ;
8184use color_eyre:: eyre:: Context ;
8285use color_eyre:: Result ;
83- use std :: borrow :: Cow ;
86+ use indoc :: indoc ;
8487use tracing:: debug;
8588
8689/// Supported disk image formats
@@ -162,43 +165,38 @@ impl ToDiskOpts {
162165 }
163166 }
164167
165- /// Generate the complete bootc installation command
168+ /// Generate the complete bootc installation command arguments for SSH execution
166169 fn generate_bootc_install_command ( & self ) -> Vec < String > {
167170 let source_imgref = format ! ( "containers-storage:{}" , self . source_image) ;
168-
169- let bootc_install = [
170- "env" ,
171- // This is the magic trick to pull the storage from the host
172- "STORAGE_OPTS=additionalimagestore=/run/virtiofs-mnt-hoststorage/" ,
173- "bootc" ,
174- "install" ,
175- "to-disk" ,
176- // Default to being a generic image here, if someone cares they can override this
177- "--generic-image" ,
178- // The default in newer versions, but support older ones too
179- "--skip-fetch-check" ,
180- "--source-imgref" ,
181- ]
182- . into_iter ( )
183- . map ( Cow :: Borrowed )
184- . chain ( std:: iter:: once ( source_imgref. into ( ) ) )
185- . chain ( self . install . to_bootc_args ( ) . into_iter ( ) . map ( Cow :: Owned ) )
186- . chain ( std:: iter:: once ( Cow :: Borrowed (
187- "/dev/disk/by-id/virtio-output" ,
188- ) ) )
189- . fold ( String :: new ( ) , |mut acc, elt| {
190- if !acc. is_empty ( ) {
191- acc. push ( ' ' ) ;
192- }
193- acc. push_str ( & * elt) ;
194- acc
195- } ) ;
196- // TODO: make /var a tmpfs by default (actually make run-ephemeral more like a readonly bootc)
197- vec ! [
198- "mount -t tmpfs tmpfs /var/lib/containers" . to_owned( ) ,
199- "mount -t tmpfs tmpfs /var/tmp" . to_owned( ) ,
200- bootc_install,
201- ]
171+ let bootc_args = self . install . to_bootc_args ( ) . join ( " " ) ;
172+
173+ // Create the complete script by substituting variables directly
174+ let script = indoc ! { r#"
175+ set -euo pipefail
176+
177+ echo "Setting up temporary filesystems..."
178+ mount -t tmpfs tmpfs /var/lib/containers
179+ mount -t tmpfs tmpfs /var/tmp
180+
181+ echo "Starting bootc installation..."
182+ echo "Source image: {SOURCE_IMGREF}"
183+ echo "Additional args: {BOOTC_ARGS}"
184+
185+ # Execute bootc installation
186+ env STORAGE_OPTS=additionalimagestore=/run/virtiofs-mnt-hoststorage/ \
187+ bootc install to-disk \
188+ --generic-image \
189+ --skip-fetch-check \
190+ --source-imgref "{SOURCE_IMGREF}" \
191+ {BOOTC_ARGS} \
192+ /dev/disk/by-id/virtio-output
193+
194+ echo "Installation completed successfully!"
195+ "# }
196+ . replace ( "{SOURCE_IMGREF}" , & source_imgref)
197+ . replace ( "{BOOTC_ARGS}" , & bootc_args) ;
198+
199+ vec ! [ "/bin/bash" . to_string( ) , "-c" . to_string( ) , script]
202200 }
203201
204202 /// Calculate the optimal target disk size based on the source image or explicit size
@@ -230,7 +228,7 @@ impl ToDiskOpts {
230228 }
231229}
232230
233- /// Execute a bootc installation using an ephemeral VM
231+ /// Execute a bootc installation using an ephemeral VM with SSH
234232///
235233/// Main entry point for the bootc installation process. See module-level documentation
236234/// for details on the installation workflow and architecture.
@@ -293,7 +291,11 @@ pub fn run(opts: ToDiskOpts) -> Result<()> {
293291 let bootc_install_command = opts. generate_bootc_install_command ( ) ;
294292
295293 // Phase 4: Ephemeral VM configuration
296- let common_opts = opts. common . clone ( ) ;
294+ let mut common_opts = opts. common . clone ( ) ;
295+ // Enable SSH key generation for SSH-based installation
296+ common_opts. ssh_keygen = true ;
297+
298+ let tty = std:: io:: stdout ( ) . is_terminal ( ) ;
297299
298300 // Configure VM for installation:
299301 // - Use source image as installer environment
@@ -304,7 +306,9 @@ pub fn run(opts: ToDiskOpts) -> Result<()> {
304306 image : opts. get_installer_image ( ) . to_string ( ) ,
305307 common : common_opts,
306308 podman : crate :: run_ephemeral:: CommonPodmanOptions {
307- rm : true , // Clean up container after installation
309+ rm : true , // Clean up container after installation
310+ detach : true , // Run in detached mode for SSH approach
311+ tty,
308312 label : opts. label ,
309313 ..Default :: default ( )
310314 } ,
@@ -324,24 +328,41 @@ pub fn run(opts: ToDiskOpts) -> Result<()> {
324328 ) ] , // Attach target disk
325329 } ;
326330
327- // Phase 5: Final VM configuration and execution
328- let mut final_opts = ephemeral_opts;
329- // Set the installation script to execute in the VM
330- final_opts. common . execute = bootc_install_command;
331-
332- // Ensure clean shutdown after installation completes
333- final_opts
334- . common
335- . kernel_args
336- . push ( "systemd.default_target=poweroff.target" . to_string ( ) ) ;
337-
338- // Phase 6: Launch VM and execute installation
339- // The ephemeral VM will:
340- // 1. Boot using the bootc image
341- // 2. Mount host storage and target disk
342- // 3. Execute the installation script
343- // 4. Shut down automatically after completion
344- match run_ephemeral ( final_opts) {
331+ // Phase 5: SSH-based VM configuration and execution
332+ // Launch VM in detached mode with SSH enabled
333+ debug ! ( "Starting ephemeral VM with SSH..." ) ;
334+ let container_id = run_detached ( ephemeral_opts) ?;
335+ debug ! ( "Ephemeral VM started with container ID: {}" , container_id) ;
336+
337+ // Use the SSH approach for better TTY forwarding and output buffering
338+ let result = ( || -> Result < ( ) > {
339+ // Wait for SSH to be ready
340+ let progress_bar = crate :: boot_progress:: create_boot_progress_bar ( ) ;
341+ let progress_bar = wait_for_ssh_ready (
342+ & container_id,
343+ std:: time:: Duration :: from_secs ( 60 ) ,
344+ progress_bar,
345+ ) ?;
346+ progress_bar. finish_and_clear ( ) ;
347+
348+ // Connect via SSH and execute the installation command
349+ debug ! (
350+ "Executing installation via SSH: {:?}" ,
351+ bootc_install_command
352+ ) ;
353+ ssh:: connect_via_container ( & container_id, bootc_install_command) ?;
354+
355+ Ok ( ( ) )
356+ } ) ( ) ;
357+
358+ // Cleanup: stop and remove the container
359+ debug ! ( "Cleaning up ephemeral container..." ) ;
360+ let _ = std:: process:: Command :: new ( "podman" )
361+ . args ( [ "rm" , "-f" , & container_id] )
362+ . output ( ) ;
363+
364+ // Handle the result - remove disk file on failure
365+ match result {
345366 Ok ( ( ) ) => Ok ( ( ) ) ,
346367 Err ( e) => {
347368 let _ = std:: fs:: remove_file ( & opts. target_disk ) ;
0 commit comments