Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions crates/integration-tests/src/tests/run_ephemeral_ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ pub fn test_run_ephemeral_ssh_system_command() {

eprintln!("Testing ephemeral run-ssh with system command...");

// Run ephemeral SSH with systemctl command
// Run ephemeral SSH with systemctl command - using /bin/sh -c for shell operators
let output = Command::new("timeout")
.args([
"60s",
Expand All @@ -137,10 +137,9 @@ pub fn test_run_ephemeral_ssh_system_command() {
INTEGRATION_TEST_LABEL,
&get_test_image(),
"--",
"systemctl",
"is-system-running",
"||",
"true", // Allow non-zero exit for degraded state
"/bin/sh",
"-c",
"systemctl is-system-running || true", // Shell command with operator
])
.output()
.expect("Failed to run bcvk ephemeral run-ssh");
Expand Down
2 changes: 2 additions & 0 deletions crates/kit/src/libvirt/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtRunOpts) -
command: vec![],
strict_host_keys: false,
timeout: 30,
log_level: "ERROR".to_string(),
extra_options: vec![],
};
crate::libvirt::ssh::run(global_opts, ssh_opts)
} else {
Expand Down
62 changes: 43 additions & 19 deletions crates/kit/src/libvirt/ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ pub struct LibvirtSshOpts {
/// SSH connection timeout in seconds
#[clap(long, default_value = "30")]
pub timeout: u32,

/// SSH log level
#[clap(long, default_value = "ERROR")]
pub log_level: String,

/// Extra SSH options in key=value format
#[clap(long)]
pub extra_options: Vec<String>,
}

/// SSH configuration extracted from domain metadata
Expand Down Expand Up @@ -245,36 +253,52 @@ impl LibvirtSshOpts {
// Build SSH command
let mut ssh_cmd = Command::new("ssh");

// Basic SSH options
// Add SSH key and port
ssh_cmd
.arg("-i")
.arg(temp_key.path())
.arg("-p")
.arg(ssh_config.ssh_port.to_string())
.args(["-o", "IdentitiesOnly=yes"])
.arg("-o")
.arg("PasswordAuthentication=no")
.arg("-o")
.arg("ConnectTimeout=30")
.arg("-o")
.arg("ServerAliveInterval=60");

// Host key checking
if !self.strict_host_keys {
ssh_cmd
.arg("-o")
.arg("StrictHostKeyChecking=no")
.arg("-o")
.arg("UserKnownHostsFile=/dev/null");
.arg(ssh_config.ssh_port.to_string());

// Parse extra options from key=value format
let mut parsed_extra_options = Vec::new();
for option in &self.extra_options {
if let Some((key, value)) = option.split_once('=') {
parsed_extra_options.push((key.to_string(), value.to_string()));
} else {
return Err(eyre!(
"Invalid extra option format '{}'. Expected 'key=value'",
option
));
}
}

// Apply common SSH options
let common_opts = crate::ssh::CommonSshOptions {
strict_host_keys: self.strict_host_keys,
connect_timeout: self.timeout,
server_alive_interval: 60,
log_level: self.log_level.clone(),
extra_options: parsed_extra_options,
};
Comment thread
cgwalters marked this conversation as resolved.
common_opts.apply_to_command(&mut ssh_cmd);

// Target host
ssh_cmd.arg(format!("{}@127.0.0.1", self.user));

// Add command if specified
// Add command if specified - use the same argument escaping logic as container SSH
if !self.command.is_empty() {
ssh_cmd.arg("--");
ssh_cmd.args(&self.command);
if self.command.len() > 1 {
// Multiple arguments need proper shell escaping
let combined_command = crate::ssh::shell_escape_command(&self.command)
.map_err(|e| eyre!("Failed to escape shell command: {}", e))?;
debug!("Combined escaped command: {}", combined_command);
ssh_cmd.arg(combined_command);
} else {
// Single argument can be passed directly
ssh_cmd.args(&self.command);
}
}

debug!("Executing SSH command: {:?}", ssh_cmd);
Expand Down
4 changes: 4 additions & 0 deletions crates/kit/src/libvirt/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtStartOpts)
command: vec![],
strict_host_keys: false,
timeout: 30,
log_level: "ERROR".to_string(),
extra_options: vec![],
};
return crate::libvirt::ssh::run(global_opts, ssh_opts);
}
Expand Down Expand Up @@ -77,6 +79,8 @@ pub fn run(global_opts: &crate::libvirt::LibvirtOptions, opts: LibvirtStartOpts)
command: vec![],
strict_host_keys: false,
timeout: 30,
log_level: "ERROR".to_string(),
extra_options: vec![],
};
crate::libvirt::ssh::run(global_opts, ssh_opts)
} else {
Expand Down
40 changes: 22 additions & 18 deletions crates/kit/src/run_ephemeral.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
use std::fs::File;
use std::io::{BufWriter, Seek, Write};
use std::os::unix::process::CommandExt;
use std::process::Command;
use std::process::{Command, Stdio};

use bootc_utils::CommandRunExt;
use camino::{Utf8Path, Utf8PathBuf};
Expand Down Expand Up @@ -301,23 +301,6 @@ pub fn run(opts: RunEphemeralOpts) -> Result<()> {
return Err(cmd.exec()).context("execve");
}

/// Launch privileged container with QEMU+KVM for ephemeral VM and wait for completion.
/// Unlike `run()`, this function waits for completion instead of using exec(), making it suitable
/// for programmatic use where the caller needs to capture output and exit codes.
pub fn run_synchronous(opts: RunEphemeralOpts) -> Result<()> {
let (mut cmd, temp_dir) = prepare_run_command_with_temp(opts)?;
// Keep temp_dir alive until command completes

// Use the same approach as run_detached but wait for completion instead of detaching
let status = cmd.status().context("Failed to execute podman command")?;
if !status.success() {
return Err(color_eyre::eyre::eyre!("ephemeral run failed {status:?}",));
}
// Explicitly drop temp_dir after successful completion
drop(temp_dir);
Ok(())
}

fn prepare_run_command_with_temp(
opts: RunEphemeralOpts,
) -> Result<(std::process::Command, tempfile::TempDir)> {
Expand Down Expand Up @@ -725,6 +708,21 @@ pub(crate) async fn run_impl(opts: RunEphemeralOpts) -> Result<()> {
};
debug!("Container image systemd version: {systemd_version:?}");

// Check if we need to handle cloud-init
let cloudinit = {
Command::new("systemctl")
.args([
"--root=/run/source-image",
"is-enabled",
"cloud-init.target",
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()?
.success()
};
tracing::debug!("Target image has cloud-init: {cloudinit}");

// Find kernel and initramfs from the container image (not the host)
let modules_dir = Utf8Path::new("/run/source-image/usr/lib/modules");
let mut vmlinuz_path = None;
Expand Down Expand Up @@ -991,6 +989,12 @@ StandardOutput=file:/dev/virtio-ports/executestatus
if opts.common.console {
kernel_cmdline.push("console=ttyS0".to_string());
}
if cloudinit {
// We don't provide any cloud-init datasource right now,
// though in the future it would make sense to do so,
// and switch over our SSH key injection.
kernel_cmdline.push("ds=iid-datasource-none".to_string());
}

kernel_cmdline.extend(opts.common.kernel_args.clone());

Expand Down
2 changes: 1 addition & 1 deletion crates/kit/src/run_ephemeral_ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ pub fn run_ephemeral_ssh(opts: RunEphemeralSshOpts) -> Result<()> {

// Execute SSH connection directly (no thread needed for this)
// This allows SSH output to be properly forwarded to stdout/stderr
debug!("Connecting to SSH...");
debug!("Connecting to SSH with args: {:?}", opts.ssh_args);
let status = ssh::connect_via_container_with_status(&container_name, opts.ssh_args)?;
debug!("SSH connection completed");

Expand Down
Loading