├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── .gitignore └── simple_vm.rs └── src ├── config ├── drive.rs ├── jailer.rs ├── machine.rs ├── mod.rs ├── network.rs └── vsock.rs ├── error.rs ├── lib.rs └── machine.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Nightly lints 4 | 5 | jobs: 6 | clippy: 7 | name: Clippy 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout sources 11 | uses: actions/checkout@v2 12 | 13 | - name: Install nightly toolchain with clippy available 14 | uses: actions-rs/toolchain@v1 15 | with: 16 | profile: minimal 17 | toolchain: nightly 18 | override: true 19 | components: clippy 20 | 21 | - name: Run cargo clippy 22 | uses: actions-rs/cargo@v1 23 | with: 24 | command: clippy 25 | args: -- -D warnings 26 | 27 | rustfmt: 28 | name: Format 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout sources 32 | uses: actions/checkout@v2 33 | 34 | - name: Install nightly toolchain with rustfmt available 35 | uses: actions-rs/toolchain@v1 36 | with: 37 | profile: minimal 38 | toolchain: nightly 39 | override: true 40 | components: rustfmt 41 | 42 | - name: Run cargo fmt 43 | uses: actions-rs/cargo@v1 44 | with: 45 | command: fmt 46 | args: --all -- --check 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "firec" 3 | description = "Rust API to interact with Firecracker" 4 | version = "0.2.0" 5 | edition = "2021" 6 | keywords = ["firecracker", "unix", "linux", "microvm", "IPC"] 7 | categories = ["os::linux-apis", "virtualization", "web-programming::http-client"] 8 | license = "Apache-2.0" 9 | repository = "https://github.com/blockjoy/firec/" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | derivative = "2.2.0" 15 | futures-util = "0.3.25" 16 | hyper = {version = "0.14.23", features = ["client", "http2"]} 17 | hyperlocal = "0.8.0" 18 | serde = {version = "1.0.152", features = ["derive"]} 19 | serde_json = "1.0.91" 20 | sysinfo = "0.27.7" 21 | thiserror = "1.0.38" 22 | tokio = {version = "1.24.2", features = ["process", "net", "fs", "rt", "time"]} 23 | tracing = "0.1.37" 24 | users = "0.11.0" 25 | uuid = {version = "1.2.2", features = ["serde", "v4"]} 26 | 27 | [dev-dependencies] 28 | doc-comment = "0.3.3" 29 | tokio = {version = "1.24.2", features = ["rt", "macros"]} 30 | reqwest = "0.11.15" 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # firec 2 | 3 | [![](https://docs.rs/firec/badge.svg)](https://docs.rs/firec/) [![](https://img.shields.io/crates/v/firec)](https://crates.io/crates/firec) 4 | 5 | `firec` (pronounced "fyrek") is Rust client library to interact with [Firecracker]. It allows you to 6 | create, manipulate, query and stop VMMs. 7 | 8 | ## Examples 9 | 10 | You can see implementations in the [`examples`](./examples/) directory. 11 | 12 | ## status 13 | 14 | Currently heavily in development and therefore expect a lot of API breakage for a while. 15 | 16 | Having said that, we'll be following Cargo's SemVer rules so breaking changes will be released in 17 | new minor releases. However, we will only support the latest release. 18 | 19 | [Firecracker]: https://github.com/firecracker-microvm/firecracker/ 20 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | simple_vm/ -------------------------------------------------------------------------------- /examples/simple_vm.rs: -------------------------------------------------------------------------------- 1 | use firec::{ 2 | config::{network::Interface, Config}, 3 | Machine, 4 | }; 5 | use std::{ 6 | fs::File, 7 | io::copy, 8 | path::{Path, PathBuf}, 9 | }; 10 | use tokio::time::{sleep, Duration}; 11 | 12 | /// This example shows how to create a simple VM with a single vCPU, 1024 MiB of RAM, a root drive and a network interface. 13 | /// 14 | /// Requirements: 15 | /// - Firecracker binary at `/usr/bin/firecracker` 16 | /// - Jailer binary at `/usr/bin/jailer` 17 | /// - KVM enabled on your system 18 | /// 19 | /// 20 | /// It downloads the kernel and rootfs from the Firecracker Quickstart Guide, and use them to boot the VM, be aware that a few 21 | /// hundred MiB of disk space will be used. Once you're done with the example, you can delete the `./examples/simple_vm` directory. 22 | /// 23 | /// It uses the jailer feature from Firecracker for enhanced security, you can learn more about it here: 24 | /// https://github.com/firecracker-microvm/firecracker/blob/main/docs/jailer.md 25 | 26 | // URLs used are from the Firecracker Quickstart Guide 27 | // ref: https://github.com/firecracker-microvm/firecracker/blob/main/docs/getting-started.md#running-firecracker 28 | fn kernel_url() -> hyper::Uri { 29 | format!( 30 | "https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/{}/kernels/vmlinux.bin", 31 | std::env::consts::ARCH 32 | ) 33 | .parse::() 34 | .unwrap() 35 | } 36 | 37 | // URLs used are from the Firecracker Quickstart Guide 38 | // ref: https://github.com/firecracker-microvm/firecracker/blob/main/docs/getting-started.md#running-firecracker 39 | fn rootfs_url() -> hyper::Uri { 40 | format!( 41 | "https://s3.amazonaws.com/spec.ccfc.min/ci-artifacts/disks/{}/ubuntu-18.04.ext4", 42 | std::env::consts::ARCH 43 | ) 44 | .parse::() 45 | .unwrap() 46 | } 47 | 48 | async fn fetch_url(url: hyper::Uri, target_path: PathBuf) { 49 | if target_path.exists() { 50 | println!("File already exists, skipping download"); 51 | return; 52 | } 53 | 54 | let client = reqwest::Client::new(); 55 | let response = client 56 | .get(url.to_string()) 57 | .send() 58 | .await 59 | .expect("Could not download file"); 60 | let mut file = File::create(target_path).expect("Could not create file"); 61 | 62 | copy( 63 | &mut response 64 | .bytes() 65 | .await 66 | .expect("Could not get bytes file into the system") 67 | .as_ref(), 68 | &mut file, 69 | ) 70 | .expect("Could not copy file"); 71 | } 72 | 73 | #[tokio::main] 74 | async fn main() -> Result<(), Box> { 75 | // Download the kernel and rootfs in a temporary directory 76 | std::fs::create_dir_all("./examples/simple_vm").unwrap(); 77 | fetch_url( 78 | rootfs_url(), 79 | PathBuf::from("./examples/simple_vm/rootfs.ext4"), 80 | ) 81 | .await; 82 | fetch_url( 83 | kernel_url(), 84 | PathBuf::from("./examples/simple_vm/kernel.bin"), 85 | ) 86 | .await; 87 | 88 | // Create a TAP interface between host and guest VM 89 | // Host iface name: tap0 90 | // Guest iface name: eth0 91 | let iface = Interface::new("tap0", "eth0", Some("AA:FC:00:00:00:01")); 92 | 93 | let kernel_args = "console=ttyS0 reboot=k panic=1 pci=off random.trust_cpu=on"; 94 | // Build a config for a microVM with 1 vCPU, 1024 MiB of RAM and a root drive 95 | let config = Config::builder(None, Path::new("./examples/simple_vm/kernel.bin")) 96 | .jailer_cfg() 97 | // Base directory where the jailer will hold its config and files 98 | .chroot_base_dir(Path::new("./tmp/simple_vm")) 99 | .exec_file(Path::new("/usr/bin/firecracker")) 100 | .build() 101 | .kernel_args(kernel_args) 102 | .machine_cfg() 103 | .vcpu_count(1) 104 | .mem_size_mib(1024) 105 | .build() 106 | .add_network_interface(iface) 107 | // Add drive to the VM configuration by specifying the path to the rootfs 108 | .add_drive("root", Path::new("./examples/simple_vm/rootfs.ext4")) 109 | .is_root_device(true) 110 | .build() 111 | // Determine where the socket will be handled 112 | .socket_path(Path::new("./tmp/firec-simple_vm.socket")) 113 | .build(); 114 | let mut machine = Machine::create(config).await?; 115 | 116 | println!("Booting the VM"); 117 | machine.start().await?; 118 | println!("Waiting a few seconds, the VM is started at this point"); 119 | sleep(Duration::from_secs(5)).await; 120 | println!("Shutting down the VM"); 121 | machine.force_shutdown().await?; 122 | 123 | Ok(()) 124 | } 125 | -------------------------------------------------------------------------------- /src/config/drive.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, path::Path}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use super::Builder; 6 | 7 | /// Configuration options for IO engine. 8 | /// 9 | /// https://github.com/firecracker-microvm/firecracker/blob/main/docs/api_requests/block-io-engine.md 10 | #[derive(Debug, Serialize, Deserialize, Clone)] 11 | pub enum IOEngineType { 12 | /// Use an Async engine, based on io_uring. Available for host kernels > 5.10.51 13 | Async, 14 | /// Use a Sync engine, based on blocking system calls. Used by default 15 | Sync, 16 | } 17 | 18 | /// Configuration options for Firecracker flavor of token bucket. 19 | /// 20 | /// More info here: 21 | /// https://github.com/firecracker-microvm/firecracker/blob/main/src/rate_limiter/src/lib.rs 22 | #[derive(Debug, Deserialize, Serialize, Clone)] 23 | pub struct TokenBucket { 24 | /// Size of bucker 25 | pub size: u64, 26 | /// Amount of immediately available, non-refillable tokens provided on initialization 27 | pub one_time_burst: Option, 28 | /// Bucket refill cycle period 29 | #[serde(rename = "refill_time")] 30 | pub refill_time_ms: u64, 31 | } 32 | 33 | /// Configuration for IO related rate limiters. 34 | /// 35 | /// Is set up for each drive separatelly. 36 | /// 37 | /// Specifiyng 0 as `size` or `refill_time_ms` of a rate limiter is the same as passing None. 38 | #[derive(Debug, Deserialize, Serialize, Clone)] 39 | pub struct RateLimiter { 40 | /// Limit in bytes 41 | pub bandwidth: Option, 42 | /// Limit in number of iops 43 | pub ops: Option, 44 | } 45 | 46 | /// Drive configuration. 47 | #[derive(Debug, Serialize, Deserialize, Clone)] 48 | pub struct Drive<'d> { 49 | drive_id: Cow<'d, str>, 50 | is_read_only: bool, 51 | is_root_device: bool, 52 | #[serde(skip_serializing_if = "Option::is_none")] 53 | part_uuid: Option>, 54 | #[serde(rename = "path_on_host")] 55 | pub(crate) src_path: Cow<'d, Path>, 56 | #[serde(skip_serializing_if = "Option::is_none")] 57 | io_engine: Option, 58 | #[serde(skip_serializing_if = "Option::is_none")] 59 | rate_limiter: Option, 60 | } 61 | 62 | impl<'d> Drive<'d> { 63 | /// The drive ID. 64 | pub fn drive_id(&self) -> &str { 65 | &self.drive_id 66 | } 67 | 68 | /// If the drive is read-only. 69 | pub fn is_read_only(&self) -> bool { 70 | self.is_read_only 71 | } 72 | 73 | /// If the drive is the root device. 74 | pub fn is_root_device(&self) -> bool { 75 | self.is_root_device 76 | } 77 | 78 | /// The unique id of the boot partition of this device. 79 | pub fn part_uuid(&self) -> Option<&str> { 80 | self.part_uuid.as_deref() 81 | } 82 | 83 | /// The source path for the guest drive. 84 | /// 85 | /// This is the path given by the application. The drive is transfered to the chroot directory 86 | /// by [`crate::Machine::create`]. 87 | pub fn src_path(&self) -> &Path { 88 | &self.src_path 89 | } 90 | } 91 | 92 | /// Builder for `Drive`. 93 | #[derive(Debug)] 94 | pub struct DriveBuilder<'d> { 95 | config_builder: Builder<'d>, 96 | drive: Drive<'d>, 97 | } 98 | 99 | impl<'d> DriveBuilder<'d> { 100 | pub(crate) fn new(config_builder: Builder<'d>, drive_id: I, src_path: P) -> Self 101 | where 102 | I: Into>, 103 | P: Into>, 104 | { 105 | Self { 106 | config_builder, 107 | drive: Drive { 108 | drive_id: drive_id.into(), 109 | is_read_only: false, 110 | is_root_device: false, 111 | part_uuid: None, 112 | src_path: src_path.into(), 113 | io_engine: None, 114 | rate_limiter: None, 115 | }, 116 | } 117 | } 118 | 119 | /// If to-be-created `Drive` will be read-only. 120 | pub fn is_read_only(mut self, is_read_only: bool) -> Self { 121 | self.drive.is_read_only = is_read_only; 122 | self 123 | } 124 | 125 | /// If to-be-created `Drive` will be the root device. 126 | pub fn is_root_device(mut self, is_root_device: bool) -> Self { 127 | self.drive.is_root_device = is_root_device; 128 | self 129 | } 130 | 131 | /// Set the unique id of the boot partition of this device. 132 | /// 133 | /// It is optional and it will be taken into account only if its root device. 134 | pub fn part_uuid(mut self, part_uuid: U) -> Self 135 | where 136 | U: Into>, 137 | { 138 | self.drive.part_uuid = Some(part_uuid.into()); 139 | self 140 | } 141 | 142 | /// Set IO engine type for to-be-created `Drive`. 143 | pub fn io_engine(mut self, io_engine: Option) -> Self { 144 | self.drive.io_engine = io_engine; 145 | self 146 | } 147 | 148 | /// Set IO rate limits for to-be-created `Drive`. 149 | pub fn rate_limiter(mut self, rate_limiter: Option) -> Self { 150 | self.drive.rate_limiter = rate_limiter; 151 | self 152 | } 153 | 154 | /// Build the `Drive`. 155 | /// 156 | /// Returns the main configuration builder with the new drive added to it. 157 | pub fn build(mut self) -> Builder<'d> { 158 | self.config_builder.0.drives.push(self.drive); 159 | 160 | self.config_builder 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/config/jailer.rs: -------------------------------------------------------------------------------- 1 | //! API to configure and interact with jailer. 2 | 3 | use derivative::Derivative; 4 | use std::{borrow::Cow, path::Path}; 5 | 6 | use super::Builder; 7 | 8 | /// Jailer specific configuration needed to execute the jailer. 9 | #[derive(Debug)] 10 | pub struct Jailer<'j> { 11 | gid: u32, 12 | uid: u32, 13 | numa_node: Option, 14 | exec_file: Cow<'j, Path>, 15 | jailer_binary: Cow<'j, Path>, 16 | chroot_base_dir: Cow<'j, Path>, 17 | workspace_dir: Cow<'j, Path>, 18 | pub(crate) mode: JailerMode<'j>, 19 | // TODO: We need an equivalent of ChrootStrategy. 20 | } 21 | 22 | impl<'j> Jailer<'j> { 23 | /// GID the jailer switches to as it execs the target binary. 24 | pub fn gid(&self) -> u32 { 25 | self.gid 26 | } 27 | 28 | /// UID the jailer switches to as it execs the target binary. 29 | pub fn uid(&self) -> u32 { 30 | self.uid 31 | } 32 | 33 | /// The NUMA node the process gets assigned to. 34 | pub fn numa_node(&self) -> Option { 35 | self.numa_node 36 | } 37 | 38 | /// The path to the Firecracker binary that will be exec-ed by the jailer. 39 | pub fn exec_file(&self) -> &Path { 40 | &self.exec_file 41 | } 42 | 43 | /// Specifies the jailer binary to be used for setting up the Firecracker VM jail. 44 | pub fn jailer_binary(&self) -> &Path { 45 | &self.jailer_binary 46 | } 47 | 48 | /// The base folder where chroot jails are built. 49 | pub fn chroot_base_dir(&self) -> &Path { 50 | &self.chroot_base_dir 51 | } 52 | 53 | /// The mode of the jailer process. 54 | pub fn mode(&self) -> &JailerMode { 55 | &self.mode 56 | } 57 | 58 | /// The path to the jailer workspace. 59 | pub fn workspace_dir(&self) -> &Path { 60 | &self.workspace_dir 61 | } 62 | } 63 | 64 | /// The mode of the jailer process. 65 | #[derive(Derivative)] 66 | #[derivative(Debug, Default)] 67 | pub enum JailerMode<'j> { 68 | /// The jailer child process will run attached to the parent process. 69 | #[derivative(Default)] 70 | Attached(Stdio), 71 | /// Calls setsid() and redirect stdin, stdout, and stderr to /dev/null. 72 | Daemon, 73 | /// Launch the jailer in a tmux session. 74 | /// 75 | /// If the session name is not provided, `` is used as the session name. tmux will be 76 | /// launched in detached mode. 77 | Tmux(Option>), 78 | } 79 | 80 | /// The standard IO handlers. 81 | #[derive(Derivative)] 82 | #[derivative(Debug, Default)] 83 | pub struct Stdio { 84 | /// Stdout specifies the IO writer for STDOUT to use when spawning the jailer. 85 | pub stdout: Option, 86 | /// Stderr specifies the IO writer for STDERR to use when spawning the jailer. 87 | pub stderr: Option, 88 | /// Stdin specifies the IO reader for STDIN to use when spawning the jailer. 89 | pub stdin: Option, 90 | } 91 | 92 | /// Builder for `Jailer` instances. 93 | #[derive(Debug)] 94 | pub struct JailerBuilder<'j> { 95 | jailer: Jailer<'j>, 96 | config_builder: Builder<'j>, 97 | } 98 | 99 | impl<'j> JailerBuilder<'j> { 100 | pub(crate) fn new(config_builder: Builder<'j>) -> Self { 101 | Self { 102 | config_builder, 103 | jailer: Jailer { 104 | gid: users::get_effective_gid(), 105 | uid: users::get_effective_uid(), 106 | numa_node: None, 107 | exec_file: Path::new("/usr/bin/firecracker").into(), 108 | jailer_binary: Path::new("jailer").into(), 109 | chroot_base_dir: Path::new("/srv/jailer").into(), 110 | workspace_dir: Path::new("/srv/jailer/firecracker/root").into(), 111 | mode: JailerMode::default(), 112 | }, 113 | } 114 | } 115 | 116 | /// GID the jailer switches to as it execs the target binary. 117 | pub fn gid(mut self, gid: u32) -> Self { 118 | self.jailer.gid = gid; 119 | self 120 | } 121 | 122 | /// UID the jailer switches to as it execs the target binary. 123 | pub fn uid(mut self, uid: u32) -> Self { 124 | self.jailer.uid = uid; 125 | self 126 | } 127 | 128 | /// NumaNode represents the NUMA node the process gets assigned to. 129 | pub fn numa_node(mut self, numa_node: i32) -> Self { 130 | self.jailer.numa_node = Some(numa_node); 131 | self 132 | } 133 | 134 | /// The path to the Firecracker binary that will be exec-ed by the jailer. 135 | /// 136 | /// The user can provide a path to any binary, but the interaction 137 | /// with the jailer is mostly Firecracker specific. 138 | pub fn exec_file

