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 "firecracker-state", 1017 "firecracker-vm", 1018 "glob", 1019 "names", 1020 "num_cpus", 1021 "owo-colors", ··· 1702 1703 [[package]] 1704 name = "libc" 1705 - version = "0.2.174" 1706 source = "registry+https://github.com/rust-lang/crates.io-index" 1707 - checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 1708 1709 [[package]] 1710 name = "libloading"
··· 1016 "firecracker-state", 1017 "firecracker-vm", 1018 "glob", 1019 + "libc", 1020 "names", 1021 "num_cpus", 1022 "owo-colors", ··· 1703 1704 [[package]] 1705 name = "libc" 1706 + version = "0.2.175" 1707 source = "registry+https://github.com/rust-lang/crates.io-index" 1708 + checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 1709 1710 [[package]] 1711 name = "libloading"
+1
README.md
··· 68 rm Delete the Firecracker MicroVM 69 serve Start fireup HTTP API server 70 inspect Inspect the Firecracker MicroVM details 71 help Print this message or the help of the given subcommand(s) 72 73 Options:
··· 68 rm Delete the Firecracker MicroVM 69 serve Start fireup HTTP API server 70 inspect Inspect the Firecracker MicroVM details 71 + exec Execute a command inside the Firecracker MicroVM 72 help Print this message or the help of the given subcommand(s) 73 74 Options:
+1
crates/firecracker-up/Cargo.toml
··· 32 chrono = "0.4.42" 33 serde_json = "1.0.145" 34 colored_json = "5.0.0"
··· 32 chrono = "0.4.42" 33 serde_json = "1.0.145" 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 down; 2 pub mod init; 3 pub mod inspect; 4 pub mod logs;
··· 1 + pub mod cp; 2 pub mod down; 3 + pub mod exec; 4 pub mod init; 5 pub mod inspect; 6 pub mod logs;
+71
crates/firecracker-up/src/command.rs
··· 1 use anyhow::{anyhow, Context, Result}; 2 use std::process::{Command, Output, Stdio}; 3 4 pub fn run_command(command: &str, args: &[&str], with_stdin: bool) -> Result<Output> { 5 let mut cmd = Command::new(command); 6 ··· 34 } 35 Ok(output) 36 }
··· 1 use anyhow::{anyhow, Context, Result}; 2 use std::process::{Command, Output, Stdio}; 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 + 16 pub fn run_command(command: &str, args: &[&str], with_stdin: bool) -> Result<Output> { 17 let mut cmd = Command::new(command); 18 ··· 46 } 47 Ok(output) 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 .arg(arg!(<name> "Name or ID of the Firecracker MicroVM to inspect").required(true)) 167 .about("Inspect the Firecracker MicroVM details"), 168 ) 169 .arg(arg!(--debian "Prepare Debian MicroVM").default_value("false")) 170 .arg(arg!(--alpine "Prepare Alpine MicroVM").default_value("false")) 171 .arg(arg!(--nixos "Prepare NixOS MicroVM").default_value("false")) ··· 340 Some(("inspect", args)) => { 341 let name = args.get_one::<String>("name").cloned().unwrap(); 342 inspect_microvm(&name).await?; 343 } 344 _ => { 345 let debian = matches.get_one::<bool>("debian").copied().unwrap_or(false);
··· 166 .arg(arg!(<name> "Name or ID of the Firecracker MicroVM to inspect").required(true)) 167 .about("Inspect the Firecracker MicroVM details"), 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 + ) 183 .arg(arg!(--debian "Prepare Debian MicroVM").default_value("false")) 184 .arg(arg!(--alpine "Prepare Alpine MicroVM").default_value("false")) 185 .arg(arg!(--nixos "Prepare NixOS MicroVM").default_value("false")) ··· 354 Some(("inspect", args)) => { 355 let name = args.get_one::<String>("name").cloned().unwrap(); 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?; 366 } 367 _ => { 368 let debian = matches.get_one::<bool>("debian").copied().unwrap_or(false);
+2
crates/firecracker-vm/src/guest.rs
··· 13 key_path, 14 "-o", 15 "StrictHostKeyChecking=no", 16 &format!("root@{}", guest_ip), 17 &format!("echo 'nameserver {}' > /etc/resolv.conf", BRIDGE_IP), 18 ],
··· 13 key_path, 14 "-o", 15 "StrictHostKeyChecking=no", 16 + "-o", 17 + "UserKnownHostsFile=/dev/null", 18 &format!("root@{}", guest_ip), 19 &format!("echo 'nameserver {}' > /etc/resolv.conf", BRIDGE_IP), 20 ],
+2
crates/firecracker-vm/src/tailscale.rs
··· 88 key_path, 89 "-o", 90 "StrictHostKeyChecking=no", 91 &format!("root@{}", guest_ip), 92 command, 93 ],
··· 88 key_path, 89 "-o", 90 "StrictHostKeyChecking=no", 91 + "-o", 92 + "UserKnownHostsFile=/dev/null", 93 &format!("root@{}", guest_ip), 94 command, 95 ],