Prepare, configure, and manage Firecracker microVMs in seconds!
virtualization
linux
microvm
firecracker
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}