(mut self, exec_file: P) -> Self 139 | where 140 | P: Into>, 141 | { 142 | self.jailer.exec_file = exec_file.into(); 143 | self 144 | } 145 | 146 | /// Specifies the jailer binary to be used for setting up the Firecracker VM jail. 147 | /// 148 | /// If the value contains no path separators, it will use the PATH environment variable to get 149 | /// the absolute path of the binary. If the value contains path separators, the value will be 150 | /// used directly to exec the jailer. This follows the same conventions as Golang's 151 | /// os/exec.Command. 152 | // 153 | /// If not specified it defaults to "jailer". 154 | pub fn jailer_binary

(mut self, jailer_binary: P) -> Self 155 | where 156 | P: Into>, 157 | { 158 | self.jailer.jailer_binary = jailer_binary.into(); 159 | self 160 | } 161 | 162 | /// The base folder where chroot jails are built. 163 | /// 164 | /// The default is `/srv/jailer`. 165 | pub fn chroot_base_dir

(mut self, chroot_base_dir: P) -> Self 166 | where 167 | P: Into>, 168 | { 169 | self.jailer.chroot_base_dir = chroot_base_dir.into(); 170 | self 171 | } 172 | 173 | /// The mode of the jailer process. 174 | pub fn mode(mut self, mode: JailerMode<'j>) -> Self { 175 | self.jailer.mode = mode; 176 | self 177 | } 178 | 179 | /// Build the `Jailer` instance. 180 | /// 181 | /// Returns the main configuration builder with new jailer. 182 | pub fn build(mut self) -> Builder<'j> { 183 | let exec_file_base = self 184 | .jailer 185 | .exec_file() 186 | .file_name() 187 | // FIXME: Check `exec_file` in the `exec_file` method so we can just assume it to 188 | // have a proper filename here. 189 | .expect("invalid jailer exec file path"); 190 | let id_str = self.config_builder.0.vm_id().to_string(); 191 | self.jailer.workspace_dir = self 192 | .jailer 193 | .chroot_base_dir() 194 | .join(exec_file_base) 195 | .join(id_str) 196 | .join("root") 197 | .into(); 198 | self.config_builder.0.jailer_cfg = Some(self.jailer); 199 | 200 | self.config_builder 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/config/machine.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use derivative::Derivative; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use super::Builder; 7 | 8 | /// Machine configuration. 9 | #[derive(Derivative, Debug, Serialize, Deserialize)] 10 | pub struct Machine<'m> { 11 | smt: bool, 12 | track_dirty_pages: bool, 13 | mem_size_mib: i64, 14 | vcpu_count: usize, 15 | // TODO: Should create a type to validate it like the Go API. 16 | #[serde(skip_serializing_if = "Option::is_none")] 17 | cpu_template: Option>, 18 | } 19 | 20 | impl<'m> Machine<'m> { 21 | /// If simultaneous multithreading is enabled. 22 | pub fn smt(&self) -> bool { 23 | self.smt 24 | } 25 | 26 | /// If dirty page tracking is enabled. 27 | pub fn track_dirty_pages(&self) -> bool { 28 | self.track_dirty_pages 29 | } 30 | 31 | /// Memory size of VM. 32 | pub fn mem_size_mib(&self) -> i64 { 33 | self.mem_size_mib 34 | } 35 | 36 | /// Number of vCPUs (either 1 or an even number) 37 | pub fn vcpu_count(&self) -> usize { 38 | self.vcpu_count 39 | } 40 | 41 | /// CPU template. 42 | pub fn cpu_template(&self) -> Option<&str> { 43 | self.cpu_template.as_deref() 44 | } 45 | } 46 | 47 | impl Default for Machine<'_> { 48 | fn default() -> Self { 49 | Machine { 50 | smt: false, 51 | track_dirty_pages: false, 52 | mem_size_mib: 1024, 53 | vcpu_count: 1, 54 | cpu_template: None, 55 | } 56 | } 57 | } 58 | 59 | /// Builder for `Machine`. 60 | #[derive(Debug)] 61 | pub struct MachineBuilder<'m> { 62 | config_builder: Builder<'m>, 63 | machine: Machine<'m>, 64 | } 65 | 66 | impl<'m> MachineBuilder<'m> { 67 | /// Create a new `MachineBuilder` instance. 68 | pub(crate) fn new(config_builder: Builder<'m>) -> Self { 69 | Self { 70 | config_builder, 71 | machine: Machine::default(), 72 | } 73 | } 74 | 75 | /// Flag for enabling/disabling simultaneous multithreading. 76 | /// 77 | /// Can be enabled only on x86. 78 | pub fn smt(mut self, smt: bool) -> Self { 79 | self.machine.smt = smt; 80 | self 81 | } 82 | 83 | /// Enable dirty page tracking. If this is enabled, then incremental guest memory snapshots 84 | /// can be created. These belong to diff snapshots, which contain, besides the microVM state, 85 | /// only the memory dirtied since a previous snapshot. Full snapshots each contain a full copy 86 | /// of the guest memory. 87 | pub fn track_dirty_pages(mut self, track_dirty_pages: bool) -> Self { 88 | self.machine.track_dirty_pages = track_dirty_pages; 89 | self 90 | } 91 | 92 | /// Memory size of VM. 93 | pub fn mem_size_mib(mut self, mem_size_mib: i64) -> Self { 94 | self.machine.mem_size_mib = mem_size_mib; 95 | self 96 | } 97 | 98 | /// Number of vCPUs (either 1 or an even number). 99 | /// 100 | /// Maximum: 32 101 | /// Minimum: 1 102 | pub fn vcpu_count(mut self, vcpu_count: usize) -> Self { 103 | self.machine.vcpu_count = vcpu_count; 104 | self 105 | } 106 | 107 | /// cpu template. 108 | pub fn cpu_template(mut self, cpu_template: Cow<'m, str>) -> Self { 109 | self.machine.cpu_template = Some(cpu_template); 110 | self 111 | } 112 | 113 | /// Build the `Machine` instance. 114 | pub fn build(mut self) -> Builder<'m> { 115 | self.config_builder.0.machine_cfg = self.machine; 116 | self.config_builder 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | //! VMM configuration. 2 | 3 | use std::{ 4 | borrow::Cow, 5 | path::{Path, PathBuf}, 6 | }; 7 | 8 | use derivative::Derivative; 9 | use serde::{Deserialize, Serialize}; 10 | 11 | mod drive; 12 | mod jailer; 13 | mod machine; 14 | /// Network configuration. 15 | pub mod network; 16 | mod vsock; 17 | 18 | pub use drive::*; 19 | pub use jailer::*; 20 | pub use machine::*; 21 | pub use vsock::*; 22 | 23 | use uuid::Uuid; 24 | 25 | use crate::Error; 26 | 27 | // FIXME: Hardcoding for now. This should come from ChrootStrategy enum, when we've that. 28 | const KERNEL_IMAGE_FILENAME: &str = "kernel"; 29 | 30 | /// VMM configuration. 31 | #[derive(Debug)] 32 | pub struct Config<'c> { 33 | pub(crate) socket_path: Cow<'c, Path>, 34 | log_path: Option>, 35 | log_fifo: Option>, 36 | log_level: Option, 37 | metrics_path: Option>, 38 | metrics_fifo: Option>, 39 | pub(crate) src_kernel_image_path: Cow<'c, Path>, 40 | pub(crate) src_initrd_path: Option>, 41 | kernel_args: Option>, 42 | pub(crate) drives: Vec>, 43 | 44 | // FIXME: Can't use trait object here because it's make `Config` non-Send, which is problematic 45 | // for async/await. 46 | //// Used to redirect the contents of the fifo log to the writer. 47 | //#[derivative(Debug = "ignore")] 48 | //pub fifo_log_writer: Option>, 49 | machine_cfg: Machine<'c>, 50 | pub(crate) jailer_cfg: Option>, 51 | vm_id: Uuid, 52 | net_ns: Option>, 53 | network_interfaces: Vec>, 54 | vsock_cfg: Option>, 55 | /* TODO: 56 | 57 | 58 | Other fields. 59 | 60 | */ 61 | } 62 | 63 | impl<'c> Config<'c> { 64 | /// Create a new `Builder` instance. 65 | /// 66 | /// # Arguments 67 | /// 68 | /// `vm_id` - The ID of the VM. It's used as the Firecracker's instance ID. Pass `None` to 69 | /// generate a random ID. 70 | /// `src_kernel_image_path`: The path to the kernel image, that must be an uncompressed ELF image. 71 | pub fn builder

