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

feat: Add IP address and boot configuration fields to virtual machines

- Added `ip_address`, `vmlinux`, `rootfs`, and `bootargs` columns to the `virtual_machines` table.
- Implemented migration scripts for adding new columns and ensuring unique constraints on `mac_address`, `pid`, and `tap`.
- Updated the `VirtualMachine` struct to include new fields.
- Modified repository functions to handle new fields in create, update, and find operations.
- Enhanced the `ps` command to display the new IP address field.
- Introduced new commands for starting, stopping, and restarting virtual machines.
- Updated the `up` command to handle existing VMs and their configurations.
- Implemented a mechanism to wait for the VM to obtain an IP address and store it in the database.
- Refactored network setup to create new tap devices if they do not exist.
- Added utility functions for formatting time durations for better user feedback.

+666 -135
+6 -11
Cargo.lock
··· 33 33 checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 34 34 35 35 [[package]] 36 - name = "android-tzdata" 37 - version = "0.1.1" 38 - source = "registry+https://github.com/rust-lang/crates.io-index" 39 - checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 40 - 41 - [[package]] 42 36 name = "android_system_properties" 43 37 version = "0.1.5" 44 38 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 269 263 270 264 [[package]] 271 265 name = "chrono" 272 - version = "0.4.41" 266 + version = "0.4.42" 273 267 source = "registry+https://github.com/rust-lang/crates.io-index" 274 - checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 268 + checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" 275 269 dependencies = [ 276 - "android-tzdata", 277 270 "iana-time-zone", 278 271 "js-sys", 279 272 "num-traits", 280 273 "serde", 281 274 "wasm-bindgen", 282 - "windows-link 0.1.3", 275 + "windows-link 0.2.0", 283 276 ] 284 277 285 278 [[package]] ··· 504 497 "libc", 505 498 "option-ext", 506 499 "redox_users", 507 - "windows-sys 0.59.0", 500 + "windows-sys 0.61.0", 508 501 ] 509 502 510 503 [[package]] ··· 597 590 version = "0.1.0" 598 591 dependencies = [ 599 592 "anyhow", 593 + "firecracker-state", 600 594 "firecracker-vm", 601 595 "libc", 602 596 "owo-colors", ··· 641 635 version = "0.4.1" 642 636 dependencies = [ 643 637 "anyhow", 638 + "chrono", 644 639 "clap 4.5.41", 645 640 "dirs", 646 641 "fire-config",
+1
crates/firecracker-process/Cargo.toml
··· 11 11 libc = "0.2.174" 12 12 owo-colors = "4.2.2" 13 13 firecracker-vm = { path = "../firecracker-vm" } 14 + firecracker-state = { path = "../firecracker-state" }
+82 -9
crates/firecracker-process/src/lib.rs
··· 1 + use std::process; 2 + 1 3 use anyhow::Result; 4 + use firecracker_state::repo; 2 5 use firecracker_vm::types::VmOptions; 6 + use owo_colors::OwoColorize; 3 7 4 8 use crate::command::{run_command, run_command_in_background}; 5 9 6 10 pub mod command; 7 11 8 - pub fn start(config: &VmOptions) -> Result<u32> { 9 - stop(config)?; 12 + pub async fn start(config: &VmOptions) -> Result<u32> { 13 + let name = config 14 + .api_socket 15 + .trim_start_matches("/tmp/firecracker-") 16 + .trim_end_matches(".sock") 17 + .to_string(); 18 + 19 + stop(Some(name)).await?; 10 20 println!("[+] Starting Firecracker..."); 11 21 let pid = run_command_in_background("firecracker", &["--api-sock", &config.api_socket], true)?; 12 22 Ok(pid) 13 23 } 14 24 15 - pub fn stop(config: &VmOptions) -> Result<()> { 16 - if !is_running() { 17 - println!("[!] Firecracker is not running."); 18 - run_command("rm", &["-rf", &config.api_socket], true)?; 25 + pub async fn stop(name: Option<String>) -> Result<()> { 26 + if name.is_none() { 27 + return stop_all().await; 28 + } 29 + 30 + let name = name.unwrap(); 31 + 32 + if !vm_is_running(&name).await? { 33 + println!("[!] {} is not running.", name.cyan()); 19 34 return Ok(()); 20 35 } 21 - run_command("killall", &["-s", "KILL", "firecracker"], true)?; 36 + 37 + let config = VmOptions { 38 + api_socket: format!("/tmp/firecracker-{}.sock", name), 39 + ..Default::default() 40 + }; 41 + 42 + let pool = firecracker_state::create_connection_pool().await?; 43 + 44 + let vm = repo::virtual_machine::find(&pool, &name).await?; 45 + if vm.is_none() { 46 + println!( 47 + "[!] No virtual machine found with name or id '{}'.", 48 + name.cyan() 49 + ); 50 + process::exit(1); 51 + } 52 + 53 + let vm = vm.unwrap(); 54 + if let Some(pid) = vm.pid { 55 + run_command("kill", &["-s", "KILL", &pid.to_string()], true)?; 56 + } 57 + 22 58 run_command("rm", &["-rf", &config.api_socket], true)?; 23 - println!("[+] Firecracker has been stopped."); 59 + println!("[+] {} has been stopped.", name.cyan()); 60 + 61 + repo::virtual_machine::update_status(&pool, &name, "STOPPED").await?; 62 + 24 63 Ok(()) 25 64 } 26 65 66 + pub async fn vm_is_running(name: &str) -> Result<bool> { 67 + let pool = firecracker_state::create_connection_pool().await?; 68 + let vm = repo::virtual_machine::find(&pool, name).await?; 69 + 70 + if let Some(vm) = vm { 71 + if std::path::Path::new(&vm.api_socket).exists() { 72 + return Ok(true); 73 + } 74 + if vm.status == "RUNNING" { 75 + return Ok(true); 76 + } 77 + } 78 + 79 + Ok(false) 80 + } 81 + 27 82 pub fn is_running() -> bool { 28 - run_command("pgrep", &["firecracker"], false).is_ok() 83 + match run_command("pgrep", &["-x", "firecracker"], false) { 84 + Ok(output) => output.status.success(), 85 + Err(_) => false, 86 + } 87 + } 88 + 89 + pub async fn stop_all() -> Result<()> { 90 + if !is_running() { 91 + println!("[!] No Firecracker process is running."); 92 + return Ok(()); 93 + } 94 + 95 + run_command("pkill", &["-x", "firecracker"], true)?; 96 + run_command("bash", &["-c", "rm -rf /tmp/firecracker-*.sock"], true)?; 97 + println!("[+] All Firecracker processes have been stopped."); 98 + 99 + let pool = firecracker_state::create_connection_pool().await?; 100 + repo::virtual_machine::update_all_status(&pool, "STOPPED").await?; 101 + Ok(()) 29 102 }
+3
crates/firecracker-state/migrations/20250910164344_ip_address.sql
··· 1 + -- Add migration script here 2 + ALTER TABLE virtual_machines 3 + ADD COLUMN ip_address VARCHAR(255);
+9
crates/firecracker-state/migrations/20250910202353_add_vmlinux_rootfs_bootargs.sql
··· 1 + -- Add migration script here 2 + ALTER TABLE virtual_machines 3 + ADD COLUMN vmlinux VARCHAR(255); 4 + 5 + ALTER TABLE virtual_machines 6 + ADD COLUMN rootfs VARCHAR(255); 7 + 8 + ALTER TABLE virtual_machines 9 + ADD COLUMN bootargs TEXT;
+4
crates/firecracker-state/migrations/20250911084132_ensure_unique_columns.sql
··· 1 + -- Add migration script here 2 + CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_mac_address ON virtual_machines (mac_address); 3 + CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_pid ON virtual_machines (pid); 4 + CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_tap ON virtual_machines (tap);
+4
crates/firecracker-state/src/entity/virtual_machine.rs
··· 15 15 pub tap: String, 16 16 pub api_socket: String, 17 17 pub project_dir: Option<String>, 18 + pub ip_address: Option<String>, 19 + pub vmlinux: Option<String>, 20 + pub rootfs: Option<String>, 21 + pub bootargs: Option<String>, 18 22 #[serde(with = "chrono::serde::ts_seconds")] 19 23 pub created_at: DateTime<Utc>, 20 24 #[serde(with = "chrono::serde::ts_seconds")]
+36
crates/firecracker-state/src/lib.rs
··· 16 16 pool.execute(include_str!("../migrations/20250804092946_init.sql")) 17 17 .await?; 18 18 19 + match pool 20 + .execute(include_str!("../migrations/20250910164344_ip_address.sql")) 21 + .await 22 + { 23 + Ok(_) => (), 24 + Err(e) => { 25 + if e.to_string().contains("duplicate column name: ip_address") { 26 + } else { 27 + return Err(anyhow!("Failed to apply migration: {}", e)); 28 + } 29 + } 30 + } 31 + 32 + match pool 33 + .execute(include_str!( 34 + "../migrations/20250910202353_add_vmlinux_rootfs_bootargs.sql" 35 + )) 36 + .await 37 + { 38 + Ok(_) => (), 39 + Err(e) => { 40 + if e.to_string().contains("duplicate column name: vmlinux") 41 + || e.to_string().contains("duplicate column name: rootfs") 42 + || e.to_string().contains("duplicate column name: bootargs") 43 + { 44 + } else { 45 + return Err(anyhow!("Failed to apply migration: {}", e)); 46 + } 47 + } 48 + } 49 + 50 + pool.execute(include_str!( 51 + "../migrations/20250911084132_ensure_unique_columns.sql" 52 + )) 53 + .await?; 54 + 19 55 sqlx::query("PRAGMA journal_mode=WAL") 20 56 .execute(&pool) 21 57 .await?;
+104 -23
crates/firecracker-state/src/repo/virtual_machine.rs
··· 5 5 6 6 use crate::entity::virtual_machine::VirtualMachine; 7 7 8 - pub async fn all(pool: Pool<Sqlite>) -> Result<Vec<VirtualMachine>, Error> { 8 + pub async fn all(pool: &Pool<Sqlite>) -> Result<Vec<VirtualMachine>, Error> { 9 9 let result: Vec<VirtualMachine> = sqlx::query_as("SELECT * FROM virtual_machines") 10 - .fetch_all(&pool) 10 + .fetch_all(pool) 11 11 .await 12 12 .with_context(|| "Failed to fetch virtual machines")?; 13 13 Ok(result) 14 14 } 15 15 16 - pub async fn find(pool: Pool<Sqlite>, name: &str) -> Result<Option<VirtualMachine>, Error> { 16 + pub async fn find(pool: &Pool<Sqlite>, name: &str) -> Result<Option<VirtualMachine>, Error> { 17 17 let result: Option<VirtualMachine> = 18 18 sqlx::query_as("SELECT * FROM virtual_machines WHERE name = ? OR id = ?") 19 19 .bind(name) 20 - .fetch_optional(&pool) 20 + .fetch_optional(pool) 21 21 .await 22 22 .with_context(|| { 23 23 format!("Failed to find virtual machine with name or id '{}'", name) ··· 26 26 } 27 27 28 28 pub async fn find_by_project_dir( 29 - pool: Pool<Sqlite>, 29 + pool: &Pool<Sqlite>, 30 30 path: &str, 31 31 ) -> Result<Option<VirtualMachine>, Error> { 32 32 let result: Option<VirtualMachine> = 33 33 sqlx::query_as("SELECT * FROM virtual_machines WHERE project_dir = ?") 34 34 .bind(path) 35 - .fetch_optional(&pool) 35 + .fetch_optional(pool) 36 36 .await 37 37 .with_context(|| { 38 38 format!("Failed to find virtual machine with project_dir '{}'", path) ··· 40 40 Ok(result) 41 41 } 42 42 43 - pub async fn create(pool: Pool<Sqlite>, vm: VirtualMachine) -> Result<(), Error> { 43 + pub async fn create(pool: &Pool<Sqlite>, vm: VirtualMachine) -> Result<(), Error> { 44 44 let id = xid::new().to_string(); 45 45 let project_dir = match Path::exists(Path::new("fire.toml")) { 46 46 true => Some(std::env::current_dir()?.display().to_string()), ··· 59 59 memory, 60 60 distro, 61 61 pid, 62 - status 63 - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 62 + status, 63 + ip_address, 64 + vmlinux, 65 + rootfs, 66 + bootargs 67 + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 64 68 ) 65 69 .bind(&vm.name) 66 70 .bind(&id) ··· 74 78 .bind(&vm.distro) 75 79 .bind(&vm.pid) 76 80 .bind("RUNNING") 77 - .execute(&pool) 81 + .bind(&vm.ip_address) 82 + .bind(&vm.vmlinux) 83 + .bind(&vm.rootfs) 84 + .bind(&vm.bootargs) 85 + .execute(pool) 78 86 .await 79 87 .with_context(|| "Failed to create virtual machine")?; 80 88 Ok(()) 81 89 } 82 90 83 - pub async fn delete(pool: Pool<Sqlite>, name: &str) -> Result<(), Error> { 91 + pub async fn delete(pool: &Pool<Sqlite>, name: &str) -> Result<(), Error> { 84 92 sqlx::query("DELETE FROM virtual_machines WHERE name = ? OR id = ?") 85 93 .bind(name) 86 94 .bind(name) 87 - .execute(&pool) 95 + .execute(pool) 88 96 .await 89 97 .with_context(|| { 90 98 format!( ··· 95 103 Ok(()) 96 104 } 97 105 98 - pub async fn update(pool: Pool<Sqlite>, vm: VirtualMachine, status: &str) -> Result<(), Error> { 99 - sqlx::query("UPDATE virtual_machines SET project_dir = ?, bridge = ?, tap = ?, api_socket = ?, mac_address = ? WHERE name = ? OR id = ?") 100 - .bind(&vm.project_dir) 101 - .bind(&vm.bridge) 102 - .bind(&vm.tap) 103 - .bind(&vm.api_socket) 104 - .bind(&vm.mac_address) 105 - .bind(&vm.name) 106 - .bind(&vm.id) 106 + pub async fn update(pool: &Pool<Sqlite>, id: &str, vm: VirtualMachine) -> Result<(), Error> { 107 + sqlx::query( 108 + r#" 109 + UPDATE virtual_machines 110 + SET project_dir = ?, 111 + bridge = ?, 112 + tap = ?, 113 + api_socket = ?, 114 + mac_address = ?, 115 + status = ?, 116 + pid = ?, 117 + ip_address = ?, 118 + vcpu = ?, 119 + memory = ?, 120 + distro = ?, 121 + vmlinux = ?, 122 + rootfs = ?, 123 + bootargs = ? 124 + WHERE id = ?"#, 125 + ) 126 + .bind(&vm.project_dir) 127 + .bind(&vm.bridge) 128 + .bind(&vm.tap) 129 + .bind(&vm.api_socket) 130 + .bind(&vm.mac_address) 131 + .bind(&vm.status) 132 + .bind(&vm.pid) 133 + .bind(&vm.ip_address) 134 + .bind(&vm.vcpu) 135 + .bind(&vm.memory) 136 + .bind(&vm.distro) 137 + .bind(&vm.vmlinux) 138 + .bind(&vm.rootfs) 139 + .bind(&vm.bootargs) 140 + .bind(id) 141 + .execute(pool) 142 + .await 143 + .with_context(|| { 144 + format!( 145 + "Failed to update virtual machine with name or id '{}'", 146 + vm.name 147 + ) 148 + })?; 149 + Ok(()) 150 + } 151 + 152 + pub async fn update_status(pool: &Pool<Sqlite>, name: &str, status: &str) -> Result<(), Error> { 153 + sqlx::query("UPDATE virtual_machines SET status = ? WHERE name = ? OR id = ?") 154 + .bind(status) 155 + .bind(name) 156 + .bind(name) 157 + .execute(pool) 158 + .await 159 + .with_context(|| { 160 + format!( 161 + "Failed to update status for virtual machine with name or id '{}'", 162 + name 163 + ) 164 + })?; 165 + Ok(()) 166 + } 167 + 168 + pub async fn update_all_status(pool: &Pool<Sqlite>, status: &str) -> Result<(), Error> { 169 + sqlx::query("UPDATE virtual_machines SET status = ?") 107 170 .bind(status) 108 - .execute(&pool) 171 + .execute(pool) 109 172 .await 110 - .with_context(|| format!("Failed to update virtual machine with name or id '{}'", vm.name))?; 173 + .with_context(|| "Failed to update status for all virtual machines")?; 111 174 Ok(()) 112 175 } 176 + 177 + pub async fn find_by_api_socket( 178 + pool: &Pool<Sqlite>, 179 + api_socket: &str, 180 + ) -> Result<Option<VirtualMachine>, Error> { 181 + let result: Option<VirtualMachine> = 182 + sqlx::query_as("SELECT * FROM virtual_machines WHERE api_socket = ?") 183 + .bind(api_socket) 184 + .fetch_optional(pool) 185 + .await 186 + .with_context(|| { 187 + format!( 188 + "Failed to find virtual machine with api_socket '{}'", 189 + api_socket 190 + ) 191 + })?; 192 + Ok(result) 193 + }
+1
crates/firecracker-up/Cargo.toml
··· 28 28 "derive", 29 29 "macros", 30 30 ] } 31 + chrono = "0.4.42"
+2 -3
crates/firecracker-up/src/cmd/down.rs
··· 1 1 use anyhow::Error; 2 - use firecracker_vm::types::VmOptions; 3 2 4 - pub fn down(options: &VmOptions) -> Result<(), Error> { 5 - firecracker_process::stop(options)?; 3 + pub async fn down() -> Result<(), Error> { 4 + firecracker_process::stop(None).await?; 6 5 Ok(()) 7 6 }
+2
crates/firecracker-up/src/cmd/mod.rs
··· 4 4 pub mod ps; 5 5 pub mod reset; 6 6 pub mod ssh; 7 + pub mod start; 7 8 pub mod status; 9 + pub mod stop; 8 10 pub mod up;
+35 -1
crates/firecracker-up/src/cmd/ps.rs
··· 1 1 use anyhow::Error; 2 2 3 - pub fn list_all_running_instances() -> Result<(), Error> { 3 + use crate::date::format_duration_ago; 4 + 5 + pub async fn list_all_instances(all: bool) -> Result<(), Error> { 6 + let pool = firecracker_state::create_connection_pool().await?; 7 + let mut vms = firecracker_state::repo::virtual_machine::all(&pool).await?; 8 + if !all { 9 + vms = vms 10 + .into_iter() 11 + .filter(|vm| vm.status == "RUNNING") 12 + .collect::<Vec<_>>(); 13 + } 14 + 15 + if vms.is_empty() { 16 + println!("No Firecracker MicroVM instances found."); 17 + return Ok(()); 18 + } 19 + 20 + println!( 21 + "{:<20} {:<10} {:<5} {:<10} {:<15} {:<10} {:<15} {:<10}", 22 + "NAME", "DISTRO", "VCPU", "MEMORY", "STATUS", "PID", "IP", "CREATED" 23 + ); 24 + for vm in vms { 25 + println!( 26 + "{:<20} {:<10} {:<5} {:<10} {:<15} {:<10} {:<15} {:<10}", 27 + vm.name, 28 + vm.distro, 29 + vm.vcpu, 30 + format!("{} MiB", vm.memory), 31 + vm.status, 32 + vm.pid.unwrap_or(0), 33 + vm.ip_address.unwrap_or_default(), 34 + format_duration_ago(vm.created_at), 35 + ); 36 + } 37 + 4 38 Ok(()) 5 39 }
+9 -4
crates/firecracker-up/src/cmd/reset.rs
··· 1 1 use anyhow::Error; 2 + use firecracker_process::stop; 2 3 use firecracker_vm::types::VmOptions; 3 4 use glob::glob; 4 5 use owo_colors::OwoColorize; 5 6 6 - use crate::cmd::down::down; 7 - 8 - pub fn reset(options: VmOptions) -> Result<(), Error> { 7 + pub async fn reset(options: VmOptions) -> Result<(), Error> { 9 8 println!( 10 9 "Are you sure you want to reset? This will remove all ext4 files. Type '{}' to confirm:", 11 10 "yes".bright_green() ··· 21 20 return Ok(()); 22 21 } 23 22 24 - down(&options)?; 23 + let name = options 24 + .api_socket 25 + .trim_start_matches("/tmp/firecracker-") 26 + .trim_end_matches(".sock") 27 + .to_string(); 28 + 29 + stop(Some(name)).await?; 25 30 26 31 let app_dir = crate::config::get_config_dir()?; 27 32 let ext4_file = glob(format!("{}/*.ext4", app_dir).as_str())
+1 -1
crates/firecracker-up/src/cmd/ssh.rs
··· 12 12 .map_err(|e| Error::msg(format!("Failed to get current directory: {}", e)))? 13 13 .display() 14 14 .to_string(); 15 - let vm = repo::virtual_machine::find_by_project_dir(pool, &current_dir).await?; 15 + let vm = repo::virtual_machine::find_by_project_dir(&pool, &current_dir).await?; 16 16 match vm { 17 17 Some(vm) => format!("{}.firecracker", vm.name), 18 18 None => {
+37
crates/firecracker-up/src/cmd/start.rs
··· 1 + use std::process; 2 + 3 + use anyhow::Error; 4 + use firecracker_state::repo; 5 + use firecracker_vm::types::VmOptions; 6 + 7 + use crate::cmd::up::up; 8 + 9 + pub async fn start(name: &str) -> Result<(), Error> { 10 + let pool = firecracker_state::create_connection_pool().await?; 11 + let vm = repo::virtual_machine::find(&pool, name).await?; 12 + if vm.is_none() { 13 + println!("[!] No virtual machine found with the name: {}", name); 14 + process::exit(1); 15 + } 16 + 17 + let vm = vm.unwrap(); 18 + 19 + up(VmOptions { 20 + debian: Some(vm.distro == "debian"), 21 + alpine: Some(vm.distro == "alpine"), 22 + ubuntu: Some(vm.distro == "ubuntu"), 23 + nixos: Some(vm.distro == "nixos"), 24 + vcpu: vm.vcpu, 25 + memory: vm.memory, 26 + vmlinux: vm.vmlinux, 27 + rootfs: vm.rootfs, 28 + bootargs: vm.bootargs, 29 + bridge: vm.bridge, 30 + tap: vm.tap, 31 + api_socket: vm.api_socket, 32 + mac_address: vm.mac_address, 33 + }) 34 + .await?; 35 + 36 + Ok(()) 37 + }
+26 -11
crates/firecracker-up/src/cmd/status.rs
··· 1 1 use anyhow::Error; 2 2 use owo_colors::OwoColorize; 3 3 4 - pub fn status() -> Result<(), Error> { 5 - if firecracker_process::is_running() { 6 - println!( 7 - "Firecracker MicroVM is running. {}", 8 - "[✓] RUNNING".bright_green() 9 - ); 10 - return Ok(()); 4 + pub async fn status(name: Option<String>) -> Result<(), Error> { 5 + match name { 6 + Some(name) => { 7 + if firecracker_process::vm_is_running(&name).await? { 8 + println!( 9 + "{} is running. {}", 10 + name.cyan(), 11 + "[✓] RUNNING".bright_green() 12 + ); 13 + return Ok(()); 14 + } 15 + 16 + println!( 17 + "{} is not running. {}", 18 + name.cyan(), 19 + "[✗] STOPPED".bright_red() 20 + ); 21 + } 22 + None => { 23 + if firecracker_process::is_running() { 24 + println!("Firecracker is running. {}", "[✓] RUNNING".bright_green()); 25 + return Ok(()); 26 + } 27 + 28 + println!("Firecracker is not running. {}", "[✗] STOPPED".bright_red()); 29 + } 11 30 } 12 31 13 - println!( 14 - "Firecracker MicroVM is not running. {}", 15 - "[✗] STOPPED".bright_red() 16 - ); 17 32 Ok(()) 18 33 }
+6
crates/firecracker-up/src/cmd/stop.rs
··· 1 + use anyhow::Error; 2 + 3 + pub async fn stop(name: &str) -> Result<(), Error> { 4 + firecracker_process::stop(Some(name.to_string())).await?; 5 + Ok(()) 6 + }
+57 -4
crates/firecracker-up/src/cmd/up.rs
··· 1 - use std::thread; 1 + use std::{process, thread}; 2 2 3 3 use anyhow::Error; 4 4 use fire_config::read_config; 5 + use firecracker_state::repo; 5 6 use firecracker_vm::types::VmOptions; 6 7 use owo_colors::OwoColorize; 7 8 ··· 10 11 pub async fn up(options: VmOptions) -> Result<(), Error> { 11 12 check_kvm_support()?; 12 13 13 - let options = match read_config() { 14 + let mut options = match read_config() { 14 15 Ok(config) => VmOptions::from(config), 15 16 Err(_) => options.clone(), 16 17 }; 17 18 18 - let pid = firecracker_process::start(&options)?; 19 + let current_dir = std::env::current_dir()?; 20 + let fire_toml = current_dir.join("fire.toml"); 21 + let mut vm_id = None; 22 + let pool = firecracker_state::create_connection_pool().await?; 23 + let vm = repo::virtual_machine::find_by_api_socket(&pool, &options.api_socket).await?; 24 + 25 + if let Some(vm) = vm { 26 + vm_id = Some(vm.id.clone()); 27 + } 28 + 29 + if fire_toml.exists() { 30 + let vm = 31 + repo::virtual_machine::find_by_project_dir(&pool, &current_dir.display().to_string()) 32 + .await?; 33 + 34 + if let Some(vm) = vm { 35 + options.api_socket = vm.api_socket.clone(); 36 + vm_id = Some(vm.id.clone()); 37 + } 38 + } 39 + 40 + let vms = repo::virtual_machine::all(&pool).await?; 41 + if options.tap.is_empty() { 42 + let vms = vms 43 + .into_iter() 44 + .filter(|vm| vm.tap.starts_with("tap")) 45 + .collect::<Vec<_>>(); 46 + options.tap = format!("tap{}", vms.len()); 47 + 48 + while vms.iter().any(|vm| vm.tap == options.tap) { 49 + let tap_num: u32 = options 50 + .tap 51 + .trim_start_matches("tap") 52 + .parse::<u32>() 53 + .unwrap_or(0) 54 + .checked_add(1) 55 + .unwrap_or(0); 56 + options.tap = format!("tap{}", tap_num); 57 + } 58 + } else { 59 + if vms 60 + .iter() 61 + .any(|vm| vm.tap == options.tap && vm.api_socket != options.api_socket) 62 + { 63 + println!( 64 + "[!] Tap device name {} is already in use. Please choose a different name.", 65 + options.tap.cyan() 66 + ); 67 + process::exit(1); 68 + } 69 + } 70 + 71 + let pid = firecracker_process::start(&options).await?; 19 72 20 73 loop { 21 74 thread::sleep(std::time::Duration::from_secs(1)); ··· 26 79 } 27 80 28 81 firecracker_prepare::prepare(options.clone().into())?; 29 - firecracker_vm::setup(&options, pid).await?; 82 + firecracker_vm::setup(&options, pid, vm_id).await?; 30 83 Ok(()) 31 84 } 32 85
+38
crates/firecracker-up/src/date.rs
··· 1 + use chrono::{DateTime, Utc}; 2 + 3 + pub fn format_duration_ago(created: DateTime<Utc>) -> String { 4 + let now = Utc::now(); 5 + let duration = now - created; 6 + 7 + let seconds = duration.num_seconds(); 8 + let minutes = duration.num_minutes(); 9 + let hours = duration.num_hours(); 10 + let days = duration.num_days(); 11 + let weeks = duration.num_weeks(); 12 + let months = days / 30; // Approximate months 13 + let years = days / 365; // Approximate years 14 + 15 + if years > 0 { 16 + format!("{} year{} ago", years, if years == 1 { "" } else { "s" }) 17 + } else if months > 0 { 18 + format!("{} month{} ago", months, if months == 1 { "" } else { "s" }) 19 + } else if weeks > 0 { 20 + format!("{} week{} ago", weeks, if weeks == 1 { "" } else { "s" }) 21 + } else if days > 0 { 22 + format!("{} day{} ago", days, if days == 1 { "" } else { "s" }) 23 + } else if hours > 0 { 24 + format!("{} hour{} ago", hours, if hours == 1 { "" } else { "s" }) 25 + } else if minutes > 0 { 26 + format!( 27 + "{} minute{} ago", 28 + minutes, 29 + if minutes == 1 { "" } else { "s" } 30 + ) 31 + } else { 32 + format!( 33 + "{} second{} ago", 34 + seconds, 35 + if seconds == 1 { "" } else { "s" } 36 + ) 37 + } 38 + }
+55 -25
crates/firecracker-up/src/main.rs
··· 1 1 use anyhow::Result; 2 2 use clap::{arg, Arg, Command}; 3 - use firecracker_vm::{ 4 - constants::{BRIDGE_DEV, TAP_DEV}, 5 - mac::generate_unique_mac, 6 - types::VmOptions, 7 - }; 3 + use firecracker_vm::{constants::BRIDGE_DEV, mac::generate_unique_mac, types::VmOptions}; 8 4 use owo_colors::OwoColorize; 9 5 10 6 use crate::cmd::{ 11 - down::down, init::init, logs::logs, ps::list_all_running_instances, reset::reset, ssh::ssh, 12 - status::status, up::up, 7 + down::down, init::init, logs::logs, ps::list_all_instances, reset::reset, ssh::ssh, 8 + start::start, status::status, stop::stop, up::up, 13 9 }; 14 10 15 11 pub mod cmd; 16 12 pub mod command; 17 13 pub mod config; 14 + pub mod date; 18 15 19 16 fn cli() -> Command { 20 17 let banner = format!( ··· 36 33 .subcommand(Command::new("init").about( 37 34 "Create a new Firecracker MicroVM configuration `fire.toml` in the current directory", 38 35 )) 39 - .subcommand(Command::new("ps").about("List all running Firecracker MicroVM instances")) 36 + .subcommand( 37 + Command::new("ps") 38 + .alias("list") 39 + .arg(arg!(-a --all "Show all Firecracker MicroVM instances").default_value("false")) 40 + .about("List all Firecracker MicroVM instances"), 41 + ) 42 + .subcommand( 43 + Command::new("start") 44 + .arg(arg!(<name> "Name of the Firecracker MicroVM to start").required(true)) 45 + .about("Start Firecracker MicroVM"), 46 + ) 47 + .subcommand( 48 + Command::new("stop") 49 + .arg(arg!([name] "Name of the Firecracker MicroVM to stop").required(false)) 50 + .about("Stop Firecracker MicroVM"), 51 + ) 52 + .subcommand( 53 + Command::new("restart") 54 + .arg(arg!(<name> "Name of the Firecracker MicroVM to restart").required(true)) 55 + .about("Restart Firecracker MicroVM"), 56 + ) 40 57 .subcommand( 41 58 Command::new("up") 42 59 .arg(arg!(--debian "Prepare Debian MicroVM").default_value("false")) ··· 48 65 .arg(arg!(--vmlinux <path> "Path to the kernel image")) 49 66 .arg(arg!(--rootfs <path> "Path to the root filesystem image")) 50 67 .arg(arg!(--bridge <name> "Name of the bridge interface").default_value(BRIDGE_DEV)) 51 - .arg(arg!(--tap <name> "Name of the tap interface").default_value(TAP_DEV)) 68 + .arg(arg!(--tap <name> "Name of the tap interface").default_value("")) 52 69 .arg( 53 70 Arg::new("mac-address") 54 71 .long("mac-address") ··· 67 84 .value_name("ARGS") 68 85 .help("Override boot arguments"), 69 86 ) 70 - .about("Start Firecracker MicroVM"), 87 + .about("Start a new Firecracker MicroVM"), 71 88 ) 89 + .subcommand(Command::new("down").about("Stop Firecracker MicroVM")) 72 90 .subcommand( 73 - Command::new("down") 74 - .arg(arg!([name] "Name of the Firecracker MicroVM to reset").required(false)) 75 - .about("Stop Firecracker MicroVM"), 91 + Command::new("status") 92 + .arg(arg!([name] "Name of the Firecracker MicroVM to check status").required(false)) 93 + .about("Check the status of Firecracker MicroVM"), 76 94 ) 77 - .subcommand(Command::new("status").about("Check the status of Firecracker MicroVM")) 78 95 .subcommand( 79 96 Command::new("logs") 80 97 .arg( ··· 104 121 .arg(arg!(--vmlinux <path> "Path to the kernel image")) 105 122 .arg(arg!(--rootfs <path> "Path to the root filesystem image")) 106 123 .arg(arg!(--bridge <name> "Name of the bridge interface").default_value(BRIDGE_DEV)) 107 - .arg(arg!(--tap <name> "Name of the tap interface").default_value(TAP_DEV)) 124 + .arg(arg!(--tap <name> "Name of the tap interface").default_value("")) 108 125 .arg( 109 126 Arg::new("mac-address") 110 127 .long("mac-address") ··· 136 153 137 154 match matches.subcommand() { 138 155 Some(("init", _)) => init()?, 139 - Some(("ps", _)) => list_all_running_instances()?, 156 + Some(("ps", args)) => { 157 + let all = args.get_one::<bool>("all").copied().unwrap_or(false); 158 + list_all_instances(all).await?; 159 + } 160 + Some(("stop", args)) => { 161 + let name = args.get_one::<String>("name").cloned().unwrap(); 162 + stop(&name).await?; 163 + } 164 + Some(("start", args)) => { 165 + let name = args.get_one::<String>("name").cloned().unwrap(); 166 + start(&name).await?; 167 + } 168 + Some(("restart", args)) => { 169 + let name = args.get_one::<String>("name").cloned().unwrap(); 170 + stop(&name).await?; 171 + start(&name).await?; 172 + } 140 173 Some(("up", args)) => { 141 174 let vcpu = matches 142 175 .get_one::<String>("vcpu") ··· 176 209 }; 177 210 up(options).await? 178 211 } 179 - Some(("down", args)) => { 180 - let name = args.get_one::<String>("name").cloned().unwrap(); 181 - let api_socket = format!("/tmp/firecracker-{}.sock", name); 182 - down(&VmOptions { 183 - api_socket, 184 - ..Default::default() 185 - })? 212 + Some(("down", _)) => down().await?, 213 + Some(("status", args)) => { 214 + let name = args.get_one::<String>("name").cloned(); 215 + status(name).await?; 186 216 } 187 - Some(("status", _)) => status()?, 188 217 Some(("logs", args)) => { 189 218 let follow = args.get_one::<bool>("follow").copied().unwrap_or(false); 190 219 logs(follow)?; ··· 199 228 reset(VmOptions { 200 229 api_socket, 201 230 ..Default::default() 202 - })? 231 + }) 232 + .await? 203 233 } 204 234 _ => { 205 235 let debian = matches.get_one::<bool>("debian").copied().unwrap_or(false);
-1
crates/firecracker-vm/src/constants.rs
··· 1 - pub const TAP_DEV: &str = "tap0"; 2 1 pub const BRIDGE_DEV: &str = "br0"; 3 2 pub const FIRECRACKER_SOCKET: &str = "/tmp/firecracker.sock"; 4 3 pub const BRIDGE_IP: &str = "172.16.0.1";
+24 -5
crates/firecracker-vm/src/coredns.rs
··· 1 1 use std::{process, thread}; 2 2 3 - use anyhow::Error; 3 + use anyhow::{Context, Error}; 4 + use firecracker_state::repo; 4 5 5 6 use crate::{command::run_command, mqttc, types::VmOptions}; 6 7 ··· 11 12 let api_socket = config.api_socket.clone(); 12 13 thread::spawn(move || { 13 14 let runtime = tokio::runtime::Runtime::new().unwrap(); 14 - runtime.block_on(async { 15 + match runtime.block_on(async { 15 16 println!("[+] Checking if CoreDNS is installed..."); 16 17 if !coredns_is_installed()? { 17 18 // TODO: install it automatically ··· 32 33 .replace("firecracker-", "") 33 34 .replace(".sock", ""); 34 35 35 - let hosts = vec![format!("{} {}.firecracker", ip_addr, name)]; 36 + std::fs::write(format!("/tmp/firecracker-{}.ip", name), ip_addr) 37 + .with_context(|| "Failed to write IP address to file")?; 38 + 39 + let pool = firecracker_state::create_connection_pool().await?; 40 + let vms = repo::virtual_machine::all(&pool).await?; 41 + let mut hosts = vms 42 + .into_iter() 43 + .filter(|vm| vm.ip_address.is_some() && vm.name != name) 44 + .map(|vm| format!("{} {}.firecracker", vm.ip_address.unwrap(), vm.name)) 45 + .collect::<Vec<String>>(); 46 + 47 + hosts.extend(vec![format!("{} {}.firecracker", ip_addr, name)]); 36 48 37 49 let hosts = hosts.join("\n "); 38 50 ··· 49 61 }} 50 62 51 63 ts.net:53 {{ 52 - # Forward non-internal queries (e.g., to Google DNS) 64 + # Forward non-internal queries (e.g., to Tailscale DNS) 53 65 forward . 100.100.100.100 54 66 # Log and errors for debugging 55 67 log ··· 98 110 restart_coredns()?; 99 111 100 112 Ok::<(), Error>(()) 101 - })?; 113 + }) { 114 + Ok(_) => {} 115 + Err(e) => { 116 + eprintln!("[✗] Error setting up CoreDNS: {}", e); 117 + process::exit(1); 118 + } 119 + } 102 120 Ok::<(), Error>(()) 103 121 }); 104 122 ··· 108 126 pub fn restart_coredns() -> Result<(), Error> { 109 127 println!("[+] Starting CoreDNS..."); 110 128 run_command("systemctl", &["enable", "coredns"], true)?; 129 + run_command("systemctl", &["daemon-reload"], true)?; 111 130 run_command("systemctl", &["restart", "coredns"], true)?; 112 131 println!("[✓] CoreDNS started successfully."); 113 132 Ok(())
+7 -2
crates/firecracker-vm/src/firecracker.rs
··· 58 58 Ok(()) 59 59 } 60 60 61 - fn setup_boot_source(kernel: &str, arch: &str, is_nixos: bool, options: &VmOptions) -> Result<()> { 61 + fn setup_boot_source( 62 + kernel: &str, 63 + arch: &str, 64 + is_nixos: bool, 65 + options: &VmOptions, 66 + ) -> Result<String> { 62 67 println!("[+] Setting boot source..."); 63 68 let mut boot_args = "console=ttyS0 reboot=k panic=1 pci=off ip=dhcp".to_string(); 64 69 if arch == "aarch64" { ··· 95 100 ], 96 101 true, 97 102 )?; 98 - Ok(()) 103 + Ok(boot_args) 99 104 } 100 105 101 106 fn setup_rootfs(rootfs: &str, options: &VmOptions) -> Result<()> {
+79 -17
crates/firecracker-vm/src/lib.rs
··· 19 19 mod nextdhcp; 20 20 pub mod types; 21 21 22 - pub async fn setup(options: &VmOptions, pid: u32) -> Result<()> { 22 + pub async fn setup(options: &VmOptions, pid: u32, vm_id: Option<String>) -> Result<()> { 23 23 let distro: Distro = options.clone().into(); 24 24 let app_dir = get_config_dir().with_context(|| "Failed to get configuration directory")?; 25 25 ··· 114 114 Distro::NixOS => "nixos".into(), 115 115 Distro::Ubuntu => "ubuntu".into(), 116 116 }; 117 - repo::virtual_machine::create( 118 - pool, 119 - VirtualMachine { 120 - vcpu: options.vcpu, 121 - memory: options.memory, 122 - api_socket: options.api_socket.clone(), 123 - bridge: options.bridge.clone(), 124 - tap: options.tap.clone(), 125 - mac_address: options.mac_address.clone(), 126 - name: name.clone(), 127 - pid: Some(pid), 128 - distro, 129 - ..Default::default() 130 - }, 131 - ) 132 - .await?; 117 + 118 + let ip_file = format!("/tmp/firecracker-{}.ip", name); 119 + 120 + // loop until the IP file is created 121 + let mut attempts = 0; 122 + while attempts < 30 { 123 + println!("[*] Waiting for VM to obtain an IP address..."); 124 + if fs::metadata(&ip_file).is_ok() { 125 + break; 126 + } 127 + std::thread::sleep(std::time::Duration::from_millis(500)); 128 + attempts += 1; 129 + } 130 + 131 + let ip_addr = fs::read_to_string(&ip_file) 132 + .with_context(|| format!("Failed to read IP address from file: {}", ip_file))? 133 + .trim() 134 + .to_string(); 135 + 136 + fs::remove_file(&ip_file) 137 + .with_context(|| format!("Failed to remove IP address file: {}", ip_file))?; 138 + 139 + let project_dir = match fs::metadata("fire.toml").is_ok() { 140 + true => Some(std::env::current_dir()?.display().to_string()), 141 + false => None, 142 + }; 143 + 144 + match vm_id { 145 + Some(id) => { 146 + repo::virtual_machine::update( 147 + &pool, 148 + &id, 149 + VirtualMachine { 150 + vcpu: options.vcpu, 151 + memory: options.memory, 152 + api_socket: options.api_socket.clone(), 153 + bridge: options.bridge.clone(), 154 + tap: options.tap.clone(), 155 + mac_address: options.mac_address.clone(), 156 + name: name.clone(), 157 + pid: Some(pid), 158 + distro, 159 + ip_address: Some(ip_addr.clone()), 160 + status: "RUNNING".into(), 161 + project_dir, 162 + vmlinux: Some(kernel), 163 + rootfs: Some(rootfs), 164 + bootargs: options.bootargs.clone(), 165 + ..Default::default() 166 + }, 167 + ) 168 + .await?; 169 + } 170 + None => { 171 + repo::virtual_machine::create( 172 + &pool, 173 + VirtualMachine { 174 + vcpu: options.vcpu, 175 + memory: options.memory, 176 + api_socket: options.api_socket.clone(), 177 + bridge: options.bridge.clone(), 178 + tap: options.tap.clone(), 179 + mac_address: options.mac_address.clone(), 180 + name: name.clone(), 181 + pid: Some(pid), 182 + distro, 183 + ip_address: Some(ip_addr.clone()), 184 + status: "RUNNING".into(), 185 + project_dir, 186 + vmlinux: Some(kernel), 187 + rootfs: Some(rootfs), 188 + bootargs: options.bootargs.clone(), 189 + ..Default::default() 190 + }, 191 + ) 192 + .await?; 193 + } 194 + } 133 195 134 196 println!("[✓] MicroVM booted and network is configured 🎉"); 135 197
+15 -1
crates/firecracker-vm/src/mqttc.rs
··· 3 3 use anyhow::Error; 4 4 use rumqttc::{AsyncClient, MqttOptions, QoS}; 5 5 6 + use crate::command::run_command; 7 + 6 8 pub async fn wait_for_mqtt_message(msgtype: &str) -> Result<String, Error> { 7 9 println!("[+] Waiting for MQTT message..."); 8 10 let mut mqttoptions = MqttOptions::new("fireup", "localhost", 1883); ··· 16 18 let payload_str = String::from_utf8_lossy(&publish.payload).to_string(); 17 19 println!("[+] Received MQTT message: {}", payload_str); 18 20 if payload_str.starts_with(msgtype) { 19 - return Ok(payload_str); 21 + let ip_addr = payload_str.split_whitespace().nth(2).ok_or_else(|| { 22 + anyhow::anyhow!("Failed to extract IP address from MQTT message") 23 + })?; 24 + 25 + if !ip_addr.is_empty() { 26 + println!("[+] Pinging IP address: {}", ip_addr); 27 + if run_command("sh", &["-c", &format!("ping -c 1 {}", ip_addr)], false).is_ok() 28 + { 29 + println!("[+] IP address {} is reachable.", ip_addr); 30 + return Ok(payload_str); 31 + } 32 + println!("[-] IP address {} is not reachable yet.", ip_addr); 33 + } 20 34 } 21 35 } 22 36 }
+20 -15
crates/firecracker-vm/src/network.rs
··· 19 19 .unwrap_or(false) 20 20 } 21 21 22 + fn create_new_tap(config: &VmOptions) -> Result<()> { 23 + run_command( 24 + "ip", 25 + &["tuntap", "add", "dev", &config.tap, "mode", "tap"], 26 + true, 27 + )?; 28 + run_command("ip", &["link", "set", "dev", &config.tap, "up"], true)?; 29 + run_command( 30 + "ip", 31 + &["link", "set", &config.tap, "master", &config.bridge], 32 + true, 33 + )?; 34 + Ok(()) 35 + } 36 + 22 37 pub fn setup_network(config: &VmOptions) -> Result<()> { 23 38 if check_tap_exists(config) { 24 39 run_command("ip", &["addr", "flush", "dev", &config.tap], true)?; ··· 29 44 return Ok(()); 30 45 } 31 46 32 - if !check_tap_exists(config) { 33 - println!("[+] Configuring {}...", &config.tap); 34 - run_command( 35 - "ip", 36 - &["tuntap", "add", "dev", &config.tap, "mode", "tap"], 37 - true, 38 - )?; 39 - run_command("ip", &["link", "set", "dev", &config.tap, "up"], true)?; 40 - } 41 - 42 47 if !check_bridge_exists(config) { 43 48 println!("[+] Configuring {}...", config.bridge); 44 49 run_command( ··· 49 54 run_command("ip", &["link", "set", &config.bridge, "up"], true)?; 50 55 run_command( 51 56 "ip", 52 - &["link", "set", &config.tap, "master", &config.bridge], 53 - true, 54 - )?; 55 - run_command( 56 - "ip", 57 57 &[ 58 58 "addr", 59 59 "add", ··· 63 63 ], 64 64 true, 65 65 )?; 66 + } 67 + 68 + if !check_tap_exists(config) { 69 + println!("[+] Configuring {}...", &config.tap); 70 + create_new_tap(config)?; 66 71 } 67 72 68 73 let ip_forward = run_command("cat", &["/proc/sys/net/ipv4/ip_forward"], false)?.stdout;
+1
crates/firecracker-vm/src/nextdhcp.rs
··· 66 66 println!("[+] Starting nextdhcp..."); 67 67 68 68 run_command("systemctl", &["enable", "nextdhcp"], true)?; 69 + run_command("systemctl", &["daemon-reload"], true)?; 69 70 run_command("systemctl", &["stop", "nextdhcp"], true)?; 70 71 run_command("systemctl", &["start", "nextdhcp"], true)?; 71 72 println!("[✓] Nextdhcp started successfully.");
+2 -2
crates/firecracker-vm/src/types.rs
··· 1 1 use fire_config::FireConfig; 2 2 use firecracker_prepare::Distro; 3 3 4 - use crate::constants::{BRIDGE_DEV, FC_MAC, FIRECRACKER_SOCKET, TAP_DEV}; 4 + use crate::constants::{BRIDGE_DEV, FC_MAC, FIRECRACKER_SOCKET}; 5 5 6 6 #[derive(Default, Clone)] 7 7 pub struct VmOptions { ··· 34 34 rootfs: vm.rootfs, 35 35 bootargs: vm.boot_args, 36 36 bridge: vm.bridge.unwrap_or(BRIDGE_DEV.into()), 37 - tap: vm.tap.unwrap_or(TAP_DEV.into()), 37 + tap: vm.tap.unwrap_or("".into()), 38 38 api_socket: vm.api_socket.unwrap_or(FIRECRACKER_SOCKET.into()), 39 39 mac_address: vm.mac.unwrap_or(FC_MAC.into()), 40 40 }