Prepare, configure, and manage Firecracker microVMs in seconds!
virtualization linux microvm firecracker
at main 210 lines 7.4 kB view raw
1use anyhow::{anyhow, Context, Result}; 2use firecracker_prepare::Distro; 3use firecracker_state::{entity::virtual_machine::VirtualMachine, repo}; 4use owo_colors::OwoColorize; 5use std::fs; 6 7use crate::{config::get_config_dir, types::VmOptions}; 8 9mod command; 10mod config; 11pub mod constants; 12mod coredns; 13mod dhcpd; 14mod firecracker; 15mod guest; 16pub mod mac; 17mod mosquitto; 18mod mqttc; 19mod network; 20mod tailscale; 21pub mod types; 22 23pub async fn setup( 24 options: &VmOptions, 25 pid: u32, 26 vm_id: Option<String>, 27 kernel_file: &str, 28) -> Result<String> { 29 let distro: Distro = options.clone().into(); 30 let app_dir = get_config_dir().with_context(|| "Failed to get configuration directory")?; 31 32 let name = options 33 .api_socket 34 .split('/') 35 .last() 36 .ok_or_else(|| anyhow!("Failed to extract VM name from API socket path"))? 37 .replace("firecracker-", "") 38 .replace(".sock", "") 39 .to_string(); 40 let name = match name.is_empty() { 41 true => names::Generator::default().next().unwrap(), 42 false => name, 43 }; 44 45 fs::create_dir_all(format!("{}/logs", app_dir)) 46 .with_context(|| format!("Failed to create logs directory: {}", app_dir))?; 47 48 let logfile = format!("{}/logs/firecracker-{}.log", app_dir, name); 49 fs::File::create(&logfile) 50 .with_context(|| format!("Failed to create log file: {}", logfile))?; 51 52 let kernel = fs::canonicalize(kernel_file) 53 .with_context(|| { 54 format!( 55 "Failed to resolve absolute path for kernel: {}", 56 kernel_file 57 ) 58 })? 59 .display() 60 .to_string(); 61 62 // readonly rootfs (squashfs) 63 let img_file = match distro { 64 Distro::Debian => format!("{}/debian-rootfs.img", app_dir), 65 Distro::Alpine => format!("{}/alpine-rootfs.img", app_dir), 66 Distro::NixOS => format!("{}/nixos-rootfs.img", app_dir), 67 Distro::Ubuntu => format!("{}/ubuntu-rootfs.img", app_dir), 68 Distro::Fedora => format!("{}/fedora-rootfs.img", app_dir), 69 Distro::Gentoo => format!("{}/gentoo-rootfs.img", app_dir), 70 Distro::Slackware => format!("{}/slackware-rootfs.img", app_dir), 71 Distro::Opensuse => format!("{}/opensuse-rootfs.img", app_dir), 72 Distro::OpensuseTumbleweed => format!("{}/opensuse-tumbleweed-rootfs.img", app_dir), 73 Distro::Almalinux => format!("{}/almalinux-rootfs.img", app_dir), 74 Distro::RockyLinux => format!("{}/rockylinux-rootfs.img", app_dir), 75 Distro::Archlinux => format!("{}/archlinux-rootfs.img", app_dir), 76 }; 77 78 let rootfs = fs::canonicalize(&img_file) 79 .with_context(|| format!("Failed to resolve absolute path for rootfs: {}", img_file))? 80 .display() 81 .to_string(); 82 83 let key_name = glob::glob(format!("{}/id_rsa", app_dir).as_str()) 84 .with_context(|| "Failed to glob ssh key files")? 85 .last() 86 .ok_or_else(|| anyhow!("No SSH key file found"))? 87 .with_context(|| "Failed to get SSH key path")?; 88 let key_name = fs::canonicalize(&key_name) 89 .with_context(|| { 90 format!( 91 "Failed to resolve absolute path for SSH key: {}", 92 key_name.display() 93 ) 94 })? 95 .display() 96 .to_string(); 97 let arch = command::run_command("uname", &["-m"], false)?.stdout; 98 let arch = String::from_utf8_lossy(&arch).trim().to_string(); 99 network::setup_network(options)?; 100 mosquitto::setup_mosquitto(options)?; 101 coredns::setup_coredns(options)?; 102 dhcpd::setup_kea_dhcp(options)?; 103 104 firecracker::configure(&logfile, &kernel, &rootfs, &arch, &options)?; 105 106 let guest_ip = format!("{}.firecracker", name); 107 guest::configure_guest_network(&key_name, &guest_ip, distro == Distro::NixOS)?; 108 109 tailscale::setup_tailscale(&name, options)?; 110 111 let pool = firecracker_state::create_connection_pool().await?; 112 113 let ip_file = format!("/tmp/firecracker-{}.ip", name); 114 115 // loop until the IP file is created 116 let mut attempts = 0; 117 while attempts < 30 { 118 println!("[*] Waiting for VM to obtain an IP address..."); 119 if fs::metadata(&ip_file).is_ok() { 120 break; 121 } 122 std::thread::sleep(std::time::Duration::from_millis(500)); 123 attempts += 1; 124 } 125 126 let ip_addr = fs::read_to_string(&ip_file) 127 .with_context(|| format!("Failed to read IP address from file: {}", ip_file))? 128 .trim() 129 .to_string(); 130 131 fs::remove_file(&ip_file) 132 .with_context(|| format!("Failed to remove IP address file: {}", ip_file))?; 133 134 let project_dir = match fs::metadata("fire.toml").is_ok() { 135 true => Some(std::env::current_dir()?.display().to_string()), 136 false => None, 137 }; 138 139 let kernel = match &options.vmlinux { 140 Some(path) => path.clone(), 141 None => kernel.into(), 142 }; 143 144 let kernel = fs::canonicalize(&kernel) 145 .with_context(|| format!("Failed to canonicalize kernel path: {}", kernel))? 146 .display() 147 .to_string(); 148 149 let vm_id = match vm_id { 150 Some(id) => { 151 repo::virtual_machine::update( 152 &pool, 153 &id, 154 VirtualMachine { 155 vcpu: options.vcpu, 156 memory: options.memory, 157 api_socket: options.api_socket.clone(), 158 bridge: options.bridge.clone(), 159 tap: options.tap.clone(), 160 mac_address: options.mac_address.clone(), 161 name: name.clone(), 162 pid: Some(pid), 163 distro: distro.to_string(), 164 ip_address: Some(ip_addr.clone()), 165 status: "RUNNING".into(), 166 project_dir, 167 vmlinux: Some(kernel), 168 rootfs: Some(rootfs), 169 bootargs: options.bootargs.clone(), 170 ssh_keys: options.ssh_keys.as_ref().map(|keys| keys.join(",")), 171 ..Default::default() 172 }, 173 ) 174 .await?; 175 id 176 } 177 None => { 178 repo::virtual_machine::create( 179 &pool, 180 VirtualMachine { 181 vcpu: options.vcpu, 182 memory: options.memory, 183 api_socket: options.api_socket.clone(), 184 bridge: options.bridge.clone(), 185 tap: options.tap.clone(), 186 mac_address: options.mac_address.clone(), 187 name: name.clone(), 188 pid: Some(pid), 189 distro: distro.to_string(), 190 ip_address: Some(ip_addr.clone()), 191 status: "RUNNING".into(), 192 project_dir, 193 vmlinux: Some(kernel), 194 rootfs: Some(rootfs), 195 bootargs: options.bootargs.clone(), 196 ssh_keys: options.ssh_keys.as_ref().map(|keys| keys.join(",")), 197 ..Default::default() 198 }, 199 ) 200 .await? 201 } 202 }; 203 204 println!("[✓] MicroVM booted and network is configured 🎉"); 205 206 println!("SSH into the VM using the following command:"); 207 println!("{} {}", "fireup ssh".bright_green(), name.bright_green()); 208 209 Ok(vm_id) 210}