(vm_id: Option, src_kernel_image_path: P) -> Builder<'c> 72 | where 73 | P: Into>, 74 | { 75 | Builder(Self { 76 | socket_path: Path::new("/run/firecracker.socket").into(), 77 | log_path: None, 78 | log_fifo: None, 79 | log_level: None, 80 | metrics_path: None, 81 | metrics_fifo: None, 82 | src_kernel_image_path: src_kernel_image_path.into(), 83 | src_initrd_path: None, 84 | kernel_args: None, 85 | drives: Vec::new(), 86 | machine_cfg: Machine::default(), 87 | jailer_cfg: None, 88 | vm_id: vm_id.unwrap_or_else(Uuid::new_v4), 89 | net_ns: None, 90 | network_interfaces: Vec::new(), 91 | vsock_cfg: None, 92 | }) 93 | } 94 | 95 | /// Create boot source from `self`. 96 | pub(crate) fn boot_source(&self) -> Result { 97 | let relative_kernel_image_path = Path::new("/").join(KERNEL_IMAGE_FILENAME); 98 | 99 | let relative_initrd_path: Result, Error> = 100 | match self.src_initrd_path.as_ref() { 101 | Some(initrd_path) => { 102 | let initrd_filename = 103 | initrd_path.file_name().ok_or(Error::InvalidInitrdPath)?; 104 | Ok(Some(Path::new("/").join(initrd_filename))) 105 | } 106 | None => Ok(None), 107 | }; 108 | 109 | Ok(BootSource { 110 | kernel_image_path: relative_kernel_image_path, 111 | initrd_path: relative_initrd_path?, 112 | boot_args: self.kernel_args.as_ref().map(AsRef::as_ref).map(Into::into), 113 | }) 114 | } 115 | 116 | /// The socket path. 117 | pub fn socket_path(&self) -> &Path { 118 | self.socket_path.as_ref() 119 | } 120 | 121 | /// The socket path in chroot location. 122 | pub fn host_socket_path(&self) -> PathBuf { 123 | let socket_path = self.socket_path.as_ref(); 124 | let relative_path = socket_path.strip_prefix("/").unwrap_or(socket_path); 125 | self.jailer().workspace_dir().join(relative_path) 126 | } 127 | 128 | /// The log path. 129 | pub fn log_path(&self) -> Option<&Path> { 130 | self.log_path.as_ref().map(AsRef::as_ref) 131 | } 132 | 133 | /// The log fifo path. 134 | pub fn log_fifo(&self) -> Option<&Path> { 135 | self.log_fifo.as_ref().map(AsRef::as_ref) 136 | } 137 | 138 | /// The metrics path. 139 | pub fn metrics_path(&self) -> Option<&Path> { 140 | self.metrics_path.as_ref().map(AsRef::as_ref) 141 | } 142 | 143 | /// The metrics fifo path. 144 | pub fn metrics_fifo(&self) -> Option<&Path> { 145 | self.metrics_fifo.as_ref().map(AsRef::as_ref) 146 | } 147 | 148 | /// The source kernel image path. 149 | /// 150 | /// This is the path given by the application. It's transfered to the chroot directory by 151 | /// [`crate::Machine::create`]. The path inside the chroot can be queried using 152 | /// [`Config::kernel_image_path`]. 153 | pub fn src_kernel_image_path(&self) -> &Path { 154 | self.src_kernel_image_path.as_ref() 155 | } 156 | 157 | /// The kernel image path in chroot location. 158 | pub fn kernel_image_path(&self) -> PathBuf { 159 | self.jailer().workspace_dir().join(KERNEL_IMAGE_FILENAME) 160 | } 161 | 162 | /// The source initrd path. 163 | /// 164 | /// This is the path given by the application. It's transfered to the chroot directory by 165 | /// [`crate::Machine::create`]. The path inside the chroot can be queried using 166 | /// [`Config::initrd_path`]. 167 | pub fn src_initrd_path(&self) -> Option<&Path> { 168 | self.src_initrd_path.as_ref().map(AsRef::as_ref) 169 | } 170 | 171 | /// The initrd path in chroot location. 172 | pub fn initrd_path(&self) -> Result, Error> { 173 | match self.src_initrd_path.as_ref() { 174 | Some(initrd_path) => { 175 | let initrd_filename = initrd_path 176 | .file_name() 177 | .ok_or(Error::InvalidInitrdPath)? 178 | .to_owned(); 179 | Ok(Some(self.jailer().workspace_dir().join(initrd_filename))) 180 | } 181 | None => Ok(None), 182 | } 183 | } 184 | 185 | /// The kernel arguments. 186 | pub fn kernel_args(&self) -> Option<&str> { 187 | self.kernel_args.as_ref().map(AsRef::as_ref) 188 | } 189 | 190 | /// The drives. 191 | pub fn drives(&self) -> &[Drive<'c>] { 192 | &self.drives 193 | } 194 | 195 | /// The machine configuration. 196 | pub fn machine_cfg(&self) -> &Machine<'c> { 197 | &self.machine_cfg 198 | } 199 | 200 | /// The jailer configuration. 201 | pub fn jailer_cfg(&self) -> Option<&Jailer<'c>> { 202 | self.jailer_cfg.as_ref() 203 | } 204 | 205 | /// The VM ID. 206 | pub fn vm_id(&self) -> &Uuid { 207 | &self.vm_id 208 | } 209 | 210 | /// The network namespace path. 211 | pub fn net_ns(&self) -> Option<&str> { 212 | self.net_ns.as_ref().map(AsRef::as_ref) 213 | } 214 | 215 | /// The network interfaces. 216 | pub fn network_interfaces(&self) -> &[network::Interface<'c>] { 217 | &self.network_interfaces 218 | } 219 | 220 | /// The vsock configuration. 221 | pub fn vsock_cfg(&self) -> Option<&VSock<'c>> { 222 | self.vsock_cfg.as_ref() 223 | } 224 | 225 | pub(crate) fn jailer(&self) -> &Jailer { 226 | // FIXME: Assuming jailer for now. 227 | self.jailer_cfg.as_ref().expect("no jailer config") 228 | } 229 | } 230 | 231 | /// The boot source for the microVM. 232 | #[derive(Debug, Serialize, Deserialize)] 233 | pub struct BootSource<'b> { 234 | /// The kernel image path. 235 | pub kernel_image_path: PathBuf, 236 | /// The (optional) kernel command line. 237 | pub boot_args: Option<&'b str>, 238 | /// The (optional) initrd image path. 239 | #[serde(skip_serializing_if = "Option::is_none")] 240 | pub initrd_path: Option, 241 | } 242 | 243 | /// defines the verbosity of Firecracker logging. 244 | #[derive(Derivative)] 245 | #[derivative(Debug, Default)] 246 | pub enum LogLevel { 247 | /// Error level logging. 248 | Error, 249 | /// Warning level logging. 250 | Warning, 251 | #[derivative(Default)] 252 | /// Info level logging. 253 | Info, 254 | /// Debug level logging. 255 | Debug, 256 | } 257 | 258 | /// Configuration builder. 259 | #[derive(Debug)] 260 | pub struct Builder<'c>(Config<'c>); 261 | 262 | impl<'c> Builder<'c> { 263 | /// Set the file path where the Firecracker control socket should be created. 264 | pub fn socket_path

