Prepare, configure, and manage Firecracker microVMs in seconds!
virtualization
linux
microvm
firecracker
1use anyhow::Result;
2use clap::{arg, Arg, Command};
3use firecracker_vm::{constants::BRIDGE_DEV, mac::generate_unique_mac, types::VmOptions};
4use owo_colors::OwoColorize;
5
6use crate::cmd::{
7 cp::cp, down::down, exec::exec, init::init, inspect::inspect_microvm, logs::logs,
8 ps::list_all_instances, reset::reset, rm::remove, serve::serve, ssh::ssh, start::start,
9 status::status, stop::stop, up::up,
10};
11
12pub mod cmd;
13pub mod command;
14pub mod config;
15pub mod date;
16pub mod ssh;
17
18fn cli() -> Command {
19 let banner = format!(
20 "{}",
21 r#"
22 _______ __ __
23 / ____(_)_______ / / / /___
24 / /_ / / ___/ _ \/ / / / __ \
25 / __/ / / / / __/ /_/ / /_/ /
26 /_/ /_/_/ \___/\____/ .___/
27 /_/
28"#
29 .yellow()
30 );
31
32 Command::new("fireup")
33 .version(env!("CARGO_PKG_VERSION"))
34 .about(&banner)
35 .subcommand(
36 Command::new("init")
37 .about("Create a new MicroVM configuration `fire.toml` in the current directory"),
38 )
39 .subcommand(
40 Command::new("ps")
41 .alias("list")
42 .arg(arg!(-a --all "Show all Firecracker MicroVM instances").default_value("false"))
43 .about("List all Firecracker MicroVM instances"),
44 )
45 .subcommand(
46 Command::new("start")
47 .arg(arg!(<name> "Name of the Firecracker MicroVM to start").required(true))
48 .arg(
49 Arg::new("tailscale-auth-key")
50 .long("tailscale-auth-key")
51 .value_name("TAILSCALE_AUTH_KEY")
52 .help("Tailscale auth key to connect the VM to a Tailscale network"),
53 )
54 .about("Start Firecracker MicroVM"),
55 )
56 .subcommand(
57 Command::new("stop")
58 .arg(arg!([name] "Name of the Firecracker MicroVM to stop").required(false))
59 .about("Stop Firecracker MicroVM"),
60 )
61 .subcommand(
62 Command::new("restart")
63 .arg(arg!(<name> "Name of the Firecracker MicroVM to restart").required(true))
64 .arg(
65 Arg::new("tailscale-auth-key")
66 .long("tailscale-auth-key")
67 .value_name("TAILSCALE_AUTH_KEY")
68 .help("Tailscale auth key to connect the VM to a Tailscale network"),
69 )
70 .about("Restart Firecracker MicroVM"),
71 )
72 .subcommand(
73 Command::new("up")
74 .arg(arg!(--debian "Prepare Debian MicroVM").default_value("false"))
75 .arg(arg!(--alpine "Prepare Alpine MicroVM").default_value("false"))
76 .arg(arg!(--nixos "Prepare NixOS MicroVM").default_value("false"))
77 .arg(arg!(--fedora "Prepare Fedora MicroVM").default_value("false"))
78 .arg(arg!(--gentoo "Prepare Gentoo MicroVM").default_value("false"))
79 .arg(arg!(--slackware "Prepare Slackware MicroVM").default_value("false"))
80 .arg(arg!(--opensuse "Prepare OpenSUSE MicroVM").default_value("false"))
81 .arg(
82 Arg::new("opensuse-tumbleweed")
83 .help("Prepare OpenSUSE Tumbleweed MicroVM")
84 .action(clap::ArgAction::SetTrue),
85 )
86 .arg(arg!(--almalinux "Prepare AlmaLinux MicroVM").default_value("false"))
87 .arg(arg!(--rockylinux "Prepare RockyLinux MicroVM").default_value("false"))
88 .arg(arg!(--archlinux "Prepare ArchLinux MicroVM").default_value("false"))
89 .arg(arg!(--ubuntu "Prepare Ubuntu MicroVM").default_value("true"))
90 .arg(arg!(--vcpu <n> "Number of vCPUs"))
91 .arg(arg!(--memory <m> "Memory size in MiB"))
92 .arg(arg!(--vmlinux <path> "Path to the kernel image"))
93 .arg(arg!(--rootfs <path> "Path to the root filesystem image"))
94 .arg(arg!(--bridge <name> "Name of the bridge interface").default_value(BRIDGE_DEV))
95 .arg(arg!(--tap <name> "Name of the tap interface").default_value(""))
96 .arg(
97 Arg::new("mac-address")
98 .long("mac-address")
99 .value_name("MAC")
100 .help("MAC address for the network interface"),
101 )
102 .arg(
103 Arg::new("api-socket")
104 .long("api-socket")
105 .value_name("path")
106 .help("Path to the Firecracker API socket"),
107 )
108 .arg(
109 Arg::new("boot-args")
110 .long("boot-args")
111 .value_name("ARGS")
112 .help("Override boot arguments"),
113 )
114 .arg(
115 Arg::new("ssh-keys")
116 .long("ssh-keys")
117 .value_name("SSH_KEYS")
118 .help("Comma-separated list of SSH public keys to add to the VM"),
119 )
120 .arg(
121 Arg::new("tailscale-auth-key")
122 .long("tailscale-auth-key")
123 .value_name("TAILSCALE_AUTH_KEY")
124 .help("Tailscale auth key to connect the VM to a Tailscale network"),
125 )
126 .about("Start a new Firecracker MicroVM"),
127 )
128 .subcommand(Command::new("down").about("Stop Firecracker MicroVM"))
129 .subcommand(
130 Command::new("status")
131 .arg(arg!([name] "Name of the Firecracker MicroVM to check status").required(false))
132 .about("Check the status of Firecracker MicroVM"),
133 )
134 .subcommand(
135 Command::new("logs")
136 .arg(
137 arg!(-f --follow "Follow the logs")
138 .short('f')
139 .long("follow")
140 .default_value("false"),
141 )
142 .about("View the logs of the Firecracker MicroVM"),
143 )
144 .subcommand(
145 Command::new("ssh")
146 .arg(arg!([name] "Name of the Firecracker MicroVM to SSH into"))
147 .about("SSH into the Firecracker MicroVM"),
148 )
149 .subcommand(
150 Command::new("reset")
151 .arg(arg!([name] "Name of the Firecracker MicroVM to reset").required(false))
152 .about("Reset the Firecracker MicroVM"),
153 )
154 .subcommand(
155 Command::new("rm")
156 .arg(arg!(<name> "Name or ID of the Firecracker MicroVM to delete").required(true))
157 .about("Delete the Firecracker MicroVM"),
158 )
159 .subcommand(
160 Command::new("serve")
161 .about("Start fireup HTTP API server")
162 .arg(arg!(--host <host> "Host to bind the server"))
163 .arg(arg!(--port <port> "Port to bind the server")),
164 )
165 .subcommand(
166 Command::new("inspect")
167 .arg(arg!(<name> "Name or ID of the Firecracker MicroVM to inspect").required(true))
168 .about("Inspect the Firecracker MicroVM details"),
169 )
170 .subcommand(
171 Command::new("exec")
172 .arg(
173 arg!(<name> "Name of the Firecracker MicroVM to execute command in")
174 .required(true),
175 )
176 .arg(
177 Arg::new("args")
178 .help("Command and arguments to execute inside the MicroVM")
179 .required(true)
180 .num_args(1..),
181 )
182 .about("Execute a command inside the Firecracker MicroVM"),
183 )
184 .subcommand(
185 Command::new("cp")
186 .arg(arg!(<source> "Source file path").required(true))
187 .arg(arg!(<destination> "Destination file path").required(true))
188 .about("Copy files to/from the Firecracker MicroVM"),
189 )
190 .arg(arg!(--debian "Prepare Debian MicroVM").default_value("false"))
191 .arg(arg!(--alpine "Prepare Alpine MicroVM").default_value("false"))
192 .arg(arg!(--nixos "Prepare NixOS MicroVM").default_value("false"))
193 .arg(arg!(--fedora "Prepare Fedora MicroVM").default_value("false"))
194 .arg(arg!(--gentoo "Prepare Gentoo MicroVM").default_value("false"))
195 .arg(arg!(--slackware "Prepare Slackware MicroVM").default_value("false"))
196 .arg(arg!(--opensuse "Prepare OpenSUSE MicroVM").default_value("false"))
197 .arg(
198 Arg::new("opensuse-tumbleweed")
199 .long("opensuse-tumbleweed")
200 .help("Prepare OpenSUSE Tumbleweed MicroVM")
201 .action(clap::ArgAction::SetTrue),
202 )
203 .arg(arg!(--almalinux "Prepare AlmaLinux MicroVM").default_value("false"))
204 .arg(arg!(--rockylinux "Prepare RockyLinux MicroVM").default_value("false"))
205 .arg(arg!(--archlinux "Prepare ArchLinux MicroVM").default_value("false"))
206 .arg(arg!(--ubuntu "Prepare Ubuntu MicroVM").default_value("true"))
207 .arg(arg!(--vcpu <n> "Number of vCPUs"))
208 .arg(arg!(--memory <m> "Memory size in MiB"))
209 .arg(arg!(--vmlinux <path> "Path to the kernel image"))
210 .arg(arg!(--rootfs <path> "Path to the root filesystem image"))
211 .arg(arg!(--bridge <name> "Name of the bridge interface").default_value(BRIDGE_DEV))
212 .arg(arg!(--tap <name> "Name of the tap interface").default_value(""))
213 .arg(
214 Arg::new("mac-address")
215 .long("mac-address")
216 .value_name("MAC")
217 .help("MAC address for the network interface"),
218 )
219 .arg(
220 Arg::new("api-socket")
221 .long("api-socket")
222 .value_name("path")
223 .help("Path to the Firecracker API socket"),
224 )
225 .arg(
226 Arg::new("boot-args")
227 .long("boot-args")
228 .value_name("ARGS")
229 .help("Override boot arguments"),
230 )
231 .arg(
232 Arg::new("ssh-keys")
233 .long("ssh-keys")
234 .value_name("SSH_KEYS")
235 .help("Comma-separated list of SSH public keys to add to the VM"),
236 )
237 .arg(
238 Arg::new("tailscale-auth-key")
239 .long("tailscale-auth-key")
240 .value_name("TAILSCALE_AUTH_KEY")
241 .help("Tailscale auth key to connect the VM to a Tailscale network"),
242 )
243}
244
245#[tokio::main]
246async fn main() -> Result<()> {
247 let matches = cli().get_matches();
248
249 let pool = firecracker_state::create_connection_pool().await?;
250 let vm_name = names::Generator::default().next().unwrap();
251 let default_socket = format!("/tmp/firecracker-{}.sock", vm_name);
252 let default_mac = generate_unique_mac();
253
254 match matches.subcommand() {
255 Some(("init", _)) => init()?,
256 Some(("ps", args)) => {
257 let all = args.get_one::<bool>("all").copied().unwrap_or(false);
258 list_all_instances(all).await?;
259 }
260 Some(("stop", args)) => {
261 let name = args.get_one::<String>("name").cloned().unwrap();
262 stop(&name).await?;
263 }
264 Some(("start", args)) => {
265 let name = args.get_one::<String>("name").cloned().unwrap();
266 let tailscale_auth_key = args.get_one::<String>("tailscale-auth-key").cloned();
267 start(&name, tailscale_auth_key).await?;
268 }
269 Some(("restart", args)) => {
270 let name = args.get_one::<String>("name").cloned().unwrap();
271 let tailscale_auth_key = args.get_one::<String>("tailscale-auth-key").cloned();
272 stop(&name).await?;
273 start(&name, tailscale_auth_key).await?;
274 }
275 Some(("up", args)) => {
276 let vcpu = matches
277 .get_one::<String>("vcpu")
278 .map(|s| s.parse::<u16>().unwrap())
279 .unwrap_or(num_cpus::get() as u16);
280 let memory = matches
281 .get_one::<String>("memory")
282 .map(|s| s.parse::<u16>().unwrap())
283 .unwrap_or(512);
284 let vmlinux = matches.get_one::<String>("vmlinux").cloned();
285 let rootfs = matches.get_one::<String>("rootfs").cloned();
286 let bootargs = matches.get_one::<String>("boot-args").cloned();
287 let bridge = args.get_one::<String>("bridge").cloned().unwrap();
288 let tap = args.get_one::<String>("tap").cloned().unwrap();
289 let api_socket = args
290 .get_one::<String>("api-socket")
291 .cloned()
292 .unwrap_or(default_socket);
293 let mac_address = args
294 .get_one::<String>("mac-address")
295 .cloned()
296 .unwrap_or(default_mac);
297 let ssh_keys = args
298 .get_one::<String>("ssh-keys")
299 .map(|s| s.split(',').map(|s| s.trim().to_string()).collect());
300 let tailscale_auth_key = args.get_one::<String>("tailscale-auth-key").cloned();
301 let options = VmOptions {
302 debian: args.get_one::<bool>("debian").copied(),
303 alpine: args.get_one::<bool>("alpine").copied(),
304 ubuntu: args.get_one::<bool>("ubuntu").copied(),
305 nixos: args.get_one::<bool>("nixos").copied(),
306 fedora: args.get_one::<bool>("fedora").copied(),
307 gentoo: args.get_one::<bool>("gentoo").copied(),
308 slackware: args.get_one::<bool>("slackware").copied(),
309 opensuse: args.get_one::<bool>("opensuse").copied(),
310 opensuse_tumbleweed: args.get_one::<bool>("opensuse-tumbleweed").copied(),
311 almalinux: args.get_one::<bool>("almalinux").copied(),
312 rockylinux: args.get_one::<bool>("rockylinux").copied(),
313 archlinux: args.get_one::<bool>("archlinux").copied(),
314 vcpu,
315 memory,
316 vmlinux,
317 rootfs,
318 bootargs,
319 bridge,
320 tap,
321 api_socket,
322 mac_address,
323 etcd: None,
324 ssh_keys,
325 tailscale: tailscale_auth_key.map(|key| fire_config::TailscaleOptions {
326 auth_key: Some(key),
327 }),
328 };
329 up(options).await?
330 }
331 Some(("down", _)) => down().await?,
332 Some(("status", args)) => {
333 let name = args.get_one::<String>("name").cloned();
334 status(name).await?;
335 }
336 Some(("logs", args)) => {
337 let follow = args.get_one::<bool>("follow").copied().unwrap_or(false);
338 logs(follow)?;
339 }
340 Some(("ssh", args)) => {
341 let name = args.get_one::<String>("name").cloned();
342 ssh(pool, name).await?
343 }
344 Some(("reset", args)) => {
345 let name = args.get_one::<String>("name").cloned();
346 let api_socket = match name {
347 Some(name) => format!("/tmp/firecracker-{}.sock", name),
348 None => String::from(""),
349 };
350 reset(VmOptions {
351 api_socket,
352 ..Default::default()
353 })
354 .await?
355 }
356 Some(("rm", args)) => {
357 let name = args.get_one::<String>("name").cloned().unwrap();
358 remove(&name).await?
359 }
360 Some(("serve", _)) => serve().await?,
361 Some(("inspect", args)) => {
362 let name = args.get_one::<String>("name").cloned().unwrap();
363 inspect_microvm(&name).await?;
364 }
365 Some(("exec", args)) => {
366 let name = args.get_one::<String>("name").cloned().unwrap();
367 let cmd_args: Vec<String> = args
368 .get_many::<String>("args")
369 .unwrap()
370 .map(|s| s.to_string())
371 .collect();
372 exec(&name, cmd_args).await?;
373 }
374 Some(("cp", args)) => {
375 let source = args.get_one::<String>("source").cloned().unwrap();
376 let destination = args.get_one::<String>("destination").cloned().unwrap();
377 cp(&source, &destination).await?;
378 }
379 _ => {
380 let debian = matches.get_one::<bool>("debian").copied().unwrap_or(false);
381 let alpine = matches.get_one::<bool>("alpine").copied().unwrap_or(false);
382 let nixos = matches.get_one::<bool>("nixos").copied().unwrap_or(false);
383 let ubuntu = matches.get_one::<bool>("ubuntu").copied().unwrap_or(false);
384 let fedora = matches.get_one::<bool>("fedora").copied().unwrap_or(false);
385 let gentoo = matches.get_one::<bool>("gentoo").copied().unwrap_or(false);
386 let slackware = matches
387 .get_one::<bool>("slackware")
388 .copied()
389 .unwrap_or(false);
390 let opensuse = matches
391 .get_one::<bool>("opensuse")
392 .copied()
393 .unwrap_or(false);
394 let opensuse_tumbleweed = matches
395 .get_one::<bool>("opensuse-tumbleweed")
396 .copied()
397 .unwrap_or(false);
398 let almalinux = matches
399 .get_one::<bool>("almalinux")
400 .copied()
401 .unwrap_or(false);
402 let rockylinux = matches
403 .get_one::<bool>("rockylinux")
404 .copied()
405 .unwrap_or(false);
406 let archlinux = matches
407 .get_one::<bool>("archlinux")
408 .copied()
409 .unwrap_or(false);
410
411 let vcpu = matches
412 .get_one::<String>("vcpu")
413 .map(|s| s.parse::<u16>().unwrap())
414 .unwrap_or(num_cpus::get() as u16);
415
416 let memory = matches
417 .get_one::<String>("memory")
418 .map(|s| s.parse::<u16>().unwrap())
419 .unwrap_or(if nixos { 2048 } else { 512 });
420
421 let vmlinux = matches.get_one::<String>("vmlinux").cloned();
422 let rootfs = matches.get_one::<String>("rootfs").cloned();
423 let bootargs = matches.get_one::<String>("boot-args").cloned();
424 let bridge = matches.get_one::<String>("bridge").cloned().unwrap();
425 let tap = matches.get_one::<String>("tap").cloned().unwrap();
426 let api_socket = matches
427 .get_one::<String>("api-socket")
428 .cloned()
429 .unwrap_or(default_socket);
430 let mac_address = matches
431 .get_one::<String>("mac-address")
432 .cloned()
433 .unwrap_or(default_mac);
434 let ssh_keys = matches
435 .get_one::<String>("ssh-keys")
436 .map(|s| s.split(',').map(|s| s.trim().to_string()).collect());
437 let tailscale_auth_key = matches.get_one::<String>("tailscale-auth-key").cloned();
438
439 let options = VmOptions {
440 debian: Some(debian),
441 alpine: Some(alpine),
442 ubuntu: Some(ubuntu),
443 nixos: Some(nixos),
444 fedora: Some(fedora),
445 gentoo: Some(gentoo),
446 slackware: Some(slackware),
447 opensuse: Some(opensuse),
448 opensuse_tumbleweed: Some(opensuse_tumbleweed),
449 almalinux: Some(almalinux),
450 rockylinux: Some(rockylinux),
451 archlinux: Some(archlinux),
452 vcpu,
453 memory,
454 vmlinux,
455 rootfs,
456 bootargs,
457 bridge,
458 tap,
459 api_socket,
460 mac_address,
461 etcd: None,
462 ssh_keys,
463 tailscale: tailscale_auth_key.map(|key| fire_config::TailscaleOptions {
464 auth_key: Some(key),
465 }),
466 };
467 up(options).await?
468 }
469 }
470
471 Ok(())
472}