Skip to content

Commit b59a478

Browse files
committed
to-disk: use ssh instead of exec
The exec flow is buffered and also doesn't handle things like tty widths etc. We could replicate all of that, but it's just way easier to fork ssh. This feels conceptually less clean in that my preference is actually for systems to be more autonomous, but this way right now is the only way we could sanely get a progress bar for example. Signed-off-by: Colin Walters <walters@verbum.org>
1 parent bc79ff7 commit b59a478

2 files changed

Lines changed: 80 additions & 76 deletions

File tree

crates/kit/src/run_ephemeral.rs

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -301,23 +301,6 @@ pub fn run(opts: RunEphemeralOpts) -> Result<()> {
301301
return Err(cmd.exec()).context("execve");
302302
}
303303

304-
/// Launch privileged container with QEMU+KVM for ephemeral VM and wait for completion.
305-
/// Unlike `run()`, this function waits for completion instead of using exec(), making it suitable
306-
/// for programmatic use where the caller needs to capture output and exit codes.
307-
pub fn run_synchronous(opts: RunEphemeralOpts) -> Result<()> {
308-
let (mut cmd, temp_dir) = prepare_run_command_with_temp(opts)?;
309-
// Keep temp_dir alive until command completes
310-
311-
// Use the same approach as run_detached but wait for completion instead of detaching
312-
let status = cmd.status().context("Failed to execute podman command")?;
313-
if !status.success() {
314-
return Err(color_eyre::eyre::eyre!("ephemeral run failed {status:?}",));
315-
}
316-
// Explicitly drop temp_dir after successful completion
317-
drop(temp_dir);
318-
Ok(())
319-
}
320-
321304
fn prepare_run_command_with_temp(
322305
opts: RunEphemeralOpts,
323306
) -> Result<(std::process::Command, tempfile::TempDir)> {

crates/kit/src/to_disk.rs

Lines changed: 80 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,17 @@
7373
//! quay.io/centos-bootc/centos-bootc:stream10 output.img
7474
//! ```
7575
76+
use std::io::IsTerminal;
77+
7678
use 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};
7982
use camino::Utf8PathBuf;
8083
use clap::{Parser, ValueEnum};
8184
use color_eyre::eyre::Context;
8285
use color_eyre::Result;
83-
use std::borrow::Cow;
86+
use indoc::indoc;
8487
use 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

Comments
 (0)