(mut self, socket_path: P) -> Self 265 | where 266 | P: Into>, 267 | { 268 | self.0.socket_path = socket_path.into(); 269 | self 270 | } 271 | 272 | /// Set the Firecracker log path. 273 | pub fn log_path

(mut self, log_path: P) -> Self 274 | where 275 | P: Into>, 276 | { 277 | self.0.log_path = Some(log_path.into()); 278 | self 279 | } 280 | 281 | /// Set the Firecracker log named-pipe path. 282 | pub fn log_fifo

(mut self, log_fifo: P) -> Self 283 | where 284 | P: Into>, 285 | { 286 | self.0.log_fifo = Some(log_fifo.into()); 287 | self 288 | } 289 | 290 | /// Set the verbosity of Firecracker logging. 291 | pub fn log_level(mut self, log_level: LogLevel) -> Self { 292 | self.0.log_level = Some(log_level); 293 | self 294 | } 295 | 296 | /// Set the Firecracker metrics path. 297 | pub fn metrics_path

(mut self, metrics_path: P) -> Self 298 | where 299 | P: Into>, 300 | { 301 | self.0.metrics_path = Some(metrics_path.into()); 302 | self 303 | } 304 | 305 | /// Set the Firecracker metrics named-pipe path. 306 | pub fn metrics_fifo

(mut self, metrics_fifo: P) -> Self 307 | where 308 | P: Into>, 309 | { 310 | self.0.metrics_fifo = Some(metrics_fifo.into()); 311 | self 312 | } 313 | 314 | /// Set the initrd image path. 315 | pub fn initrd_path

(mut self, initrd_path: P) -> Self 316 | where 317 | P: Into>, 318 | { 319 | self.0.src_initrd_path = Some(initrd_path.into()); 320 | self 321 | } 322 | 323 | /// Set the command-line arguments that should be passed to the kernel. 324 | pub fn kernel_args

