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