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

feat: add SSH key management for Firecracker MicroVMs

+325 -68
+15 -1
crates/fire-server/src/services/microvm.rs
··· 76 api_socket: vm.api_socket.clone(), 77 mac_address: vm.mac_address.clone(), 78 etcd: None, 79 }; 80 81 let vm = start(pool, options, Some(vm.id)).await?; ··· 146 } 147 } 148 149 - firecracker_prepare::prepare(options.clone().into(), options.vmlinux.clone())?; 150 let vm_id = firecracker_vm::setup(&options, pid, vm_id).await?; 151 let vm = repo::virtual_machine::find(&pool, &vm_id) 152 .await?
··· 76 api_socket: vm.api_socket.clone(), 77 mac_address: vm.mac_address.clone(), 78 etcd: None, 79 + ssh_keys: vm 80 + .ssh_keys 81 + .map(|keys| keys.split(',').map(|s| s.to_string()).collect()), 82 }; 83 84 let vm = start(pool, options, Some(vm.id)).await?; ··· 149 } 150 } 151 152 + let mut ssh_keys = options.ssh_keys.clone(); 153 + if let Some(vm_id) = vm_id.clone() { 154 + let vm = repo::virtual_machine::find(&pool, &vm_id).await?; 155 + if ssh_keys.is_none() { 156 + ssh_keys = vm.and_then(|vm| { 157 + vm.ssh_keys 158 + .map(|keys| keys.split(',').map(|s| s.to_string()).collect()) 159 + }); 160 + } 161 + } 162 + 163 + firecracker_prepare::prepare(options.clone().into(), options.vmlinux.clone(), ssh_keys)?; 164 let vm_id = firecracker_vm::setup(&options, pid, vm_id).await?; 165 let vm = repo::virtual_machine::find(&pool, &vm_id) 166 .await?
+137 -40
crates/firecracker-prepare/src/lib.rs
··· 53 } 54 } 55 56 - pub fn prepare(distro: Distro, kernel_file: Option<String>) -> Result<()> { 57 let arch = run_command("uname", &["-m"], false)?.stdout; 58 let arch = String::from_utf8_lossy(&arch).trim().to_string(); 59 println!("[+] Detected architecture: {}", arch.bright_green()); ··· 85 Distro::Archlinux => Box::new(ArchlinuxPreparer), 86 }; 87 88 - let (kernel_file, img_file, ssh_key_file) = preparer.prepare(&arch, &app_dir, kernel_file)?; 89 90 extract_vmlinuz(&kernel_file)?; 91 92 println!("[✓] Kernel: {}", kernel_file.bright_green()); 93 println!("[✓] Rootfs: {}", img_file.bright_green()); 94 - println!("[✓] SSH Key: {}", ssh_key_file.bright_green()); 95 96 Ok(()) 97 } ··· 102 arch: &str, 103 app_dir: &str, 104 kernel_file: Option<String>, 105 - ) -> Result<(String, String, String)>; 106 fn name(&self) -> &'static str; 107 } 108 ··· 129 arch: &str, 130 app_dir: &str, 131 kernel_file: Option<String>, 132 - ) -> Result<(String, String, String)> { 133 println!( 134 "[+] Preparing {} rootfs for {}...", 135 self.name(), ··· 202 &["-p", &format!("{}/root/.ssh", debootstrap_dir)], 203 true, 204 )?; 205 - ssh::generate_and_copy_ssh_key(&ssh_key_name, &debootstrap_dir)?; 206 207 if !run_command("chroot", &[&debootstrap_dir, "which", "sshd"], true) 208 .map(|output| output.status.success()) ··· 236 rootfs::add_overlay_init(&debootstrap_dir)?; 237 rootfs::create_squashfs(&debootstrap_dir, &img_file)?; 238 239 - let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 240 241 Ok((kernel_file, img_file, ssh_key_file)) 242 } ··· 252 arch: &str, 253 app_dir: &str, 254 kernel_file: Option<String>, 255 - ) -> Result<(String, String, String)> { 256 println!( 257 "[+] Preparing {} rootfs for {}...", 258 self.name(), ··· 364 )?; 365 366 let ssh_key_name = "id_rsa"; 367 - ssh::generate_and_copy_ssh_key(&ssh_key_name, &minirootfs)?; 368 369 let img_file = format!("{}/alpine-rootfs.img", app_dir); 370 rootfs::create_squashfs(&minirootfs, &img_file)?; 371 372 - let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 373 374 Ok((kernel_file, img_file, ssh_key_file)) 375 } ··· 385 arch: &str, 386 app_dir: &str, 387 kernel_file: Option<String>, 388 - ) -> Result<(String, String, String)> { 389 println!( 390 "[+] Preparing {} rootfs for {}...", 391 self.name(), ··· 425 )?; 426 427 let ssh_key_name = "id_rsa"; 428 - ssh::generate_and_copy_ssh_key(&ssh_key_name, &squashfs_root_dir)?; 429 430 let img_file = format!("{}/ubuntu-rootfs.img", app_dir); 431 rootfs::create_overlay_dirs(&squashfs_root_dir)?; 432 rootfs::add_overlay_init(&squashfs_root_dir)?; 433 rootfs::create_squashfs(&squashfs_root_dir, &img_file)?; 434 435 - let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 436 437 Ok((kernel_file, img_file, ssh_key_file)) 438 } ··· 448 arch: &str, 449 app_dir: &str, 450 kernel_file: Option<String>, 451 - ) -> Result<(String, String, String)> { 452 println!( 453 "[+] Preparing {} rootfs for {}...", 454 self.name(), ··· 465 rootfs::extract_squashfs(&squashfs_file, &nixos_rootfs)?; 466 467 let ssh_key_name = "id_rsa"; 468 - ssh::generate_and_copy_ssh_key_nixos(&ssh_key_name, &nixos_rootfs)?; 469 470 let img_file = format!("{}/nixos-rootfs.img", app_dir); 471 rootfs::create_squashfs(&nixos_rootfs, &img_file)?; 472 473 - let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 474 475 println!( 476 "[+] {} rootfs prepared at: {}", ··· 492 arch: &str, 493 app_dir: &str, 494 kernel_file: Option<String>, 495 - ) -> Result<(String, String, String)> { 496 println!( 497 "[+] Preparing {} rootfs for {}...", 498 self.name(), ··· 516 )?; 517 518 let ssh_key_name = "id_rsa"; 519 - ssh::generate_and_copy_ssh_key(&ssh_key_name, &fedora_rootfs)?; 520 521 let img_file = format!("{}/fedora-rootfs.img", app_dir); 522 rootfs::create_squashfs(&fedora_rootfs, &img_file)?; 523 524 - let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 525 526 println!( 527 "[+] {} rootfs prepared at: {}", ··· 543 arch: &str, 544 app_dir: &str, 545 kernel_file: Option<String>, 546 - ) -> Result<(String, String, String)> { 547 println!( 548 "[+] Preparing {} rootfs for {}...", 549 self.name(), ··· 569 )?; 570 571 let ssh_key_name = "id_rsa"; 572 - ssh::generate_and_copy_ssh_key(&ssh_key_name, &gentoo_rootfs)?; 573 574 let img_file = format!("{}/gentoo-rootfs.img", app_dir); 575 rootfs::create_squashfs(&gentoo_rootfs, &img_file)?; 576 577 - let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 578 579 Ok((kernel_file, img_file, ssh_key_file)) 580 } ··· 590 arch: &str, 591 app_dir: &str, 592 kernel_file: Option<String>, 593 - ) -> Result<(String, String, String)> { 594 println!( 595 "[+] Preparing {} rootfs for {}...", 596 self.name(), ··· 621 )?; 622 623 let ssh_key_name = "id_rsa"; 624 - ssh::generate_and_copy_ssh_key(&ssh_key_name, &slackware_rootfs)?; 625 626 let img_file = format!("{}/slackware-rootfs.img", app_dir); 627 rootfs::create_squashfs(&slackware_rootfs, &img_file)?; 628 629 - let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 630 631 Ok((kernel_file, img_file, ssh_key_file)) 632 } ··· 642 arch: &str, 643 app_dir: &str, 644 kernel_file: Option<String>, 645 - ) -> Result<(String, String, String)> { 646 println!( 647 "[+] Preparing {} rootfs for {}...", 648 self.name(), ··· 667 )?; 668 669 let ssh_key_name = "id_rsa"; 670 - ssh::generate_and_copy_ssh_key(&ssh_key_name, &opensuse_rootfs)?; 671 672 let img_file = format!("{}/opensuse-rootfs.img", app_dir); 673 rootfs::create_squashfs(&opensuse_rootfs, &img_file)?; 674 675 - let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 676 Ok((kernel_file, img_file, ssh_key_file)) 677 } 678 } ··· 687 arch: &str, 688 app_dir: &str, 689 kernel_file: Option<String>, 690 - ) -> Result<(String, String, String)> { 691 println!( 692 "[+] Preparing {} rootfs for {}...", 693 self.name(), ··· 706 rootfs::extract_squashfs(&squashfs_file, &almalinux_rootfs)?; 707 708 let ssh_key_name = "id_rsa"; 709 - ssh::generate_and_copy_ssh_key(&ssh_key_name, &almalinux_rootfs)?; 710 711 let img_file = format!("{}/almalinux-rootfs.img", app_dir); 712 rootfs::create_squashfs(&almalinux_rootfs, &img_file)?; 713 714 - let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 715 716 Ok((kernel_file, img_file, ssh_key_file)) 717 } ··· 727 arch: &str, 728 app_dir: &str, 729 kernel_file: Option<String>, 730 - ) -> Result<(String, String, String)> { 731 println!( 732 "[+] Preparing {} rootfs for {}...", 733 self.name(), ··· 746 rootfs::extract_squashfs(&squashfs_file, &rockylinux_rootfs)?; 747 748 let ssh_key_name = "id_rsa"; 749 - ssh::generate_and_copy_ssh_key(&ssh_key_name, &rockylinux_rootfs)?; 750 751 let img_file = format!("{}/rockylinux-rootfs.img", app_dir); 752 rootfs::create_squashfs(&rockylinux_rootfs, &img_file)?; 753 754 - let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 755 756 Ok((kernel_file, img_file, ssh_key_file)) 757 } ··· 767 arch: &str, 768 app_dir: &str, 769 kernel_file: Option<String>, 770 - ) -> Result<(String, String, String)> { 771 println!( 772 "[+] Preparing {} rootfs for {}...", 773 self.name(), ··· 796 )?; 797 798 let ssh_key_name = "id_rsa"; 799 - ssh::generate_and_copy_ssh_key(&ssh_key_name, &archlinux_rootfs)?; 800 801 let img_file = format!("{}/archlinux-rootfs.img", app_dir); 802 rootfs::create_squashfs(&archlinux_rootfs, &img_file)?; 803 804 - let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 805 806 Ok((kernel_file, img_file, ssh_key_file)) 807 } ··· 817 arch: &str, 818 app_dir: &str, 819 kernel_file: Option<String>, 820 - ) -> Result<(String, String, String)> { 821 println!( 822 "[+] Preparing {} rootfs for {}...", 823 self.name(), ··· 842 )?; 843 844 let ssh_key_name = "id_rsa"; 845 - ssh::generate_and_copy_ssh_key(&ssh_key_name, &opensuse_rootfs)?; 846 847 let img_file = format!("{}/opensuse-tumbleweed-rootfs.img", app_dir); 848 rootfs::create_squashfs(&opensuse_rootfs, &img_file)?; 849 850 - let ssh_key_file = format!("{}/{}", app_dir, ssh_key_name); 851 Ok((kernel_file, img_file, ssh_key_file)) 852 } 853 }
··· 53 } 54 } 55 56 + pub fn prepare( 57 + distro: Distro, 58 + kernel_file: Option<String>, 59 + ssh_keys: Option<Vec<String>>, 60 + ) -> Result<()> { 61 let arch = run_command("uname", &["-m"], false)?.stdout; 62 let arch = String::from_utf8_lossy(&arch).trim().to_string(); 63 println!("[+] Detected architecture: {}", arch.bright_green()); ··· 89 Distro::Archlinux => Box::new(ArchlinuxPreparer), 90 }; 91 92 + let (kernel_file, img_file, ssh_key_file) = 93 + preparer.prepare(&arch, &app_dir, kernel_file, ssh_keys)?; 94 95 extract_vmlinuz(&kernel_file)?; 96 97 println!("[✓] Kernel: {}", kernel_file.bright_green()); 98 println!("[✓] Rootfs: {}", img_file.bright_green()); 99 + match ssh_key_file { 100 + None => println!("[✓] SSH Keys: User provided"), 101 + Some(ssh_key_file) => println!("[✓] SSH Key: {}", ssh_key_file.bright_green()), 102 + } 103 104 Ok(()) 105 } ··· 110 arch: &str, 111 app_dir: &str, 112 kernel_file: Option<String>, 113 + ssh_keys: Option<Vec<String>>, 114 + ) -> Result<(String, String, Option<String>)>; 115 fn name(&self) -> &'static str; 116 } 117 ··· 138 arch: &str, 139 app_dir: &str, 140 kernel_file: Option<String>, 141 + ssh_keys: Option<Vec<String>>, 142 + ) -> Result<(String, String, Option<String>)> { 143 println!( 144 "[+] Preparing {} rootfs for {}...", 145 self.name(), ··· 212 &["-p", &format!("{}/root/.ssh", debootstrap_dir)], 213 true, 214 )?; 215 + match ssh_keys { 216 + Some(ref keys) => ssh::copy_ssh_keys(keys, &debootstrap_dir)?, 217 + None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &debootstrap_dir)?, 218 + } 219 220 if !run_command("chroot", &[&debootstrap_dir, "which", "sshd"], true) 221 .map(|output| output.status.success()) ··· 249 rootfs::add_overlay_init(&debootstrap_dir)?; 250 rootfs::create_squashfs(&debootstrap_dir, &img_file)?; 251 252 + let ssh_key_file = match ssh_keys { 253 + Some(_) => None, 254 + None => Some(format!("{}/{}", app_dir, ssh_key_name)), 255 + }; 256 257 Ok((kernel_file, img_file, ssh_key_file)) 258 } ··· 268 arch: &str, 269 app_dir: &str, 270 kernel_file: Option<String>, 271 + ssh_keys: Option<Vec<String>>, 272 + ) -> Result<(String, String, Option<String>)> { 273 println!( 274 "[+] Preparing {} rootfs for {}...", 275 self.name(), ··· 381 )?; 382 383 let ssh_key_name = "id_rsa"; 384 + match ssh_keys { 385 + Some(ref keys) => ssh::copy_ssh_keys(keys, &minirootfs)?, 386 + None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &minirootfs)?, 387 + } 388 389 let img_file = format!("{}/alpine-rootfs.img", app_dir); 390 rootfs::create_squashfs(&minirootfs, &img_file)?; 391 392 + let ssh_key_file = match ssh_keys { 393 + Some(_) => None, 394 + None => Some(format!("{}/{}", app_dir, ssh_key_name)), 395 + }; 396 397 Ok((kernel_file, img_file, ssh_key_file)) 398 } ··· 408 arch: &str, 409 app_dir: &str, 410 kernel_file: Option<String>, 411 + ssh_keys: Option<Vec<String>>, 412 + ) -> Result<(String, String, Option<String>)> { 413 println!( 414 "[+] Preparing {} rootfs for {}...", 415 self.name(), ··· 449 )?; 450 451 let ssh_key_name = "id_rsa"; 452 + match ssh_keys { 453 + Some(ref keys) => ssh::copy_ssh_keys(keys, &squashfs_root_dir)?, 454 + None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &squashfs_root_dir)?, 455 + } 456 457 let img_file = format!("{}/ubuntu-rootfs.img", app_dir); 458 rootfs::create_overlay_dirs(&squashfs_root_dir)?; 459 rootfs::add_overlay_init(&squashfs_root_dir)?; 460 rootfs::create_squashfs(&squashfs_root_dir, &img_file)?; 461 462 + let ssh_key_file = match ssh_keys { 463 + Some(_) => None, 464 + None => Some(format!("{}/{}", app_dir, ssh_key_name)), 465 + }; 466 467 Ok((kernel_file, img_file, ssh_key_file)) 468 } ··· 478 arch: &str, 479 app_dir: &str, 480 kernel_file: Option<String>, 481 + ssh_keys: Option<Vec<String>>, 482 + ) -> Result<(String, String, Option<String>)> { 483 println!( 484 "[+] Preparing {} rootfs for {}...", 485 self.name(), ··· 496 rootfs::extract_squashfs(&squashfs_file, &nixos_rootfs)?; 497 498 let ssh_key_name = "id_rsa"; 499 + match ssh_keys { 500 + Some(ref keys) => ssh::copy_ssh_keys(keys, &nixos_rootfs)?, 501 + None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &nixos_rootfs)?, 502 + } 503 504 let img_file = format!("{}/nixos-rootfs.img", app_dir); 505 rootfs::create_squashfs(&nixos_rootfs, &img_file)?; 506 507 + let ssh_key_file = match ssh_keys { 508 + Some(_) => None, 509 + None => Some(format!("{}/{}", app_dir, ssh_key_name)), 510 + }; 511 512 println!( 513 "[+] {} rootfs prepared at: {}", ··· 529 arch: &str, 530 app_dir: &str, 531 kernel_file: Option<String>, 532 + ssh_keys: Option<Vec<String>>, 533 + ) -> Result<(String, String, Option<String>)> { 534 println!( 535 "[+] Preparing {} rootfs for {}...", 536 self.name(), ··· 554 )?; 555 556 let ssh_key_name = "id_rsa"; 557 + match ssh_keys { 558 + Some(ref keys) => ssh::copy_ssh_keys(keys, &fedora_rootfs)?, 559 + None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &fedora_rootfs)?, 560 + } 561 562 let img_file = format!("{}/fedora-rootfs.img", app_dir); 563 rootfs::create_squashfs(&fedora_rootfs, &img_file)?; 564 565 + let ssh_key_file = match ssh_keys { 566 + Some(_) => None, 567 + None => Some(format!("{}/{}", app_dir, ssh_key_name)), 568 + }; 569 570 println!( 571 "[+] {} rootfs prepared at: {}", ··· 587 arch: &str, 588 app_dir: &str, 589 kernel_file: Option<String>, 590 + ssh_keys: Option<Vec<String>>, 591 + ) -> Result<(String, String, Option<String>)> { 592 println!( 593 "[+] Preparing {} rootfs for {}...", 594 self.name(), ··· 614 )?; 615 616 let ssh_key_name = "id_rsa"; 617 + match ssh_keys { 618 + Some(ref keys) => ssh::copy_ssh_keys(keys, &gentoo_rootfs)?, 619 + None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &gentoo_rootfs)?, 620 + } 621 622 let img_file = format!("{}/gentoo-rootfs.img", app_dir); 623 rootfs::create_squashfs(&gentoo_rootfs, &img_file)?; 624 625 + let ssh_key_file = match ssh_keys { 626 + Some(_) => None, 627 + None => Some(format!("{}/{}", app_dir, ssh_key_name)), 628 + }; 629 630 Ok((kernel_file, img_file, ssh_key_file)) 631 } ··· 641 arch: &str, 642 app_dir: &str, 643 kernel_file: Option<String>, 644 + ssh_keys: Option<Vec<String>>, 645 + ) -> Result<(String, String, Option<String>)> { 646 println!( 647 "[+] Preparing {} rootfs for {}...", 648 self.name(), ··· 673 )?; 674 675 let ssh_key_name = "id_rsa"; 676 + match ssh_keys { 677 + Some(ref keys) => ssh::copy_ssh_keys(keys, &slackware_rootfs)?, 678 + None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &slackware_rootfs)?, 679 + } 680 681 let img_file = format!("{}/slackware-rootfs.img", app_dir); 682 rootfs::create_squashfs(&slackware_rootfs, &img_file)?; 683 684 + let ssh_key_file = match ssh_keys { 685 + Some(_) => None, 686 + None => Some(format!("{}/{}", app_dir, ssh_key_name)), 687 + }; 688 689 Ok((kernel_file, img_file, ssh_key_file)) 690 } ··· 700 arch: &str, 701 app_dir: &str, 702 kernel_file: Option<String>, 703 + ssh_keys: Option<Vec<String>>, 704 + ) -> Result<(String, String, Option<String>)> { 705 println!( 706 "[+] Preparing {} rootfs for {}...", 707 self.name(), ··· 726 )?; 727 728 let ssh_key_name = "id_rsa"; 729 + match ssh_keys { 730 + Some(ref keys) => ssh::copy_ssh_keys(keys, &opensuse_rootfs)?, 731 + None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &opensuse_rootfs)?, 732 + } 733 734 let img_file = format!("{}/opensuse-rootfs.img", app_dir); 735 rootfs::create_squashfs(&opensuse_rootfs, &img_file)?; 736 737 + let ssh_key_file = match ssh_keys { 738 + Some(_) => None, 739 + None => Some(format!("{}/{}", app_dir, ssh_key_name)), 740 + }; 741 + 742 Ok((kernel_file, img_file, ssh_key_file)) 743 } 744 } ··· 753 arch: &str, 754 app_dir: &str, 755 kernel_file: Option<String>, 756 + ssh_keys: Option<Vec<String>>, 757 + ) -> Result<(String, String, Option<String>)> { 758 println!( 759 "[+] Preparing {} rootfs for {}...", 760 self.name(), ··· 773 rootfs::extract_squashfs(&squashfs_file, &almalinux_rootfs)?; 774 775 let ssh_key_name = "id_rsa"; 776 + match ssh_keys { 777 + Some(ref keys) => ssh::copy_ssh_keys(keys, &almalinux_rootfs)?, 778 + None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &almalinux_rootfs)?, 779 + } 780 781 let img_file = format!("{}/almalinux-rootfs.img", app_dir); 782 rootfs::create_squashfs(&almalinux_rootfs, &img_file)?; 783 784 + let ssh_key_file = match ssh_keys { 785 + Some(_) => None, 786 + None => Some(format!("{}/{}", app_dir, ssh_key_name)), 787 + }; 788 789 Ok((kernel_file, img_file, ssh_key_file)) 790 } ··· 800 arch: &str, 801 app_dir: &str, 802 kernel_file: Option<String>, 803 + ssh_keys: Option<Vec<String>>, 804 + ) -> Result<(String, String, Option<String>)> { 805 println!( 806 "[+] Preparing {} rootfs for {}...", 807 self.name(), ··· 820 rootfs::extract_squashfs(&squashfs_file, &rockylinux_rootfs)?; 821 822 let ssh_key_name = "id_rsa"; 823 + match ssh_keys { 824 + Some(ref keys) => ssh::copy_ssh_keys(keys, &rockylinux_rootfs)?, 825 + None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &rockylinux_rootfs)?, 826 + } 827 828 let img_file = format!("{}/rockylinux-rootfs.img", app_dir); 829 rootfs::create_squashfs(&rockylinux_rootfs, &img_file)?; 830 831 + let ssh_key_file = match ssh_keys { 832 + Some(_) => None, 833 + None => Some(format!("{}/{}", app_dir, ssh_key_name)), 834 + }; 835 836 Ok((kernel_file, img_file, ssh_key_file)) 837 } ··· 847 arch: &str, 848 app_dir: &str, 849 kernel_file: Option<String>, 850 + ssh_keys: Option<Vec<String>>, 851 + ) -> Result<(String, String, Option<String>)> { 852 println!( 853 "[+] Preparing {} rootfs for {}...", 854 self.name(), ··· 877 )?; 878 879 let ssh_key_name = "id_rsa"; 880 + match ssh_keys { 881 + Some(ref keys) => ssh::copy_ssh_keys(keys, &archlinux_rootfs)?, 882 + None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &archlinux_rootfs)?, 883 + } 884 885 let img_file = format!("{}/archlinux-rootfs.img", app_dir); 886 + 887 rootfs::create_squashfs(&archlinux_rootfs, &img_file)?; 888 889 + let ssh_key_file = match ssh_keys { 890 + Some(_) => None, 891 + None => Some(format!("{}/{}", app_dir, ssh_key_name)), 892 + }; 893 894 Ok((kernel_file, img_file, ssh_key_file)) 895 } ··· 905 arch: &str, 906 app_dir: &str, 907 kernel_file: Option<String>, 908 + ssh_keys: Option<Vec<String>>, 909 + ) -> Result<(String, String, Option<String>)> { 910 println!( 911 "[+] Preparing {} rootfs for {}...", 912 self.name(), ··· 931 )?; 932 933 let ssh_key_name = "id_rsa"; 934 + 935 + match ssh_keys { 936 + Some(ref keys) => ssh::copy_ssh_keys(keys, &opensuse_rootfs)?, 937 + None => ssh::generate_and_copy_ssh_key(&ssh_key_name, &opensuse_rootfs)?, 938 + } 939 940 let img_file = format!("{}/opensuse-tumbleweed-rootfs.img", app_dir); 941 rootfs::create_squashfs(&opensuse_rootfs, &img_file)?; 942 943 + let ssh_key_file = match ssh_keys { 944 + Some(_) => None, 945 + None => Some(format!("{}/{}", app_dir, ssh_key_name)), 946 + }; 947 + 948 Ok((kernel_file, img_file, ssh_key_file)) 949 } 950 }
+66 -2
crates/firecracker-prepare/src/ssh.rs
··· 30 Ok(()) 31 } 32 33 - pub fn generate_and_copy_ssh_key_nixos(key_name: &str, squashfs_root_dir: &str) -> Result<()> { 34 let app_dir = crate::config::get_config_dir()?; 35 const DEFAULT_SSH: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAR4Gvuv3lTpXIYeZTRO22nVEj64uMmlDAdt5+GG80hm tsiry@tsiry-XPS-9320"; 36 ··· 54 ], 55 true, 56 )?; 57 - return Ok(()); 58 } 59 60 let key_name = format!("{}/{}", app_dir, key_name); ··· 75 ], 76 true, 77 )?; 78 Ok(()) 79 }
··· 30 Ok(()) 31 } 32 33 + pub fn generate_and_copy_ssh_key_nixos(key_name: &str, squashfs_root_dir: &str) -> Result<String> { 34 let app_dir = crate::config::get_config_dir()?; 35 const DEFAULT_SSH: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAR4Gvuv3lTpXIYeZTRO22nVEj64uMmlDAdt5+GG80hm tsiry@tsiry-XPS-9320"; 36 ··· 54 ], 55 true, 56 )?; 57 + 58 + run_command( 59 + "mkdir", 60 + &["-p", &format!("{}/root/.ssh", squashfs_root_dir)], 61 + true, 62 + )?; 63 + run_command( 64 + "cp", 65 + &[ 66 + &pub_key_path, 67 + &format!("{}/root/.ssh/authorized_keys", squashfs_root_dir), 68 + ], 69 + true, 70 + )?; 71 + 72 + return Ok(public_key); 73 } 74 75 let key_name = format!("{}/{}", app_dir, key_name); ··· 90 ], 91 true, 92 )?; 93 + 94 + run_command( 95 + "mkdir", 96 + &["-p", &format!("{}/root/.ssh", squashfs_root_dir)], 97 + true, 98 + )?; 99 + run_command( 100 + "cp", 101 + &[ 102 + &pub_key_path, 103 + &format!("{}/root/.ssh/authorized_keys", squashfs_root_dir), 104 + ], 105 + true, 106 + )?; 107 + 108 + Ok(public_key) 109 + } 110 + 111 + pub fn copy_ssh_keys(ssh_keys: &[String], squashfs_root_dir: &str) -> Result<()> { 112 + run_command( 113 + "mkdir", 114 + &["-p", &format!("{}/root/.ssh", squashfs_root_dir)], 115 + true, 116 + )?; 117 + 118 + let auth_keys_path = "/tmp/authorized_keys"; 119 + let mut auth_keys_file = std::fs::OpenOptions::new() 120 + .create(true) 121 + .append(true) 122 + .open(&auth_keys_path) 123 + .map_err(|e| anyhow::anyhow!("Failed to open authorized_keys file: {}", e))?; 124 + 125 + for key in ssh_keys { 126 + use std::io::Write; 127 + writeln!(auth_keys_file, "{}", key) 128 + .map_err(|e| anyhow::anyhow!("Failed to write to authorized_keys file: {}", e))?; 129 + } 130 + 131 + run_command( 132 + "cp", 133 + &[ 134 + &auth_keys_path, 135 + &format!("{}/root/.ssh/authorized_keys", squashfs_root_dir), 136 + ], 137 + true, 138 + )?; 139 + std::fs::remove_file(&auth_keys_path) 140 + .map_err(|e| anyhow::anyhow!("Failed to remove temporary authorized_keys file: {}", e))?; 141 + 142 Ok(()) 143 }
+3
crates/firecracker-state/migrations/20250917153615_add_ssh_keys.sql
···
··· 1 + -- Add migration script here 2 + ALTER TABLE virtual_machines 3 + ADD COLUMN ssh_keys TEXT;
+1
crates/firecracker-state/src/entity/virtual_machine.rs
··· 19 pub vmlinux: Option<String>, 20 pub rootfs: Option<String>, 21 pub bootargs: Option<String>, 22 #[serde(with = "chrono::serde::ts_seconds")] 23 pub created_at: DateTime<Utc>, 24 #[serde(with = "chrono::serde::ts_seconds")]
··· 19 pub vmlinux: Option<String>, 20 pub rootfs: Option<String>, 21 pub bootargs: Option<String>, 22 + pub ssh_keys: Option<String>, 23 #[serde(with = "chrono::serde::ts_seconds")] 24 pub created_at: DateTime<Utc>, 25 #[serde(with = "chrono::serde::ts_seconds")]
+15
crates/firecracker-state/src/lib.rs
··· 52 )) 53 .await?; 54 55 sqlx::query("PRAGMA journal_mode=WAL") 56 .execute(&pool) 57 .await?;
··· 52 )) 53 .await?; 54 55 + match pool 56 + .execute(include_str!( 57 + "../migrations/20250917153615_add_ssh_keys.sql" 58 + )) 59 + .await 60 + { 61 + Ok(_) => (), 62 + Err(e) => { 63 + if e.to_string().contains("duplicate column name: ssh_keys") { 64 + } else { 65 + return Err(anyhow!("Failed to apply migration: {}", e)); 66 + } 67 + } 68 + } 69 + 70 sqlx::query("PRAGMA journal_mode=WAL") 71 .execute(&pool) 72 .await?;
+4 -2
crates/firecracker-state/src/repo/virtual_machine.rs
··· 64 ip_address, 65 vmlinux, 66 rootfs, 67 - bootargs 68 - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 69 ) 70 .bind(&vm.name) 71 .bind(&id) ··· 83 .bind(&vm.vmlinux) 84 .bind(&vm.rootfs) 85 .bind(&vm.bootargs) 86 .execute(pool) 87 .await 88 .with_context(|| "Failed to create virtual machine")?;
··· 64 ip_address, 65 vmlinux, 66 rootfs, 67 + bootargs, 68 + ssh_keys 69 + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 70 ) 71 .bind(&vm.name) 72 .bind(&id) ··· 84 .bind(&vm.vmlinux) 85 .bind(&vm.rootfs) 86 .bind(&vm.bootargs) 87 + .bind(&vm.ssh_keys) 88 .execute(pool) 89 .await 90 .with_context(|| "Failed to create virtual machine")?;
+59 -20
crates/firecracker-up/src/cmd/reset.rs
··· 1 use anyhow::Error; 2 use firecracker_process::stop; 3 use firecracker_vm::types::VmOptions; 4 use glob::glob; 5 use owo_colors::OwoColorize; 6 7 pub async fn reset(options: VmOptions) -> Result<(), Error> { 8 - println!( 9 - "Are you sure you want to reset? This will remove all ext4 files. Type '{}' to confirm:", 10 - "yes".bright_green() 11 - ); 12 let mut input = String::new(); 13 std::io::stdin() 14 .read_line(&mut input) 15 .map_err(|e| Error::msg(format!("Failed to read input: {}", e)))?; 16 let input = input.trim(); 17 - 18 if input != "yes" { 19 println!("Reset cancelled."); 20 return Ok(()); 21 } 22 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?; 30 31 - let app_dir = crate::config::get_config_dir()?; 32 - let ext4_file = glob(format!("{}/*.ext4", app_dir).as_str()) 33 - .map_err(|e| Error::msg(format!("Failed to find ext4 file: {}", e)))?; 34 35 - for file in ext4_file { 36 - if let Ok(path) = file { 37 - std::fs::remove_file(path) 38 - .map_err(|e| Error::msg(format!("Failed to remove file: {}", e)))?; 39 - } 40 } 41 42 - println!("[+] Reset complete. All ext4 files have been removed."); 43 println!( 44 "[+] You can now run '{}' to start a new Firecracker MicroVM.", 45 "fireup".bright_green()
··· 1 + use std::process; 2 + 3 use anyhow::Error; 4 use firecracker_process::stop; 5 + use firecracker_state::repo; 6 use firecracker_vm::types::VmOptions; 7 use glob::glob; 8 use owo_colors::OwoColorize; 9 + 10 + use crate::command::run_command; 11 12 pub async fn reset(options: VmOptions) -> Result<(), Error> { 13 + let name = options 14 + .api_socket 15 + .trim_start_matches("/tmp/firecracker-") 16 + .trim_end_matches(".sock") 17 + .to_string(); 18 + 19 + if options.api_socket.is_empty() { 20 + println!( 21 + "Are you sure you want to reset? This will remove all *.img files. Type '{}' to confirm:", 22 + "yes".bright_green() 23 + ); 24 + let mut input = String::new(); 25 + std::io::stdin() 26 + .read_line(&mut input) 27 + .map_err(|e| Error::msg(format!("Failed to read input: {}", e)))?; 28 + let input = input.trim(); 29 + 30 + if input != "yes" { 31 + println!("Reset cancelled."); 32 + return Ok(()); 33 + } 34 + 35 + stop(Some(name)).await?; 36 + 37 + let app_dir = crate::config::get_config_dir()?; 38 + let img_file = glob(format!("{}/*.img", app_dir).as_str()) 39 + .map_err(|e| Error::msg(format!("Failed to find img file: {}", e)))?; 40 + 41 + for file in img_file { 42 + if let Ok(path) = file { 43 + run_command("rm", &[path.to_str().unwrap_or_default()], true)?; 44 + } 45 + } 46 + 47 + println!("[+] Reset complete. All *.img files have been removed."); 48 + println!( 49 + "[+] You can now run '{}' to start a new Firecracker MicroVM.", 50 + "fireup".bright_green() 51 + ); 52 + return Ok(()); 53 + } 54 + 55 + println!("Are you sure you want to reset the VM {}? This will remove its associated {} file. Type '{}' to confirm:", name.cyan(), "*.img".cyan(), "yes".bright_green()); 56 let mut input = String::new(); 57 std::io::stdin() 58 .read_line(&mut input) 59 .map_err(|e| Error::msg(format!("Failed to read input: {}", e)))?; 60 let input = input.trim(); 61 if input != "yes" { 62 println!("Reset cancelled."); 63 return Ok(()); 64 } 65 66 + let pool = firecracker_state::create_connection_pool().await?; 67 68 + let vm = repo::virtual_machine::find_by_api_socket(&pool, &options.api_socket).await?; 69 + if vm.is_none() { 70 + println!("[!] No virtual machine found with name: {}", name); 71 + process::exit(1); 72 + } 73 74 + let vm = vm.unwrap(); 75 + stop(Some(vm.name.clone())).await?; 76 77 + if let Some(rootfs) = &vm.rootfs { 78 + run_command("rm", &[rootfs], true)?; 79 } 80 81 + println!("[+] Reset complete. Associated *.img files have been removed."); 82 println!( 83 "[+] You can now run '{}' to start a new Firecracker MicroVM.", 84 "fireup".bright_green()
+3
crates/firecracker-up/src/cmd/start.rs
··· 43 api_socket: vm.api_socket, 44 mac_address: vm.mac_address, 45 etcd, 46 }) 47 .await?; 48
··· 43 api_socket: vm.api_socket, 44 mac_address: vm.mac_address, 45 etcd, 46 + ssh_keys: vm 47 + .ssh_keys 48 + .map(|keys| keys.split(',').map(|s| s.to_string()).collect()), 49 }) 50 .await?; 51
+5 -1
crates/firecracker-up/src/cmd/up.rs
··· 78 } 79 } 80 81 - firecracker_prepare::prepare(options.clone().into(), options.vmlinux.clone())?; 82 firecracker_vm::setup(&options, pid, vm_id).await?; 83 Ok(()) 84 }
··· 78 } 79 } 80 81 + firecracker_prepare::prepare( 82 + options.clone().into(), 83 + options.vmlinux.clone(), 84 + options.ssh_keys.clone(), 85 + )?; 86 firecracker_vm::setup(&options, pid, vm_id).await?; 87 Ok(()) 88 }
+13 -2
crates/firecracker-up/src/main.rs
··· 240 .get_one::<String>("mac-address") 241 .cloned() 242 .unwrap_or(default_mac); 243 let options = VmOptions { 244 debian: args.get_one::<bool>("debian").copied(), 245 alpine: args.get_one::<bool>("alpine").copied(), ··· 263 api_socket, 264 mac_address, 265 etcd: None, 266 }; 267 up(options).await? 268 } ··· 280 ssh(pool, name).await? 281 } 282 Some(("reset", args)) => { 283 - let name = args.get_one::<String>("name").cloned().unwrap(); 284 - let api_socket = format!("/tmp/firecracker-{}.sock", name); 285 reset(VmOptions { 286 api_socket, 287 ..Default::default() ··· 348 .get_one::<String>("mac-address") 349 .cloned() 350 .unwrap_or(default_mac); 351 352 let options = VmOptions { 353 debian: Some(debian), ··· 372 api_socket, 373 mac_address, 374 etcd: None, 375 }; 376 up(options).await? 377 }
··· 240 .get_one::<String>("mac-address") 241 .cloned() 242 .unwrap_or(default_mac); 243 + let ssh_keys = args 244 + .get_one::<String>("ssh-keys") 245 + .map(|s| s.split(',').map(|s| s.trim().to_string()).collect()); 246 let options = VmOptions { 247 debian: args.get_one::<bool>("debian").copied(), 248 alpine: args.get_one::<bool>("alpine").copied(), ··· 266 api_socket, 267 mac_address, 268 etcd: None, 269 + ssh_keys, 270 }; 271 up(options).await? 272 } ··· 284 ssh(pool, name).await? 285 } 286 Some(("reset", args)) => { 287 + let name = args.get_one::<String>("name").cloned(); 288 + let api_socket = match name { 289 + Some(name) => format!("/tmp/firecracker-{}.sock", name), 290 + None => String::from(""), 291 + }; 292 reset(VmOptions { 293 api_socket, 294 ..Default::default() ··· 355 .get_one::<String>("mac-address") 356 .cloned() 357 .unwrap_or(default_mac); 358 + let ssh_keys = matches 359 + .get_one::<String>("ssh-keys") 360 + .map(|s| s.split(',').map(|s| s.trim().to_string()).collect()); 361 362 let options = VmOptions { 363 debian: Some(debian), ··· 382 api_socket, 383 mac_address, 384 etcd: None, 385 + ssh_keys, 386 }; 387 up(options).await? 388 }
+2
crates/firecracker-vm/src/lib.rs
··· 165 vmlinux: Some(kernel), 166 rootfs: Some(rootfs), 167 bootargs: options.bootargs.clone(), 168 ..Default::default() 169 }, 170 ) ··· 190 vmlinux: Some(kernel), 191 rootfs: Some(rootfs), 192 bootargs: options.bootargs.clone(), 193 ..Default::default() 194 }, 195 )
··· 165 vmlinux: Some(kernel), 166 rootfs: Some(rootfs), 167 bootargs: options.bootargs.clone(), 168 + ssh_keys: options.ssh_keys.as_ref().map(|keys| keys.join(",")), 169 ..Default::default() 170 }, 171 ) ··· 191 vmlinux: Some(kernel), 192 rootfs: Some(rootfs), 193 bootargs: options.bootargs.clone(), 194 + ssh_keys: options.ssh_keys.as_ref().map(|keys| keys.join(",")), 195 ..Default::default() 196 }, 197 )
+2
crates/firecracker-vm/src/types.rs
··· 27 pub api_socket: String, 28 pub mac_address: String, 29 pub etcd: Option<EtcdConfig>, 30 } 31 32 impl From<FireConfig> for VmOptions { ··· 55 api_socket: vm.api_socket.unwrap_or(FIRECRACKER_SOCKET.into()), 56 mac_address: vm.mac.unwrap_or(FC_MAC.into()), 57 etcd: config.etcd.clone(), 58 } 59 } 60 }
··· 27 pub api_socket: String, 28 pub mac_address: String, 29 pub etcd: Option<EtcdConfig>, 30 + pub ssh_keys: Option<Vec<String>>, 31 } 32 33 impl From<FireConfig> for VmOptions { ··· 56 api_socket: vm.api_socket.unwrap_or(FIRECRACKER_SOCKET.into()), 57 mac_address: vm.mac.unwrap_or(FC_MAC.into()), 58 etcd: config.etcd.clone(), 59 + ssh_keys: vm.ssh_keys.clone(), 60 } 61 } 62 }