Prepare, configure, and manage Firecracker microVMs in seconds!
virtualization linux microvm firecracker

feat: add exec command for Firecracker MicroVM and update dependencies

+154 -2
+3 -2
Cargo.lock
··· 1016 1016 "firecracker-state", 1017 1017 "firecracker-vm", 1018 1018 "glob", 1019 + "libc", 1019 1020 "names", 1020 1021 "num_cpus", 1021 1022 "owo-colors", ··· 1702 1703 1703 1704 [[package]] 1704 1705 name = "libc" 1705 - version = "0.2.174" 1706 + version = "0.2.175" 1706 1707 source = "registry+https://github.com/rust-lang/crates.io-index" 1707 - checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 1708 + checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 1708 1709 1709 1710 [[package]] 1710 1711 name = "libloading"
+1
README.md
··· 68 68 rm Delete the Firecracker MicroVM 69 69 serve Start fireup HTTP API server 70 70 inspect Inspect the Firecracker MicroVM details 71 + exec Execute a command inside the Firecracker MicroVM 71 72 help Print this message or the help of the given subcommand(s) 72 73 73 74 Options:
+1
crates/firecracker-up/Cargo.toml
··· 32 32 chrono = "0.4.42" 33 33 serde_json = "1.0.145" 34 34 colored_json = "5.0.0" 35 + libc = "0.2.175"
+1
crates/firecracker-up/src/cmd/cp.rs
··· 1 +
+48
crates/firecracker-up/src/cmd/exec.rs
··· 1 + use anyhow::{anyhow, Context, Error, Result}; 2 + use firecracker_state::repo; 3 + use owo_colors::OwoColorize; 4 + use std::fs; 5 + 6 + use crate::command::run_ssh_command; 7 + 8 + pub async fn exec(name: &str, args: Vec<String>) -> Result<(), Error> { 9 + let pool = firecracker_state::create_connection_pool().await?; 10 + let vm = repo::virtual_machine::find(&pool, name).await?; 11 + 12 + if vm.is_none() { 13 + println!("[-] MicroVM '{}' not found.", name); 14 + std::process::exit(1); 15 + } 16 + 17 + if !firecracker_process::vm_is_running(name).await? { 18 + println!("[-] MicroVM '{}' is not running.", name); 19 + let start_cmd = format!("fireup start {}", name); 20 + println!(" Start it with {}", start_cmd.cyan()); 21 + std::process::exit(1); 22 + } 23 + 24 + let guest_ip = format!("{}.firecracker", name); 25 + run_ssh_command(&get_private_key_path()?, &guest_ip, args.join(" ").as_str())?; 26 + 27 + Ok(()) 28 + } 29 + 30 + fn get_private_key_path() -> Result<String, Error> { 31 + let home_dir = dirs::home_dir().ok_or_else(|| anyhow!("Failed to get home directory"))?; 32 + let app_dir = format!("{}/.fireup", home_dir.display()); 33 + let key_name = glob::glob(format!("{}/id_rsa", app_dir).as_str()) 34 + .with_context(|| "Failed to glob ssh key files")? 35 + .last() 36 + .ok_or_else(|| anyhow!("No SSH key file found"))? 37 + .with_context(|| "Failed to get SSH key path")?; 38 + let key_name = fs::canonicalize(&key_name) 39 + .with_context(|| { 40 + format!( 41 + "Failed to resolve absolute path for SSH key: {}", 42 + key_name.display() 43 + ) 44 + })? 45 + .display() 46 + .to_string(); 47 + Ok(key_name) 48 + }
+2
crates/firecracker-up/src/cmd/mod.rs
··· 1 + pub mod cp; 1 2 pub mod down; 3 + pub mod exec; 2 4 pub mod init; 3 5 pub mod inspect; 4 6 pub mod logs;
+71
crates/firecracker-up/src/command.rs
··· 1 1 use anyhow::{anyhow, Context, Result}; 2 2 use std::process::{Command, Output, Stdio}; 3 3 4 + pub fn has_sudo() -> bool { 5 + Command::new("sudo") 6 + .arg("-h") 7 + .output() 8 + .map(|output| output.status.success()) 9 + .unwrap_or(false) 10 + } 11 + 12 + pub fn is_root() -> bool { 13 + unsafe { libc::getuid() == 0 } 14 + } 15 + 4 16 pub fn run_command(command: &str, args: &[&str], with_stdin: bool) -> Result<Output> { 5 17 let mut cmd = Command::new(command); 6 18 ··· 34 46 } 35 47 Ok(output) 36 48 } 49 + 50 + pub fn run_command_with_stdout_inherit(command: &str, args: &[&str], use_sudo: bool) -> Result<()> { 51 + let mut cmd = if use_sudo { 52 + if !has_sudo() && !is_root() { 53 + return Err(anyhow!( 54 + "sudo is required for command '{}', but not available", 55 + command 56 + )); 57 + } 58 + let mut c = Command::new("sudo"); 59 + c.arg(command); 60 + 61 + match is_root() { 62 + true => Command::new(command), 63 + false => c, 64 + } 65 + } else { 66 + Command::new(command) 67 + }; 68 + 69 + let mut child = cmd 70 + .args(args) 71 + .stdin(Stdio::inherit()) 72 + .stderr(Stdio::inherit()) 73 + .stdout(Stdio::inherit()) 74 + .spawn() 75 + .with_context(|| format!("Failed to execute {}", command))?; 76 + 77 + let status = child.wait()?; 78 + 79 + if !status.success() { 80 + return Err(anyhow!( 81 + "Command {} failed with status: {}", 82 + command, 83 + status 84 + )); 85 + } 86 + 87 + Ok(()) 88 + } 89 + 90 + pub fn run_ssh_command(key_path: &str, guest_ip: &str, command: &str) -> Result<()> { 91 + run_command_with_stdout_inherit( 92 + "ssh", 93 + &[ 94 + "-q", 95 + "-i", 96 + key_path, 97 + "-o", 98 + "StrictHostKeyChecking=no", 99 + "-o", 100 + "UserKnownHostsFile=/dev/null", 101 + &format!("root@{}", guest_ip), 102 + command, 103 + ], 104 + false, 105 + )?; 106 + Ok(()) 107 + }
+23
crates/firecracker-up/src/main.rs
··· 166 166 .arg(arg!(<name> "Name or ID of the Firecracker MicroVM to inspect").required(true)) 167 167 .about("Inspect the Firecracker MicroVM details"), 168 168 ) 169 + .subcommand( 170 + Command::new("exec") 171 + .arg( 172 + arg!(<name> "Name of the Firecracker MicroVM to execute command in") 173 + .required(true), 174 + ) 175 + .arg( 176 + Arg::new("args") 177 + .help("Command and arguments to execute inside the MicroVM") 178 + .required(true) 179 + .num_args(1..), 180 + ) 181 + .about("Execute a command inside the Firecracker MicroVM"), 182 + ) 169 183 .arg(arg!(--debian "Prepare Debian MicroVM").default_value("false")) 170 184 .arg(arg!(--alpine "Prepare Alpine MicroVM").default_value("false")) 171 185 .arg(arg!(--nixos "Prepare NixOS MicroVM").default_value("false")) ··· 340 354 Some(("inspect", args)) => { 341 355 let name = args.get_one::<String>("name").cloned().unwrap(); 342 356 inspect_microvm(&name).await?; 357 + } 358 + Some(("exec", args)) => { 359 + let name = args.get_one::<String>("name").cloned().unwrap(); 360 + let cmd_args: Vec<String> = args 361 + .get_many::<String>("args") 362 + .unwrap() 363 + .map(|s| s.to_string()) 364 + .collect(); 365 + cmd::exec::exec(&name, cmd_args).await?; 343 366 } 344 367 _ => { 345 368 let debian = matches.get_one::<bool>("debian").copied().unwrap_or(false);
+2
crates/firecracker-vm/src/guest.rs
··· 13 13 key_path, 14 14 "-o", 15 15 "StrictHostKeyChecking=no", 16 + "-o", 17 + "UserKnownHostsFile=/dev/null", 16 18 &format!("root@{}", guest_ip), 17 19 &format!("echo 'nameserver {}' > /etc/resolv.conf", BRIDGE_IP), 18 20 ],
+2
crates/firecracker-vm/src/tailscale.rs
··· 88 88 key_path, 89 89 "-o", 90 90 "StrictHostKeyChecking=no", 91 + "-o", 92 + "UserKnownHostsFile=/dev/null", 91 93 &format!("root@{}", guest_ip), 92 94 command, 93 95 ],