(mut self, kernel_args: P) -> Self 325 | where 326 | P: Into>, 327 | { 328 | self.0.kernel_args = Some(kernel_args.into()); 329 | self 330 | } 331 | 332 | /// Add a drive. 333 | pub fn add_drive(self, drive_id: I, src_path: P) -> DriveBuilder<'c> 334 | where 335 | I: Into>, 336 | P: Into>, 337 | { 338 | DriveBuilder::new(self, drive_id, src_path) 339 | } 340 | 341 | /// Set the Firecracker microVM process configuration builder. 342 | pub fn machine_cfg(self) -> MachineBuilder<'c> { 343 | MachineBuilder::new(self) 344 | } 345 | 346 | /// Create the jailer process configuration builder. 347 | pub fn jailer_cfg(self) -> JailerBuilder<'c> { 348 | JailerBuilder::new(self) 349 | } 350 | 351 | /// Set the path to a network namespace handle. 352 | /// 353 | /// If specified, the application will use this to join the associated network namespace. 354 | pub fn net_ns(mut self, net_ns: N) -> Self 355 | where 356 | N: Into>, 357 | { 358 | self.0.net_ns = Some(net_ns.into()); 359 | self 360 | } 361 | 362 | /// Add a network interface. 363 | /// 364 | /// Add a tap device that should be made available to the microVM. 365 | pub fn add_network_interface(mut self, network_interface: network::Interface<'c>) -> Self { 366 | self.0.network_interfaces.push(network_interface); 367 | self 368 | } 369 | 370 | /// Set the vsock configuration. 371 | /// 372 | /// For guest-initialiated connections, a `_PORT` suffix is expected in the actual socket 373 | /// filename of `uds_path`. 374 | pub fn vsock_cfg

