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 } ;
81- use color_eyre:: eyre:: Context ;
84+ use color_eyre:: 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,51 @@ impl ToDiskOpts {
162165 }
163166 }
164167
165- /// Generate the complete bootc installation command
166- fn generate_bootc_install_command ( & self ) -> Vec < String > {
168+ /// Generate the complete bootc installation command arguments for SSH execution
169+ fn generate_bootc_install_command ( & self ) -> Result < Vec < String > > {
167170 let source_imgref = format ! ( "containers-storage:{}" , self . source_image) ;
168171
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- ]
172+ // Quote each bootc argument individually to prevent shell injection
173+ let mut quoted_bootc_args = Vec :: new ( ) ;
174+ for arg in self . install . to_bootc_args ( ) {
175+ let quoted = shlex:: try_quote ( & arg)
176+ . map_err ( |e| eyre ! ( "Failed to quote bootc argument '{}': {}" , arg, e) ) ?;
177+ quoted_bootc_args. push ( quoted. to_string ( ) ) ;
178+ }
179+ let bootc_args = quoted_bootc_args. join ( " " ) ;
180+
181+ // Quote the source image reference to prevent shell injection
182+ let quoted_source_imgref = shlex:: try_quote ( & source_imgref)
183+ . map_err ( |e| eyre ! ( "Failed to quote source imgref '{}': {}" , source_imgref, e) ) ?
184+ . to_string ( ) ;
185+
186+ // Create the complete script by substituting variables directly
187+ let script = indoc ! { r#"
188+ set -euo pipefail
189+
190+ echo "Setting up temporary filesystems..."
191+ mount -t tmpfs tmpfs /var/lib/containers
192+ mount -t tmpfs tmpfs /var/tmp
193+
194+ echo "Starting bootc installation..."
195+ echo "Source image: {SOURCE_IMGREF}"
196+ echo "Additional args: {BOOTC_ARGS}"
197+
198+ # Execute bootc installation
199+ env STORAGE_OPTS=additionalimagestore=/run/virtiofs-mnt-hoststorage/ \
200+ bootc install to-disk \
201+ --generic-image \
202+ --skip-fetch-check \
203+ --source-imgref {SOURCE_IMGREF} \
204+ {BOOTC_ARGS} \
205+ /dev/disk/by-id/virtio-output
206+
207+ echo "Installation completed successfully!"
208+ "# }
209+ . replace ( "{SOURCE_IMGREF}" , & quoted_source_imgref)
210+ . replace ( "{BOOTC_ARGS}" , & bootc_args) ;
211+
212+ Ok ( vec ! [ "/bin/bash" . to_string( ) , "-c" . to_string( ) , script] )
202213 }
203214
204215 /// Calculate the optimal target disk size based on the source image or explicit size
@@ -230,7 +241,7 @@ impl ToDiskOpts {
230241 }
231242}
232243
233- /// Execute a bootc installation using an ephemeral VM
244+ /// Execute a bootc installation using an ephemeral VM with SSH
234245///
235246/// Main entry point for the bootc installation process. See module-level documentation
236247/// for details on the installation workflow and architecture.
@@ -290,10 +301,14 @@ pub fn run(opts: ToDiskOpts) -> Result<()> {
290301
291302 // Phase 3: Installation command generation
292303 // Generate complete script including storage setup and bootc install
293- let bootc_install_command = opts. generate_bootc_install_command ( ) ;
304+ let bootc_install_command = opts. generate_bootc_install_command ( ) ? ;
294305
295306 // Phase 4: Ephemeral VM configuration
296- let common_opts = opts. common . clone ( ) ;
307+ let mut common_opts = opts. common . clone ( ) ;
308+ // Enable SSH key generation for SSH-based installation
309+ common_opts. ssh_keygen = true ;
310+
311+ let tty = std:: io:: stdout ( ) . is_terminal ( ) ;
297312
298313 // Configure VM for installation:
299314 // - Use source image as installer environment
@@ -304,7 +319,9 @@ pub fn run(opts: ToDiskOpts) -> Result<()> {
304319 image : opts. get_installer_image ( ) . to_string ( ) ,
305320 common : common_opts,
306321 podman : crate :: run_ephemeral:: CommonPodmanOptions {
307- rm : true , // Clean up container after installation
322+ rm : true , // Clean up container after installation
323+ detach : true , // Run in detached mode for SSH approach
324+ tty,
308325 label : opts. label ,
309326 ..Default :: default ( )
310327 } ,
@@ -324,24 +341,41 @@ pub fn run(opts: ToDiskOpts) -> Result<()> {
324341 ) ] , // Attach target disk
325342 } ;
326343
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) {
344+ // Phase 5: SSH-based VM configuration and execution
345+ // Launch VM in detached mode with SSH enabled
346+ debug ! ( "Starting ephemeral VM with SSH..." ) ;
347+ let container_id = run_detached ( ephemeral_opts) ?;
348+ debug ! ( "Ephemeral VM started with container ID: {}" , container_id) ;
349+
350+ // Use the SSH approach for better TTY forwarding and output buffering
351+ let result = ( || -> Result < ( ) > {
352+ // Wait for SSH to be ready
353+ let progress_bar = crate :: boot_progress:: create_boot_progress_bar ( ) ;
354+ let progress_bar = wait_for_ssh_ready (
355+ & container_id,
356+ std:: time:: Duration :: from_secs ( 60 ) ,
357+ progress_bar,
358+ ) ?;
359+ progress_bar. finish_and_clear ( ) ;
360+
361+ // Connect via SSH and execute the installation command
362+ debug ! (
363+ "Executing installation via SSH: {:?}" ,
364+ bootc_install_command
365+ ) ;
366+ ssh:: connect_via_container ( & container_id, bootc_install_command) ?;
367+
368+ Ok ( ( ) )
369+ } ) ( ) ;
370+
371+ // Cleanup: stop and remove the container
372+ debug ! ( "Cleaning up ephemeral container..." ) ;
373+ let _ = std:: process:: Command :: new ( "podman" )
374+ . args ( [ "rm" , "-f" , & container_id] )
375+ . output ( ) ;
376+
377+ // Handle the result - remove disk file on failure
378+ match result {
345379 Ok ( ( ) ) => Ok ( ( ) ) ,
346380 Err ( e) => {
347381 let _ = std:: fs:: remove_file ( & opts. target_disk ) ;
0 commit comments