diff --git a/Cargo.lock b/Cargo.lock index 4c96b7b..ec90115 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1016,6 +1016,7 @@ dependencies = [ "firecracker-state", "firecracker-vm", "glob", + "libc", "names", "num_cpus", "owo-colors", @@ -1702,9 +1703,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.174" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libloading" diff --git a/README.md b/README.md index d1c531e..0d6bfa6 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ Commands: rm Delete the Firecracker MicroVM serve Start fireup HTTP API server inspect Inspect the Firecracker MicroVM details + exec Execute a command inside the Firecracker MicroVM help Print this message or the help of the given subcommand(s) Options: diff --git a/crates/firecracker-up/Cargo.toml b/crates/firecracker-up/Cargo.toml index 66c3093..7dac8e4 100644 --- a/crates/firecracker-up/Cargo.toml +++ b/crates/firecracker-up/Cargo.toml @@ -32,3 +32,4 @@ sqlx = { version = "0.8.6", features = [ chrono = "0.4.42" serde_json = "1.0.145" colored_json = "5.0.0" +libc = "0.2.175" diff --git a/crates/firecracker-up/src/cmd/cp.rs b/crates/firecracker-up/src/cmd/cp.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/firecracker-up/src/cmd/cp.rs @@ -0,0 +1 @@ + diff --git a/crates/firecracker-up/src/cmd/exec.rs b/crates/firecracker-up/src/cmd/exec.rs new file mode 100644 index 0000000..c9d2b50 --- /dev/null +++ b/crates/firecracker-up/src/cmd/exec.rs @@ -0,0 +1,48 @@ +use anyhow::{anyhow, Context, Error, Result}; +use firecracker_state::repo; +use owo_colors::OwoColorize; +use std::fs; + +use crate::command::run_ssh_command; + +pub async fn exec(name: &str, args: Vec) -> Result<(), Error> { + let pool = firecracker_state::create_connection_pool().await?; + let vm = repo::virtual_machine::find(&pool, name).await?; + + if vm.is_none() { + println!("[-] MicroVM '{}' not found.", name); + std::process::exit(1); + } + + if !firecracker_process::vm_is_running(name).await? { + println!("[-] MicroVM '{}' is not running.", name); + let start_cmd = format!("fireup start {}", name); + println!(" Start it with {}", start_cmd.cyan()); + std::process::exit(1); + } + + let guest_ip = format!("{}.firecracker", name); + run_ssh_command(&get_private_key_path()?, &guest_ip, args.join(" ").as_str())?; + + Ok(()) +} + +fn get_private_key_path() -> Result { + let home_dir = dirs::home_dir().ok_or_else(|| anyhow!("Failed to get home directory"))?; + let app_dir = format!("{}/.fireup", home_dir.display()); + let key_name = glob::glob(format!("{}/id_rsa", app_dir).as_str()) + .with_context(|| "Failed to glob ssh key files")? + .last() + .ok_or_else(|| anyhow!("No SSH key file found"))? + .with_context(|| "Failed to get SSH key path")?; + let key_name = fs::canonicalize(&key_name) + .with_context(|| { + format!( + "Failed to resolve absolute path for SSH key: {}", + key_name.display() + ) + })? + .display() + .to_string(); + Ok(key_name) +} diff --git a/crates/firecracker-up/src/cmd/mod.rs b/crates/firecracker-up/src/cmd/mod.rs index 3ad7a8f..fb9251a 100644 --- a/crates/firecracker-up/src/cmd/mod.rs +++ b/crates/firecracker-up/src/cmd/mod.rs @@ -1,4 +1,6 @@ +pub mod cp; pub mod down; +pub mod exec; pub mod init; pub mod inspect; pub mod logs; diff --git a/crates/firecracker-up/src/command.rs b/crates/firecracker-up/src/command.rs index e8614d4..7110330 100644 --- a/crates/firecracker-up/src/command.rs +++ b/crates/firecracker-up/src/command.rs @@ -1,6 +1,18 @@ use anyhow::{anyhow, Context, Result}; use std::process::{Command, Output, Stdio}; +pub fn has_sudo() -> bool { + Command::new("sudo") + .arg("-h") + .output() + .map(|output| output.status.success()) + .unwrap_or(false) +} + +pub fn is_root() -> bool { + unsafe { libc::getuid() == 0 } +} + pub fn run_command(command: &str, args: &[&str], with_stdin: bool) -> Result { let mut cmd = Command::new(command); @@ -34,3 +46,62 @@ pub fn run_command(command: &str, args: &[&str], with_stdin: bool) -> Result Result<()> { + let mut cmd = if use_sudo { + if !has_sudo() && !is_root() { + return Err(anyhow!( + "sudo is required for command '{}', but not available", + command + )); + } + let mut c = Command::new("sudo"); + c.arg(command); + + match is_root() { + true => Command::new(command), + false => c, + } + } else { + Command::new(command) + }; + + let mut child = cmd + .args(args) + .stdin(Stdio::inherit()) + .stderr(Stdio::inherit()) + .stdout(Stdio::inherit()) + .spawn() + .with_context(|| format!("Failed to execute {}", command))?; + + let status = child.wait()?; + + if !status.success() { + return Err(anyhow!( + "Command {} failed with status: {}", + command, + status + )); + } + + Ok(()) +} + +pub fn run_ssh_command(key_path: &str, guest_ip: &str, command: &str) -> Result<()> { + run_command_with_stdout_inherit( + "ssh", + &[ + "-q", + "-i", + key_path, + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + &format!("root@{}", guest_ip), + command, + ], + false, + )?; + Ok(()) +} diff --git a/crates/firecracker-up/src/main.rs b/crates/firecracker-up/src/main.rs index 99a5768..df4b66b 100644 --- a/crates/firecracker-up/src/main.rs +++ b/crates/firecracker-up/src/main.rs @@ -166,6 +166,20 @@ fn cli() -> Command { .arg(arg!( "Name or ID of the Firecracker MicroVM to inspect").required(true)) .about("Inspect the Firecracker MicroVM details"), ) + .subcommand( + Command::new("exec") + .arg( + arg!( "Name of the Firecracker MicroVM to execute command in") + .required(true), + ) + .arg( + Arg::new("args") + .help("Command and arguments to execute inside the MicroVM") + .required(true) + .num_args(1..), + ) + .about("Execute a command inside the Firecracker MicroVM"), + ) .arg(arg!(--debian "Prepare Debian MicroVM").default_value("false")) .arg(arg!(--alpine "Prepare Alpine MicroVM").default_value("false")) .arg(arg!(--nixos "Prepare NixOS MicroVM").default_value("false")) @@ -341,6 +355,15 @@ async fn main() -> Result<()> { let name = args.get_one::("name").cloned().unwrap(); inspect_microvm(&name).await?; } + Some(("exec", args)) => { + let name = args.get_one::("name").cloned().unwrap(); + let cmd_args: Vec = args + .get_many::("args") + .unwrap() + .map(|s| s.to_string()) + .collect(); + cmd::exec::exec(&name, cmd_args).await?; + } _ => { let debian = matches.get_one::("debian").copied().unwrap_or(false); let alpine = matches.get_one::("alpine").copied().unwrap_or(false); diff --git a/crates/firecracker-vm/src/guest.rs b/crates/firecracker-vm/src/guest.rs index eff5e4a..c4a5306 100644 --- a/crates/firecracker-vm/src/guest.rs +++ b/crates/firecracker-vm/src/guest.rs @@ -13,6 +13,8 @@ pub fn configure_guest_network(key_path: &str, guest_ip: &str) -> Result<()> { key_path, "-o", "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", &format!("root@{}", guest_ip), &format!("echo 'nameserver {}' > /etc/resolv.conf", BRIDGE_IP), ], diff --git a/crates/firecracker-vm/src/tailscale.rs b/crates/firecracker-vm/src/tailscale.rs index a1ded97..6eb4c84 100644 --- a/crates/firecracker-vm/src/tailscale.rs +++ b/crates/firecracker-vm/src/tailscale.rs @@ -88,6 +88,8 @@ fn run_ssh_command(key_path: &str, guest_ip: &str, command: &str) -> Result<(), key_path, "-o", "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", &format!("root@{}", guest_ip), command, ],