(mut self, guest_cid: u32, uds_path: P) -> Self 375 | where 376 | P: Into>, 377 | { 378 | self.0.vsock_cfg = Some(VSock { 379 | guest_cid, 380 | uds_path: uds_path.into(), 381 | }); 382 | self 383 | } 384 | 385 | /// Build the configuration. 386 | pub fn build(self) -> Config<'c> { 387 | self.0 388 | } 389 | } 390 | 391 | #[cfg(test)] 392 | mod tests { 393 | use super::*; 394 | use Uuid; 395 | 396 | #[test] 397 | fn config_host_values() { 398 | let id = Uuid::new_v4(); 399 | 400 | let config = Config::builder(Some(id), Path::new("/tmp/kernel.path")) 401 | .jailer_cfg() 402 | .chroot_base_dir(Path::new("/chroot")) 403 | .exec_file(Path::new("/usr/bin/firecracker")) 404 | .mode(JailerMode::Daemon) 405 | .build() 406 | .initrd_path(Path::new("/tmp/initrd.img")) 407 | .add_drive("root", Path::new("/tmp/debian.ext4")) 408 | .is_root_device(true) 409 | .build() 410 | .socket_path(Path::new("/firecracker.socket")) 411 | .vsock_cfg(3, Path::new("/vsock.sock")) 412 | .build(); 413 | 414 | assert_eq!( 415 | config.src_initrd_path.as_ref().unwrap().as_os_str(), 416 | "/tmp/initrd.img" 417 | ); 418 | assert_eq!( 419 | config 420 | .initrd_path() 421 | .unwrap() 422 | .unwrap() 423 | .as_os_str() 424 | .to_string_lossy(), 425 | format!("/chroot/firecracker/{}/root/initrd.img", id) 426 | ); 427 | 428 | assert_eq!( 429 | config.src_kernel_image_path.as_ref().as_os_str(), 430 | "/tmp/kernel.path" 431 | ); 432 | assert_eq!( 433 | config.kernel_image_path().as_os_str().to_string_lossy(), 434 | format!("/chroot/firecracker/{}/root/kernel", id) 435 | ); 436 | assert_eq!( 437 | config.socket_path.as_ref().as_os_str(), 438 | "/firecracker.socket" 439 | ); 440 | assert_eq!( 441 | config.host_socket_path().as_os_str().to_string_lossy(), 442 | format!("/chroot/firecracker/{}/root/firecracker.socket", id) 443 | ); 444 | assert_eq!( 445 | config 446 | .vsock_cfg() 447 | .unwrap() 448 | .uds_path() 449 | .as_os_str() 450 | .to_string_lossy(), 451 | format!("/vsock.sock") 452 | ); 453 | 454 | let boot_source = config.boot_source().unwrap(); 455 | assert_eq!(boot_source.boot_args, None); 456 | assert_eq!(boot_source.kernel_image_path.as_os_str(), "/kernel"); 457 | assert_eq!(boot_source.initrd_path.unwrap().as_os_str(), "/initrd.img"); 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /src/config/network.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Network configuration. 6 | #[derive(Debug, Serialize, Deserialize, Clone)] 7 | pub struct Interface<'i> { 8 | #[serde(rename = "host_dev_name")] 9 | host_if_name: Cow<'i, str>, 10 | #[serde(rename = "iface_id")] 11 | vm_if_name: Cow<'i, str>, 12 | #[serde(rename = "guest_mac", skip_serializing_if = "Option::is_none")] 13 | vm_mac_address: Option>, 14 | } 15 | 16 | impl<'i> Interface<'i> { 17 | /// Create a new `Interface` instance. 18 | pub fn new(host_if_name: H, vm_if_name: V, vm_mac_address: Option) -> Self 19 | where 20 | H: Into>, 21 | V: Into>, 22 | M: Into>, 23 | { 24 | Interface { 25 | host_if_name: host_if_name.into(), 26 | vm_if_name: vm_if_name.into(), 27 | vm_mac_address: vm_mac_address.map(Into::into), 28 | } 29 | } 30 | 31 | /// The name of the host interface. 32 | pub fn host_if_name(&self) -> &str { 33 | &self.host_if_name 34 | } 35 | 36 | /// The interface name in the VM. 37 | pub fn vm_if_name(&self) -> &str { 38 | &self.vm_if_name 39 | } 40 | 41 | /// MAC address of the VM. 42 | pub fn vm_mac_address(&self) -> Option<&str> { 43 | self.vm_mac_address.as_deref() 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | #[test] 50 | #[ignore] 51 | fn string_generics() { 52 | // Compile-only test to ensure the generics work for both string types. 53 | let _ = super::Interface::new("host_if_name", "vm_if_name", Some("AA:FC:00:00:00:01")); 54 | // Different types are fine, as long as they've the same lifetime. 55 | let _ = super::Interface::new("host_if_name".to_string(), "vm_if_name", None::); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/config/vsock.rs: -------------------------------------------------------------------------------- 1 | use derivative::Derivative; 2 | use serde::{Deserialize, Serialize}; 3 | use std::{borrow::Cow, path::Path}; 4 | 5 | /// VSock configuration. 6 | /// 7 | /// For information about VSOCK, please refer to its [manpage]. For details on how to use VSOCK with 8 | /// Firecracker, please refer to the relevant [Firecracker documentation]. 9 | /// 10 | /// [manpage]: https://man7.org/linux/man-pages/man7/vsock.7.html 11 | /// [Firecracker documentation]: https://github.com/firecracker-microvm/firecracker/blob/main/docs/vsock.md 12 | #[derive(Derivative, Debug, Serialize, Deserialize)] 13 | pub struct VSock<'v> { 14 | pub(crate) guest_cid: u32, 15 | pub(crate) uds_path: Cow<'v, Path>, 16 | } 17 | 18 | impl VSock<'_> { 19 | /// The Context ID. 20 | pub fn guest_cid(&self) -> u32 { 21 | self.guest_cid 22 | } 23 | 24 | /// The path to the Unix socket file. 25 | /// 26 | /// For guest-initialiated connections, a `_PORT` suffix is expected in the actual socket 27 | /// filename. 28 | pub fn uds_path(&self) -> &Path { 29 | &self.uds_path 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use hyper::StatusCode; 2 | use thiserror::Error; 3 | 4 | /// Error type for this crate. 5 | #[derive(Debug, Error)] 6 | pub enum Error { 7 | /// Failed to generate UUID. 8 | #[error("Failed to generate UUID: {0}")] 9 | Uuid(#[from] uuid::Error), 10 | 11 | /// IO error. 12 | #[error("IO error: {0}")] 13 | Io(#[from] std::io::Error), 14 | 15 | /// Hyper error. 16 | #[error("Hyper error: {0}")] 17 | Hyper(#[from] hyper::Error), 18 | 19 | /// HTTP error. 20 | #[error("HTTP error: {0}")] 21 | Http(#[from] hyper::http::Error), 22 | 23 | /// JSON error. 24 | #[error("JSON error: {0}")] 25 | Json(#[from] serde_json::Error), 26 | 27 | /// Integral type conversion error. 28 | #[error("Integral type conversion error: {0}")] 29 | TryFromIntError(#[from] std::num::TryFromIntError), 30 | 31 | /// Task join error. 32 | #[error("Task join error: {0}")] 33 | JoinError(#[from] tokio::task::JoinError), 34 | 35 | /// Invalid Jailer executable path specified. 36 | #[error("Invalid Jailer executable path specified")] 37 | InvalidJailerExecPath, 38 | 39 | /// Invalid initrd path specified. 40 | #[error("Invalid initrd path specified")] 41 | InvalidInitrdPath, 42 | 43 | /// Invalid socket path specified. 44 | #[error("Invalid socket path specified")] 45 | InvalidSocketPath, 46 | 47 | /// Invalid drive path specified. 48 | #[error("Invalid drive path specified")] 49 | InvalidDrivePath, 50 | 51 | /// Invalid chroot base path specified. 52 | #[error("Invalid chroot base path specified")] 53 | InvalidChrootBasePath, 54 | 55 | /// Firecracker REST API error 56 | #[error("Firecracker API call failed with status={status}, body={body:?}")] 57 | FirecrackerAPIError { 58 | /// Error HTTP status code 59 | status: StatusCode, 60 | /// Optional error message body 61 | body: Option, 62 | }, 63 | 64 | /// Jailer start timed out 65 | #[error("Jailer start timed out")] 66 | JailerStartTimedOut, 67 | 68 | /// Failed to start 69 | #[error("Failed to start")] 70 | FailedToStart, 71 | 72 | /// Process already running 73 | #[error("Process is already running")] 74 | ProcessAlreadyRunning, 75 | 76 | /// Process not started 77 | #[error("Process not started")] 78 | ProcessNotStarted, 79 | 80 | /// Process not running 81 | #[error("Process not running for pid: {0}")] 82 | ProcessNotRunning(u32), 83 | 84 | /// Process not killed 85 | #[error("Process not killed for pid: {0}")] 86 | ProcessNotKilled(u32), 87 | 88 | /// Process exited immediatelly after start. 89 | #[error("Process exited immediatelly with status: {exit_status}")] 90 | ProcessExitedImmediatelly { 91 | /// Result of a process after it has terminated 92 | exit_status: std::process::ExitStatus, 93 | }, 94 | } 95 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![forbid(unsafe_code)] 3 | #![deny(missing_debug_implementations, nonstandard_style)] 4 | #![warn(missing_docs, rustdoc::missing_doc_code_examples, unreachable_pub)] 5 | 6 | pub mod config; 7 | mod error; 8 | mod machine; 9 | 10 | pub use error::*; 11 | pub use machine::*; 12 | 13 | #[cfg(doctest)] 14 | mod doctests { 15 | doc_comment::doctest!("../README.md"); 16 | } 17 | -------------------------------------------------------------------------------- /src/machine.rs: -------------------------------------------------------------------------------- 1 | //! A VMM machine. 2 | 3 | use std::{io::ErrorKind, path::Path, process::Stdio, time::Duration}; 4 | 5 | use crate::{ 6 | config::{Config, JailerMode}, 7 | Error, 8 | }; 9 | use futures_util::TryFutureExt; 10 | use serde::Serialize; 11 | use sysinfo::{Pid, PidExt, ProcessExt, ProcessRefreshKind, ProcessStatus, System, SystemExt}; 12 | use tokio::{ 13 | fs::{self, copy, DirBuilder}, 14 | process::Command, 15 | task, 16 | time::sleep, 17 | }; 18 | use tracing::{info, instrument, trace, warn}; 19 | 20 | use hyper::{Body, Client, Method, Request}; 21 | use hyperlocal::{UnixClientExt, UnixConnector, Uri}; 22 | 23 | const JAILER_START_TIMEOUT: Duration = Duration::from_secs(10); 24 | 25 | /// A VMM machine. 26 | #[derive(Debug)] 27 | pub struct Machine<'m> { 28 | config: Config<'m>, 29 | /// Pid of a started jailer/firecracker process, or None if not started yet 30 | pid: Option, 31 | client: Client, 32 | } 33 | 34 | /// VM state 35 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 36 | pub enum MachineState { 37 | /// Machine is not started or already shut down 38 | SHUTOFF, 39 | /// Machine is running 40 | RUNNING, 41 | } 42 | 43 | impl<'m> Machine<'m> { 44 | /// Create a new machine. 45 | /// 46 | /// The machine is not started yet. 47 | #[instrument(skip_all)] 48 | pub async fn create(config: Config<'m>) -> Result, Error> { 49 | let vm_id = *config.vm_id(); 50 | info!("Creating new machine with VM ID `{vm_id}`"); 51 | trace!("{vm_id}: Configuration: {:?}", config); 52 | 53 | let jailer_workspace_dir = config.jailer().workspace_dir(); 54 | trace!( 55 | "{vm_id}: Ensuring Jailer workspace directory exist at `{}`", 56 | jailer_workspace_dir.display() 57 | ); 58 | DirBuilder::new() 59 | .recursive(true) 60 | .create(jailer_workspace_dir) 61 | .await?; 62 | 63 | let dest = config.kernel_image_path(); 64 | if dest.exists() { 65 | trace!( 66 | "{vm_id}: Skipping existing kernel image at `{}`", 67 | dest.display() 68 | ); 69 | } else { 70 | trace!( 71 | "{vm_id}: Copying kernel image from `{}` to `{}`", 72 | config.src_kernel_image_path.display(), 73 | dest.display() 74 | ); 75 | copy(config.src_kernel_image_path(), dest).await?; 76 | } 77 | 78 | if let (Some(src_initrd_path), Some(initrd_path)) = 79 | (config.src_initrd_path(), config.initrd_path()?) 80 | { 81 | if initrd_path.exists() { 82 | trace!( 83 | "{vm_id}: Skipping existing initrd at `{}`", 84 | initrd_path.display() 85 | ); 86 | } else { 87 | trace!( 88 | "{vm_id}: Copying initrd from `{}` to `{}`", 89 | src_initrd_path.display(), 90 | initrd_path.display() 91 | ); 92 | copy(src_initrd_path, initrd_path).await?; 93 | } 94 | } 95 | 96 | for drive in &config.drives { 97 | let drive_filename = drive 98 | .src_path() 99 | .file_name() 100 | .ok_or(Error::InvalidDrivePath)?; 101 | let dest = jailer_workspace_dir.join(drive_filename); 102 | if dest.exists() { 103 | trace!("{vm_id}: Skipping existing drive at `{}`", dest.display()); 104 | } else { 105 | trace!( 106 | "{vm_id}: Copying drive `{}` from `{}` to `{}`", 107 | drive.drive_id(), 108 | drive.src_path().display(), 109 | dest.display() 110 | ); 111 | copy(&drive.src_path(), dest).await?; 112 | } 113 | } 114 | 115 | if let Some(socket_dir) = config.host_socket_path().parent() { 116 | trace!( 117 | "{vm_id}: Ensuring socket directory exist at `{}`", 118 | socket_dir.display() 119 | ); 120 | DirBuilder::new().recursive(true).create(socket_dir).await?; 121 | } 122 | 123 | // TODO: Handle fifos. See https://github.com/firecracker-microvm/firecracker-go-sdk/blob/f0a967ef386caec37f6533dce5797038edf8c226/jailer.go#L435 124 | 125 | // `request` doesn't provide API to connect to unix sockets so we we use the low-level 126 | // approach using hyper: https://github.com/seanmonstar/reqwest/issues/39 127 | let client = Client::unix(); 128 | 129 | let machine = Self { 130 | config, 131 | pid: None, 132 | client, 133 | }; 134 | 135 | Ok(machine) 136 | } 137 | 138 | /// Connect to already created machine. 139 | /// 140 | /// The machine should be created first via call to `create` 141 | #[instrument(skip_all)] 142 | pub async fn connect(config: Config<'m>, pid: Option) -> Machine<'m> { 143 | let vm_id = *config.vm_id(); 144 | info!("Connecting to machine with VM ID `{vm_id}`"); 145 | trace!("{vm_id}: Configuration: {:?}, pid: {:?}", config, pid); 146 | 147 | let client = Client::unix(); 148 | 149 | Self { 150 | config, 151 | pid, 152 | client, 153 | } 154 | } 155 | 156 | /// Start the machine. 157 | #[instrument(skip_all)] 158 | pub async fn start(&mut self) -> Result<(), Error> { 159 | if self.state() == MachineState::RUNNING { 160 | return Err(Error::ProcessAlreadyRunning); 161 | } 162 | let vm_id = self.config.vm_id().to_string(); 163 | info!("Starting machine with VM ID `{vm_id}`"); 164 | 165 | self.cleanup_before_starting().await?; 166 | 167 | // FIXME: Assuming jailer for now. 168 | let jailer = self.config.jailer_cfg.as_mut().expect("no jailer config"); 169 | let jailer_bin = jailer.jailer_binary().to_owned(); 170 | let jailer_exec_path = jailer 171 | .exec_file() 172 | .to_str() 173 | .ok_or(Error::InvalidJailerExecPath)? 174 | .to_owned(); 175 | let jailer_exec_name = jailer 176 | .exec_file() 177 | .file_name() 178 | .and_then(|name| name.to_str()) 179 | .ok_or(Error::InvalidJailerExecPath)? 180 | .to_owned(); 181 | let (mut cmd, daemonize_arg, stdin, stdout, stderr) = match &mut jailer.mode { 182 | JailerMode::Daemon => ( 183 | Command::new(jailer.jailer_binary()), 184 | Some("--daemonize"), 185 | Stdio::null(), 186 | Stdio::null(), 187 | Stdio::null(), 188 | ), 189 | JailerMode::Attached(stdio) => ( 190 | Command::new(jailer_bin), 191 | None, 192 | stdio.stdin.take().unwrap_or_else(Stdio::inherit), 193 | stdio.stdout.take().unwrap_or_else(Stdio::inherit), 194 | stdio.stderr.take().unwrap_or_else(Stdio::inherit), 195 | ), 196 | JailerMode::Tmux(session_name) => { 197 | let session_name = session_name 198 | .clone() 199 | .unwrap_or_else(|| vm_id.to_string().into()); 200 | let mut cmd = Command::new("tmux"); 201 | cmd.args([ 202 | "new-session", 203 | "-d", 204 | "-s", 205 | &session_name, 206 | jailer.jailer_binary().to_str().unwrap(), 207 | ]); 208 | 209 | (cmd, None, Stdio::null(), Stdio::null(), Stdio::null()) 210 | } 211 | }; 212 | 213 | if let Some(daemonize_arg) = daemonize_arg { 214 | cmd.arg(daemonize_arg); 215 | } 216 | let cmd = cmd 217 | .args([ 218 | "--id", 219 | &vm_id, 220 | "--exec-file", 221 | &jailer_exec_path, 222 | "--uid", 223 | &jailer.uid().to_string(), 224 | "--gid", 225 | &jailer.gid().to_string(), 226 | "--chroot-base-dir", 227 | jailer 228 | .chroot_base_dir() 229 | .to_str() 230 | .ok_or(Error::InvalidChrootBasePath)?, 231 | // `firecracker` binary args. 232 | "--", 233 | "--api-sock", 234 | self.config 235 | .socket_path 236 | .to_str() 237 | .ok_or(Error::InvalidSocketPath)?, 238 | ]) 239 | .stdin(stdin) 240 | .stdout(stdout) 241 | .stderr(stderr); 242 | trace!("{vm_id}: Running command: {:?}", cmd); 243 | let mut child = cmd.spawn()?; 244 | if child.id().is_none() { 245 | let exit_status = child.wait().await?; 246 | return Err(Error::ProcessExitedImmediatelly { exit_status }); 247 | } 248 | self.pid = Some(self.wait_for_jailer(&jailer_exec_name).await?); 249 | 250 | if let Err(e) = self 251 | .setup_vm() 252 | .and_then(|_| async { 253 | trace!("{vm_id}: Booting the VM instance..."); 254 | 255 | self.send_action(Action::InstanceStart).await 256 | }) 257 | .await 258 | { 259 | warn!( 260 | "{vm_id}: Failed to boot VM instance: {}. Force shutting down..", 261 | e 262 | ); 263 | self.force_shutdown().await.unwrap_or_else(|e| { 264 | // We want to return to original error so only log the error from shutdown. 265 | warn!("{vm_id}: Failed to force shutdown: {}", e); 266 | }); 267 | 268 | return Err(e); 269 | } 270 | 271 | trace!("{vm_id}: VM started successfully."); 272 | 273 | Ok(()) 274 | } 275 | 276 | /// Forcefully shutdown the machine. 277 | /// 278 | /// This will be done by killing VM process. 279 | #[instrument(skip_all)] 280 | pub async fn force_shutdown(&mut self) -> Result<(), Error> { 281 | let vm_id = self.config.vm_id(); 282 | info!("{vm_id}: Killing VM..."); 283 | 284 | let pid = self.pid.ok_or(Error::ProcessNotStarted)?; 285 | match self.config.jailer_cfg().expect("no jailer config").mode() { 286 | JailerMode::Daemon | JailerMode::Attached(_) => { 287 | let killed = task::spawn_blocking(move || { 288 | let mut sys = System::new(); 289 | if sys.refresh_process_specifics(Pid::from_u32(pid), ProcessRefreshKind::new()) 290 | { 291 | match sys.process(Pid::from_u32(pid)) { 292 | Some(process) => Ok(process.kill()), 293 | None => Err(Error::ProcessNotRunning(pid)), 294 | } 295 | } else { 296 | Err(Error::ProcessNotRunning(pid)) 297 | } 298 | }) 299 | .await??; 300 | 301 | if !killed { 302 | return Err(Error::ProcessNotKilled(pid)); 303 | } 304 | trace!("{vm_id}: Successfully sent KILL signal to VM (pid: `{pid}`)."); 305 | } 306 | JailerMode::Tmux(session_name) => { 307 | let session_name = session_name 308 | .clone() 309 | .unwrap_or_else(|| vm_id.to_string().into()); 310 | // In case of tmux, we need to kill the tmux session. 311 | let cmd = &mut Command::new("tmux"); 312 | cmd.args(["kill-session", "-t", &session_name]); 313 | trace!("{vm_id}: Running command: {:?}", cmd); 314 | cmd.spawn()?.wait().await?; 315 | } 316 | } 317 | self.pid = None; 318 | Ok(()) 319 | } 320 | 321 | /// Shutdown requests a clean shutdown of the VM by sending CtrlAltDelete on the virtual keyboard. 322 | #[instrument(skip_all)] 323 | pub async fn shutdown(&self) -> Result<(), Error> { 324 | let vm_id = self.config.vm_id(); 325 | info!("{vm_id}: Sending CTRL+ALT+DEL to VM..."); 326 | self.send_action(Action::SendCtrlAltDel).await?; 327 | trace!("{vm_id}: CTRL+ALT+DEL sent to VM successfully."); 328 | Ok(()) 329 | } 330 | 331 | /// Delete the machine. 332 | /// 333 | /// Deletes the machine, cleaning up all associated resources. 334 | /// 335 | /// If machine is running, it is shut down before resources are deleted. 336 | #[instrument(skip_all)] 337 | pub async fn delete(mut self) -> Result<(), Error> { 338 | let vm_id = self.config.vm_id().to_string(); 339 | info!("{vm_id}: Deleting VM..."); 340 | 341 | let jailer_workspace_dir = self.config.jailer_cfg().unwrap().workspace_dir().to_owned(); 342 | 343 | if MachineState::RUNNING == self.state() { 344 | if let Err(err) = self.shutdown().await { 345 | warn!("{vm_id}: Shutdown error: {err}"); 346 | } else { 347 | info!("{vm_id}: Waiting for the VM process to shut down..."); 348 | sleep(Duration::from_secs(10)).await; 349 | } 350 | 351 | if let Err(err) = self.force_shutdown().await { 352 | warn!("{vm_id}: Forced shutdown error: {err}"); 353 | } 354 | } 355 | 356 | trace!("{vm_id}: Deleting VM resources..."); 357 | // The jailer workspace dir is `root` dir under the VM dir and we want to delete everything 358 | // related to the VM so we need to delete the VM dir, and not just the workspace dir under 359 | // it. 360 | let vm_dir = jailer_workspace_dir 361 | .parent() 362 | .expect("VM workspace dir must have a parent"); 363 | trace!( 364 | "{vm_id}: Deleting VM jailer directory at `{}`", 365 | vm_dir.display() 366 | ); 367 | fs::remove_dir_all(vm_dir).await?; 368 | trace!("{vm_id}: VM deleted successfully."); 369 | 370 | Ok(()) 371 | } 372 | 373 | /// Get the configuration of the machine. 374 | pub fn config(&self) -> &Config<'m> { 375 | &self.config 376 | } 377 | 378 | /// Checks the machine actual state 379 | /// 380 | /// Returns SHUTOFF is machine is not running 381 | pub fn state(&self) -> MachineState { 382 | if let Some(pid) = self.pid { 383 | let mut sys = System::new(); 384 | // TODO set self.pid=None somewhere if process doesn't exists anymore 385 | if sys.refresh_process_specifics(Pid::from_u32(pid), ProcessRefreshKind::new()) { 386 | sys.process(Pid::from_u32(pid)) 387 | .map_or(MachineState::SHUTOFF, |proc| { 388 | // sometime FC is not reaped by jailer for some time, so lets ignore 389 | // zombies for state purpose 390 | if proc.status() != ProcessStatus::Zombie { 391 | MachineState::RUNNING 392 | } else { 393 | MachineState::SHUTOFF 394 | } 395 | }) 396 | } else { 397 | MachineState::SHUTOFF 398 | } 399 | } else { 400 | MachineState::SHUTOFF 401 | } 402 | } 403 | 404 | #[instrument(skip_all)] 405 | async fn wait_for_jailer(&self, jailer_exec_name: &str) -> Result { 406 | let vm_id = self.config.vm_id(); 407 | // Wait jailer to start up and create the socket. 408 | info!("{vm_id}: Waiting for the jailer to start up..."); 409 | 410 | // get try to get FC version to verify if jailer already started 411 | let request_version = || async { 412 | if self 413 | .client 414 | .request( 415 | Request::builder() 416 | .method(Method::GET) 417 | .uri(Uri::new(self.config.host_socket_path(), "/version")) 418 | .header("Accept", "application/json") 419 | .header("Content-Type", "application/json") 420 | .body(Body::empty())?, 421 | ) 422 | .await? 423 | .status() 424 | .is_success() 425 | { 426 | Ok(()) 427 | } else { 428 | Err(Error::ProcessNotStarted) 429 | } 430 | }; 431 | let start = std::time::Instant::now(); 432 | let elapsed = || std::time::Instant::now() - start; 433 | while request_version().await.is_err() { 434 | if elapsed() < JAILER_START_TIMEOUT { 435 | sleep(Duration::from_millis(100)).await; 436 | } else { 437 | return Err(Error::JailerStartTimedOut); 438 | } 439 | } 440 | // get PID of started firecracker 441 | let mut sys = System::new(); 442 | sys.refresh_specifics( 443 | sysinfo::RefreshKind::new().with_processes(ProcessRefreshKind::everything()), 444 | ); 445 | let processes: Vec<_> = sys 446 | .processes_by_name(jailer_exec_name) 447 | .filter(|&process| process.cmd().contains(&vm_id.to_string())) 448 | .collect(); 449 | 450 | match processes.len() { 451 | 1 => Ok(processes[0].pid().as_u32()), 452 | _ => Err(Error::FailedToStart), 453 | } 454 | } 455 | 456 | #[instrument(skip_all)] 457 | async fn send_request(&self, url: hyper::Uri, body: String) -> Result<(), Error> { 458 | let vm_id = self.config.vm_id(); 459 | trace!("{vm_id}: sending request to url={url}, body={body}"); 460 | 461 | let request = Request::builder() 462 | .method(Method::PUT) 463 | .uri(url.clone()) 464 | .header("Accept", "application/json") 465 | .header("Content-Type", "application/json") 466 | .body(Body::from(body))?; 467 | 468 | let resp = self.client.request(request).await?; 469 | 470 | let status = resp.status(); 471 | if status.is_success() { 472 | trace!("{vm_id}: request to url={url} successful"); 473 | } else { 474 | let body = hyper::body::to_bytes(resp.into_body()).await?; 475 | let body = if body.is_empty() { 476 | trace!("{vm_id}: request to url={url} failed: status={status}"); 477 | None 478 | } else { 479 | let body = String::from_utf8_lossy(&body).into_owned(); 480 | trace!("{vm_id}: request to url={url} failed: status={status}, body={body}"); 481 | Some(body) 482 | }; 483 | return Err(Error::FirecrackerAPIError { status, body }); 484 | } 485 | 486 | Ok(()) 487 | } 488 | 489 | async fn send_action(&self, action: Action) -> Result<(), Error> { 490 | let url: hyper::Uri = Uri::new(self.config.host_socket_path(), "/actions").into(); 491 | let json = serde_json::to_string(&action)?; 492 | self.send_request(url, json).await?; 493 | 494 | Ok(()) 495 | } 496 | 497 | /// Prepare the machine for running. 498 | #[instrument(skip_all)] 499 | async fn setup_vm(&self) -> Result<(), Error> { 500 | let vm_id = self.config.vm_id(); 501 | info!("{vm_id}: Setting the VM..."); 502 | self.setup_resources().await?; 503 | self.setup_boot_source().await?; 504 | self.setup_drives().await?; 505 | self.setup_network().await?; 506 | self.setup_vsock().await?; 507 | trace!("{vm_id}: VM successfully setup."); 508 | 509 | Ok(()) 510 | } 511 | 512 | #[instrument(skip_all)] 513 | async fn setup_resources(&self) -> Result<(), Error> { 514 | let vm_id = self.config.vm_id(); 515 | trace!("{vm_id}: Configuring machine resources..."); 516 | let json = serde_json::to_string(self.config.machine_cfg())?; 517 | let url: hyper::Uri = Uri::new(self.config.host_socket_path(), "/machine-config").into(); 518 | self.send_request(url, json).await?; 519 | trace!("{vm_id}: Machine resources configured successfully."); 520 | 521 | Ok(()) 522 | } 523 | 524 | #[instrument(skip_all)] 525 | async fn setup_boot_source(&self) -> Result<(), Error> { 526 | let vm_id = self.config.vm_id(); 527 | trace!("{vm_id}: Configuring boot source..."); 528 | let boot_source = self.config.boot_source()?; 529 | let json = serde_json::to_string(&boot_source)?; 530 | let url: hyper::Uri = Uri::new(self.config.host_socket_path(), "/boot-source").into(); 531 | self.send_request(url, json).await?; 532 | trace!("{vm_id}: Boot source configured successfully."); 533 | 534 | Ok(()) 535 | } 536 | 537 | #[instrument(skip_all)] 538 | async fn setup_drives(&self) -> Result<(), Error> { 539 | let vm_id = self.config.vm_id(); 540 | trace!("{vm_id}: Configuring drives..."); 541 | for drive in &self.config.drives { 542 | let path = format!("/drives/{}", drive.drive_id()); 543 | let url: hyper::Uri = Uri::new(self.config.host_socket_path(), &path).into(); 544 | // Send modified drive object, with drive file in chroot location 545 | let mut drive_obj = drive.clone(); 546 | let drive_filename = drive 547 | .src_path() 548 | .file_name() 549 | .ok_or(Error::InvalidDrivePath)?; 550 | drive_obj.src_path = Path::new(&drive_filename).into(); 551 | let json = serde_json::to_string(&drive_obj)?; 552 | self.send_request(url, json).await?; 553 | } 554 | trace!("{vm_id}: Drives configured successfully."); 555 | 556 | Ok(()) 557 | } 558 | 559 | #[instrument(skip_all)] 560 | async fn setup_network(&self) -> Result<(), Error> { 561 | let vm_id = self.config.vm_id(); 562 | trace!("{vm_id}: Configuring network..."); 563 | for network in self.config.network_interfaces() { 564 | let json = serde_json::to_string(network)?; 565 | let path = format!("/network-interfaces/{}", network.vm_if_name()); 566 | let url: hyper::Uri = Uri::new(self.config.host_socket_path(), &path).into(); 567 | self.send_request(url, json).await?; 568 | } 569 | trace!("{vm_id}: All networks configured successfully."); 570 | Ok(()) 571 | } 572 | 573 | #[instrument(skip_all)] 574 | async fn setup_vsock(&self) -> Result<(), Error> { 575 | let vsock_cfg = match self.config.vsock_cfg() { 576 | Some(vsock) => vsock, 577 | None => return Ok(()), 578 | }; 579 | let vm_id = self.config.vm_id(); 580 | trace!("{vm_id}: Configuring vsock..."); 581 | let url: hyper::Uri = Uri::new(self.config.host_socket_path(), "/vsock").into(); 582 | let json = serde_json::to_string(vsock_cfg)?; 583 | self.send_request(url, json).await?; 584 | trace!("{vm_id}: vsock configured successfully."); 585 | 586 | Ok(()) 587 | } 588 | 589 | #[instrument(skip_all)] 590 | async fn cleanup_before_starting(&self) -> Result<(), Error> { 591 | let vm_id = self.config.vm_id(); 592 | trace!("{vm_id}: Deleting intermediate VM resources before starting..."); 593 | let socket_path = self.config.host_socket_path(); 594 | trace!("{vm_id}: Removing socket file {}...", socket_path.display()); 595 | match fs::remove_file(&socket_path).await { 596 | Ok(_) => trace!("{vm_id}: Deleted `{}`", socket_path.display()), 597 | Err(e) if e.kind() == ErrorKind::NotFound => { 598 | trace!("{vm_id}: `{}` not found", socket_path.display()) 599 | } 600 | Err(e) => return Err(e.into()), 601 | } 602 | 603 | let jailer_workspace_dir = self.config.jailer().workspace_dir(); 604 | 605 | // Remove the vsock socket file if it exists. 606 | if let Some(path) = self.config.vsock_cfg().map(|v| v.uds_path()) { 607 | let relative_path = path.strip_prefix("/").unwrap_or(path); 608 | let path = jailer_workspace_dir.join(relative_path); 609 | trace!("{vm_id}: Removing vsock socket file {}...", path.display()); 610 | match fs::remove_file(&path).await { 611 | Ok(_) => trace!("{vm_id}: Deleted `{}`", path.display()), 612 | Err(e) if e.kind() == ErrorKind::NotFound => { 613 | trace!("{vm_id}: `{}` not found", path.display()) 614 | } 615 | Err(e) => return Err(e.into()), 616 | } 617 | } 618 | 619 | let dev_dir = jailer_workspace_dir.join("dev"); 620 | trace!("{vm_id}: Deleting `{}`", dev_dir.display()); 621 | match fs::remove_dir_all(&dev_dir).await { 622 | Ok(_) => trace!("{vm_id}: Deleted `{}`", dev_dir.display()), 623 | Err(e) if e.kind() == ErrorKind::NotFound => { 624 | trace!("{vm_id}: `{}` not found", dev_dir.display()) 625 | } 626 | Err(e) => return Err(e.into()), 627 | } 628 | 629 | Ok(()) 630 | } 631 | } 632 | 633 | #[derive(Debug, Serialize)] 634 | #[serde(tag = "action_type", rename_all = "PascalCase")] 635 | enum Action { 636 | InstanceStart, 637 | SendCtrlAltDel, 638 | #[allow(unused)] 639 | FlushMetrics, 640 | } 641 | --------------------------------------------------------------------------------