···11+-- Add migration script here
22+CREATE TABLE IF NOT EXISTS virtual_machines (
33+ id VARCHAR(255) PRIMARY KEY,
44+ name VARCHAR(255) UNIQUE NOT NULL,
55+ status VARCHAR(255) NOT NULL,
66+ vcpu INT NOT NULL,
77+ memory INT NOT NULL,
88+ distro VARCHAR(255) NOT NULL,
99+ pid INT,
1010+ mac_address VARCHAR(255) NOT NULL,
1111+ bridge VARCHAR(255) NOT NULL,
1212+ tap VARCHAR(255) NOT NULL,
1313+ api_socket VARCHAR(255) UNIQUE NOT NULL,
1414+ project_dir VARCHAR(255) UNIQUE,
1515+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
1616+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
1717+);
···11+-- Add migration script here
22+CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_mac_address ON virtual_machines (mac_address);
33+CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_pid ON virtual_machines (pid);
44+CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_tap ON virtual_machines (tap);
···11pub mod down;
22pub mod init;
33pub mod logs;
44+pub mod ps;
45pub mod reset;
56pub mod ssh;
77+pub mod start;
68pub mod status;
99+pub mod stop;
710pub mod up;
···11use anyhow::{anyhow, Context, Result};
22use firecracker_prepare::Distro;
33+use firecracker_state::{entity::virtual_machine::VirtualMachine, repo};
34use owo_colors::OwoColorize;
45use std::fs;
56···89mod command;
910mod config;
1011pub mod constants;
1111-mod dnsmasq;
1212+mod coredns;
1213mod firecracker;
1314mod guest;
1515+pub mod mac;
1616+mod mosquitto;
1717+mod mqttc;
1418mod network;
1919+mod nextdhcp;
1520pub mod types;
16211717-pub fn setup(options: &VmOptions) -> Result<()> {
2222+pub async fn setup(options: &VmOptions, pid: u32, vm_id: Option<String>) -> Result<()> {
1823 let distro: Distro = options.clone().into();
1924 let app_dir = get_config_dir().with_context(|| "Failed to get configuration directory")?;
20252121- let logfile = format!("{}/firecracker.log", app_dir);
2626+ let name = options
2727+ .api_socket
2828+ .split('/')
2929+ .last()
3030+ .ok_or_else(|| anyhow!("Failed to extract VM name from API socket path"))?
3131+ .replace("firecracker-", "")
3232+ .replace(".sock", "")
3333+ .to_string();
3434+ let name = match name.is_empty() {
3535+ true => names::Generator::default().next().unwrap(),
3636+ false => name,
3737+ };
3838+3939+ fs::create_dir_all(format!("{}/logs", app_dir))
4040+ .with_context(|| format!("Failed to create logs directory: {}", app_dir))?;
4141+4242+ let logfile = format!("{}/logs/firecracker-{}.log", app_dir, name);
2243 fs::File::create(&logfile)
2344 .with_context(|| format!("Failed to create log file: {}", logfile))?;
2445···7697 let arch = command::run_command("uname", &["-m"], false)?.stdout;
7798 let arch = String::from_utf8_lossy(&arch).trim().to_string();
7899 network::setup_network(options)?;
100100+ mosquitto::setup_mosquitto(options)?;
101101+ coredns::setup_coredns(options)?;
102102+ nextdhcp::setup_nextdhcp(options)?;
7910380104 firecracker::configure(&logfile, &kernel, &rootfs, &arch, &options, distro)?;
8110582106 if distro != Distro::NixOS {
8383- guest::configure_guest_network(&key_name)?;
107107+ let guest_ip = format!("{}.firecracker", name);
108108+ guest::configure_guest_network(&key_name, &guest_ip)?;
109109+ }
110110+ let pool = firecracker_state::create_connection_pool().await?;
111111+ let distro = match distro {
112112+ Distro::Debian => "debian".into(),
113113+ Distro::Alpine => "alpine".into(),
114114+ Distro::NixOS => "nixos".into(),
115115+ Distro::Ubuntu => "ubuntu".into(),
116116+ };
117117+118118+ let ip_file = format!("/tmp/firecracker-{}.ip", name);
119119+120120+ // loop until the IP file is created
121121+ let mut attempts = 0;
122122+ while attempts < 30 {
123123+ println!("[*] Waiting for VM to obtain an IP address...");
124124+ if fs::metadata(&ip_file).is_ok() {
125125+ break;
126126+ }
127127+ std::thread::sleep(std::time::Duration::from_millis(500));
128128+ attempts += 1;
129129+ }
130130+131131+ let ip_addr = fs::read_to_string(&ip_file)
132132+ .with_context(|| format!("Failed to read IP address from file: {}", ip_file))?
133133+ .trim()
134134+ .to_string();
135135+136136+ fs::remove_file(&ip_file)
137137+ .with_context(|| format!("Failed to remove IP address file: {}", ip_file))?;
138138+139139+ let project_dir = match fs::metadata("fire.toml").is_ok() {
140140+ true => Some(std::env::current_dir()?.display().to_string()),
141141+ false => None,
142142+ };
143143+144144+ match vm_id {
145145+ Some(id) => {
146146+ repo::virtual_machine::update(
147147+ &pool,
148148+ &id,
149149+ VirtualMachine {
150150+ vcpu: options.vcpu,
151151+ memory: options.memory,
152152+ api_socket: options.api_socket.clone(),
153153+ bridge: options.bridge.clone(),
154154+ tap: options.tap.clone(),
155155+ mac_address: options.mac_address.clone(),
156156+ name: name.clone(),
157157+ pid: Some(pid),
158158+ distro,
159159+ ip_address: Some(ip_addr.clone()),
160160+ status: "RUNNING".into(),
161161+ project_dir,
162162+ vmlinux: Some(kernel),
163163+ rootfs: Some(rootfs),
164164+ bootargs: options.bootargs.clone(),
165165+ ..Default::default()
166166+ },
167167+ )
168168+ .await?;
169169+ }
170170+ None => {
171171+ repo::virtual_machine::create(
172172+ &pool,
173173+ VirtualMachine {
174174+ vcpu: options.vcpu,
175175+ memory: options.memory,
176176+ api_socket: options.api_socket.clone(),
177177+ bridge: options.bridge.clone(),
178178+ tap: options.tap.clone(),
179179+ mac_address: options.mac_address.clone(),
180180+ name: name.clone(),
181181+ pid: Some(pid),
182182+ distro,
183183+ ip_address: Some(ip_addr.clone()),
184184+ status: "RUNNING".into(),
185185+ project_dir,
186186+ vmlinux: Some(kernel),
187187+ rootfs: Some(rootfs),
188188+ bootargs: options.bootargs.clone(),
189189+ ..Default::default()
190190+ },
191191+ )
192192+ .await?;
193193+ }
84194 }
8519586196 println!("[✓] MicroVM booted and network is configured 🎉");
8719788198 println!("SSH into the VM using the following command:");
8989- println!("{}", "fireup ssh".bright_green());
199199+ println!("{} {}", "fireup ssh".bright_green(), name.bright_green());
9020091201 Ok(())
92202}
+45
crates/firecracker-vm/src/mac.rs
···11+use rand::Rng;
22+33+pub fn generate_unique_mac() -> String {
44+ let mut rng = rand::thread_rng();
55+66+ // First byte must have the least significant bit set to 0 (unicast)
77+ // and the second least significant bit set to 0 (globally unique)
88+ let first_byte = (rng.gen::<u8>() & 0xFC) | 0x02; // Set locally administered bit
99+1010+ // Generate the remaining 5 bytes
1111+ let mut mac_bytes = [0u8; 6];
1212+ mac_bytes[0] = first_byte;
1313+ for i in 1..6 {
1414+ mac_bytes[i] = rng.gen::<u8>();
1515+ }
1616+1717+ // Format as a MAC address (XX:XX:XX:XX:XX:XX)
1818+ format!(
1919+ "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
2020+ mac_bytes[0], mac_bytes[1], mac_bytes[2], mac_bytes[3], mac_bytes[4], mac_bytes[5]
2121+ )
2222+}
2323+2424+#[cfg(test)]
2525+mod tests {
2626+ use super::*;
2727+2828+ #[test]
2929+ fn test_generate_unique_mac() {
3030+ let mac1 = generate_unique_mac();
3131+ let mac2 = generate_unique_mac();
3232+3333+ // Check format (6 pairs of 2 hex digits separated by colons)
3434+ assert!(mac1.chars().count() == 17);
3535+ assert!(mac1.split(':').count() == 6);
3636+3737+ // Check that the MAC is locally administered (second bit of first byte is 1)
3838+ let first_byte = u8::from_str_radix(&mac1[0..2], 16).unwrap();
3939+ assert_eq!(first_byte & 0x02, 0x02);
4040+ assert_eq!(first_byte & 0x01, 0x00); // Unicast
4141+4242+ // Check uniqueness
4343+ assert_ne!(mac1, mac2);
4444+ }
4545+}