diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml index 89739b3..80f6ad1 100644 --- a/.github/workflows/code_quality.yml +++ b/.github/workflows/code_quality.yml @@ -26,8 +26,17 @@ jobs: - name: Install asciidoctor run: sudo apt-get install -y asciidoctor + - name: Install additional Rust rust targets + run: rustup target add aarch64-unknown-linux-gnu aarch64-apple-darwin + - name: Formatting (rustfmt) run: cargo fmt -- --check - - name: Clippy (all features) - run: cargo clippy --all-targets --all-features + - name: Clippy x86_64-unknown-linux-gnu (all features) + run: cargo clippy --all-features --target x86_64-unknown-linux-gnu + + - name: Clippy aarch64-unknown-linux-gnu (all features) + run: cargo clippy --all-features --target aarch64-unknown-linux-gnu + + - name: Clippy aarch64-apple-darwin (all features) + run: cargo clippy --all-features --target aarch64-apple-darwin diff --git a/Cargo.lock b/Cargo.lock index 509f15c..8933ac6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,12 +3,51 @@ version = 3 [[package]] -name = "ansi_term" -version = "0.11.0" +name = "anstream" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" dependencies = [ - "winapi", + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", ] [[package]] @@ -23,17 +62,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.0.1" @@ -48,9 +76,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bitflags" -version = "1.2.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "blake2b_simd" @@ -77,19 +105,50 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "2.33.3" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" dependencies = [ - "ansi_term", - "atty", - "bitflags", + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", "strsim", - "textwrap", - "unicode-width", - "vec_map", ] +[[package]] +name = "clap_derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "confy" version = "0.4.0" @@ -151,13 +210,10 @@ dependencies = [ ] [[package]] -name = "hermit-abi" -version = "0.1.18" +name = "heck" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" -dependencies = [ - "libc", -] +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "krunvm" @@ -166,6 +222,7 @@ dependencies = [ "clap", "confy", "libc", + "nix", "serde", "serde_derive", "text_io", @@ -179,24 +236,45 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.90" +version = "0.2.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" + +[[package]] +name = "memoffset" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4aede83fc3617411dc6993bc8c70919750c1c257c6ca6a502aed6e0e2394ae" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "libc", + "memoffset", +] [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.9" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -244,14 +322,14 @@ checksum = "1800f7693e94e186f5e25a28291ae1570da908aff7d97a095dec1e56ff99069b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.64", ] [[package]] name = "strsim" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" @@ -265,19 +343,21 @@ dependencies = [ ] [[package]] -name = "text_io" -version = "0.1.8" +name = "syn" +version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb170b4f47dc48835fbc56259c12d8963e542b05a24be2e3a1f5a6c320fd2d4" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] [[package]] -name = "textwrap" -version = "0.11.0" +name = "text_io" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] +checksum = "6cb170b4f47dc48835fbc56259c12d8963e542b05a24be2e3a1f5a6c320fd2d4" [[package]] name = "toml" @@ -289,10 +369,10 @@ dependencies = [ ] [[package]] -name = "unicode-width" -version = "0.1.8" +name = "unicode-ident" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-xid" @@ -301,10 +381,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" [[package]] -name = "vec_map" -version = "0.8.2" +name = "utf8parse" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "wasi" @@ -333,3 +413,69 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/Cargo.toml b/Cargo.toml index a345f02..80451e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,10 @@ edition = "2018" build = "build.rs" [dependencies] -clap = "2.33.3" +clap = {version = "4.4.6", features = ["derive"]} confy = "0.4.0" libc = "0.2.82" serde = "1.0.120" serde_derive = "1.0.120" text_io = "0.1.8" +nix = {version = "0.27.1", features = ["socket", "fs"]} \ No newline at end of file diff --git a/docs/krunvm-changevm.1.txt b/docs/krunvm-changevm.1.txt index 4f14e90..e00838d 100644 --- a/docs/krunvm-changevm.1.txt +++ b/docs/krunvm-changevm.1.txt @@ -61,6 +61,8 @@ host visible in the guest. An empty string ("") tells krunvm to not set a working directory explicitly, letting libkrun decide which one should be set. +*--net* _NETWORK_MODE_:: + Configures the network connection mode. Supported modes are either PASST or TSI. SEE ALSO -------- diff --git a/docs/krunvm-config.1.txt b/docs/krunvm-config.1.txt index 168ed91..ae3b416 100644 --- a/docs/krunvm-config.1.txt +++ b/docs/krunvm-config.1.txt @@ -34,6 +34,9 @@ OPTIONS Sets the default mount of RAM, in MiB, that will be configured for newly created microVMs. +*--net* _NETWORK_MODE_:: + Sets the default network connection mode, that will be configured for + newly created microVMs. Supported modes are PASST or TSI. SEE ALSO -------- diff --git a/docs/krunvm-create.1.txt b/docs/krunvm-create.1.txt index d63f3f8..488bcb4 100644 --- a/docs/krunvm-create.1.txt +++ b/docs/krunvm-create.1.txt @@ -53,6 +53,8 @@ host visible in the guest. An empty string ("") tells krunvm to not set a working directory explicitly, letting libkrun decide which one should be set. +*--net* _NETWORK_MODE_:: + Set the network connection mode. Supported modes are either PASST or TSI. SEE ALSO -------- diff --git a/docs/krunvm.1.txt b/docs/krunvm.1.txt index 8a956e8..e04ea82 100644 --- a/docs/krunvm.1.txt +++ b/docs/krunvm.1.txt @@ -29,10 +29,15 @@ microVM and exposing ports from the guest to the host (and the networks connected to it). Networking to the guest running in the microVM is provided by -libkrun's TSI (Transparent Socket Impersonation), enabling a seamless -experience that doesn't require network bridges nor other explicit -network configuration. +either libkrun's TSI (Transparent Socket Impersonation) or PASST. +TSI enables a seamless experience that doesn't require network bridges nor other explicit +network configuration. It only supports impersonating AF_INET SOCK_DGRAM and SOCK_STREAM sockets. +This implies it's not possible to communicate outside the VM with raw sockets. + +PASST uses virtio-net guest device and sends all traffic to a passt subprocess. +Support of network protocols is therefore dependent on what passt supports. +Note that currently you need to run a DHCP client in the guest to get an IP address. GLOBAL OPTIONS -------------- diff --git a/src/bindings.rs b/src/bindings.rs index 16b0894..b8676e3 100644 --- a/src/bindings.rs +++ b/src/bindings.rs @@ -1,7 +1,7 @@ // Copyright 2021 Red Hat, Inc. // SPDX-License-Identifier: Apache-2.0 -use libc::c_char; +use libc::{c_char, c_int}; #[link(name = "krun")] extern "C" { @@ -13,6 +13,7 @@ extern "C" { pub fn krun_set_mapped_volumes(ctx: u32, mapped_volumes: *const *const c_char) -> i32; pub fn krun_set_port_map(ctx: u32, port_map: *const *const c_char) -> i32; pub fn krun_set_workdir(ctx: u32, workdir_path: *const c_char) -> i32; + pub fn krun_set_passt_fd(ctx: u32, fd: c_int) -> i32; pub fn krun_set_exec( ctx: u32, exec_path: *const c_char, diff --git a/src/changevm.rs b/src/changevm.rs deleted file mode 100644 index 99499c2..0000000 --- a/src/changevm.rs +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright 2021 Red Hat, Inc. -// SPDX-License-Identifier: Apache-2.0 - -use std::collections::HashMap; - -use crate::{ArgMatches, KrunvmConfig, APP_NAME}; - -use super::list::printvm; -use super::utils::{parse_mapped_ports, parse_mapped_volumes}; - -pub fn changevm(cfg: &mut KrunvmConfig, matches: &ArgMatches) { - let mut cfg_changed = false; - - let name = matches.value_of("NAME").unwrap(); - - let vmcfg = if let Some(new_name) = matches.value_of("new-name") { - if cfg.vmconfig_map.contains_key(new_name) { - println!("A VM with name {} already exists", new_name); - std::process::exit(-1); - } - - let mut vmcfg = match cfg.vmconfig_map.remove(name) { - None => { - println!("No VM found with name {}", name); - std::process::exit(-1); - } - Some(vmcfg) => vmcfg, - }; - - cfg_changed = true; - let name = new_name.to_string(); - vmcfg.name = name.clone(); - cfg.vmconfig_map.insert(name.clone(), vmcfg); - cfg.vmconfig_map.get_mut(&name).unwrap() - } else { - match cfg.vmconfig_map.get_mut(name) { - None => { - println!("No VM found with name {}", name); - std::process::exit(-1); - } - Some(vmcfg) => vmcfg, - } - }; - - if let Some(cpus_str) = matches.value_of("cpus") { - match cpus_str.parse::() { - Err(_) => println!("Invalid value for \"cpus\""), - Ok(cpus) => { - if cpus > 8 { - println!("Error: the maximum number of CPUs supported is 8"); - } else { - vmcfg.cpus = cpus; - cfg_changed = true; - } - } - } - } - - if let Some(mem_str) = matches.value_of("mem") { - match mem_str.parse::() { - Err(_) => println!("Invalid value for \"mem\""), - Ok(mem) => { - if mem > 16384 { - println!("Error: the maximum amount of RAM supported is 16384 MiB"); - } else { - vmcfg.mem = mem; - cfg_changed = true; - } - } - } - } - - if matches.is_present("remove-volumes") { - vmcfg.mapped_volumes = HashMap::new(); - cfg_changed = true; - } else { - let volume_matches = if matches.is_present("volume") { - matches.values_of("volume").unwrap().collect() - } else { - vec![] - }; - let mapped_volumes = parse_mapped_volumes(volume_matches); - - if !mapped_volumes.is_empty() { - vmcfg.mapped_volumes = mapped_volumes; - cfg_changed = true; - } - } - - if matches.is_present("remove-ports") { - vmcfg.mapped_ports = HashMap::new(); - cfg_changed = true; - } else { - let port_matches = if matches.is_present("port") { - matches.values_of("port").unwrap().collect() - } else { - vec![] - }; - let mapped_ports = parse_mapped_ports(port_matches); - - if !mapped_ports.is_empty() { - vmcfg.mapped_ports = mapped_ports; - cfg_changed = true; - } - } - - if let Some(workdir) = matches.value_of("workdir") { - vmcfg.workdir = workdir.to_string(); - cfg_changed = true; - } - - println!(); - printvm(vmcfg); - println!(); - - if cfg_changed { - confy::store(APP_NAME, &cfg).unwrap(); - } -} diff --git a/src/commands/changevm.rs b/src/commands/changevm.rs new file mode 100644 index 0000000..9b7e9d7 --- /dev/null +++ b/src/commands/changevm.rs @@ -0,0 +1,150 @@ +// Copyright 2021 Red Hat, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use clap::Args; +use std::collections::HashMap; + +use crate::config::{KrunvmConfig, NetworkMode}; +use crate::utils::{path_pairs_to_hash_map, port_pairs_to_hash_map, PathPair, PortPair}; + +use super::list::printvm; + +/// Change the configuration of a microVM +#[derive(Args, Debug)] +pub struct ChangeVmCmd { + /// Name of the VM to be modified + name: String, + + /// Assign a new name to the VM + #[arg(long)] + new_name: Option, + + /// Number of vCPUs + #[arg(long)] + cpus: Option, + + /// Amount of RAM in MiB + #[arg(long)] + mem: Option, + + /// Working directory inside the microVM + #[arg(short, long)] + workdir: Option, + + /// Remove all volume mappings + #[arg(long)] + remove_volumes: bool, + + /// Volume(s) in form "host_path:guest_path" to be exposed to the guest + #[arg(short, long = "volume")] + volumes: Vec, + + /// Remove all port mappings + #[arg(long)] + remove_ports: bool, + + /// Port(s) in format "host_port:guest_port" to be exposed to the host + #[arg(long = "port")] + ports: Vec, + + /// Set the network connection mode for the microVM + #[arg(long)] + net: Option, +} + +impl ChangeVmCmd { + pub fn run(self, cfg: &mut KrunvmConfig) { + let mut cfg_changed = false; + + let vmcfg = if let Some(new_name) = &self.new_name { + if cfg.vmconfig_map.contains_key(new_name) { + println!("A VM with name {} already exists", new_name); + std::process::exit(-1); + } + + let mut vmcfg = match cfg.vmconfig_map.remove(&self.name) { + None => { + println!("No VM found with name {}", &self.name); + std::process::exit(-1); + } + Some(vmcfg) => vmcfg, + }; + + cfg_changed = true; + let name = new_name.to_string(); + vmcfg.name = name.clone(); + cfg.vmconfig_map.insert(name.clone(), vmcfg); + cfg.vmconfig_map.get_mut(&name).unwrap() + } else { + match cfg.vmconfig_map.get_mut(&self.name) { + None => { + println!("No VM found with name {}", self.name); + std::process::exit(-1); + } + Some(vmcfg) => vmcfg, + } + }; + + if let Some(cpus) = self.cpus { + if cpus > 8 { + println!("Error: the maximum number of CPUs supported is 8"); + } else { + vmcfg.cpus = cpus; + cfg_changed = true; + } + } + + if let Some(mem) = self.mem { + if mem > 16384 { + println!("Error: the maximum amount of RAM supported is 16384 MiB"); + } else { + vmcfg.mem = mem; + cfg_changed = true; + } + } + + if self.remove_volumes { + vmcfg.mapped_volumes = HashMap::new(); + cfg_changed = true; + } else { + let mapped_volumes = path_pairs_to_hash_map(self.volumes); + + if !mapped_volumes.is_empty() { + vmcfg.mapped_volumes = mapped_volumes; + cfg_changed = true; + } + } + // TODO: don't just silently ignore --volume args when --remove_volumes is specified + + if self.remove_ports { + vmcfg.mapped_ports = HashMap::new(); + cfg_changed = true; + } else { + let mapped_ports = port_pairs_to_hash_map(self.ports); + + if !mapped_ports.is_empty() { + vmcfg.mapped_ports = mapped_ports; + cfg_changed = true; + } + } + // TODO: don't just silently ignore --port args when --remove_ports is specified + + if let Some(workdir) = self.workdir { + vmcfg.workdir = workdir.to_string(); + cfg_changed = true; + } + + if let Some(network_mode) = self.net { + vmcfg.network_mode = network_mode; + cfg_changed = true; + } + + println!(); + printvm(vmcfg); + println!(); + + if cfg_changed { + crate::config::save(cfg).unwrap(); + } + } +} diff --git a/src/commands/config.rs b/src/commands/config.rs new file mode 100644 index 0000000..1e3b372 --- /dev/null +++ b/src/commands/config.rs @@ -0,0 +1,79 @@ +// Copyright 2021 Red Hat, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::{KrunvmConfig, NetworkMode}; +use clap::Args; + +/// Configure global values +#[derive(Args, Debug)] +pub struct ConfigCmd { + // Default number of vCPUs for newly created VMs + #[arg(long)] + cpus: Option, + + ///Default amount of RAM in MiB for newly created VMs + #[arg(long)] + mem: Option, + + /// DNS server to use in the microVM + #[arg(long)] + dns: Option, + + /// Default network connection mode to use + #[arg(long)] + net: Option, +} + +impl ConfigCmd { + pub fn run(self, cfg: &mut KrunvmConfig) { + let mut cfg_changed = false; + + if let Some(cpus) = self.cpus { + if cpus > 8 { + println!("Error: the maximum number of CPUs supported is 8"); + } else { + cfg.default_cpus = cpus; + cfg_changed = true; + } + } + + if let Some(mem) = self.mem { + if mem > 16384 { + println!("Error: the maximum amount of RAM supported is 16384 MiB"); + } else { + cfg.default_mem = mem; + cfg_changed = true; + } + } + + if let Some(dns) = self.dns { + cfg.default_dns = dns; + cfg_changed = true; + } + + if let Some(network_mode) = self.net { + if network_mode != cfg.default_network_mode { + cfg.default_network_mode = network_mode; + cfg_changed = true; + } + } + + if cfg_changed { + crate::config::save(cfg).unwrap(); + } + + println!("Global config:"); + println!( + "Default number of CPUs for newly created VMs: {}", + cfg.default_cpus + ); + println!( + "Default amount of RAM (MiB) for newly created VMs: {}", + cfg.default_mem + ); + println!( + "Default DNS server for newly created VMs: {}", + cfg.default_dns + ); + } +} diff --git a/src/commands/create.rs b/src/commands/create.rs new file mode 100644 index 0000000..60d7ae0 --- /dev/null +++ b/src/commands/create.rs @@ -0,0 +1,235 @@ +// Copyright 2021 Red Hat, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::{KrunvmConfig, NetworkMode, VmConfig}; +use crate::APP_NAME; +use clap::Args; +use std::fs; +use std::io::Write; +#[cfg(target_os = "macos")] +use std::path::Path; +use std::process::Command; + +use crate::utils::{ + get_buildah_args, mount_container, path_pairs_to_hash_map, port_pairs_to_hash_map, + umount_container, BuildahCommand, PathPair, PortPair, +}; +#[cfg(target_os = "macos")] +const KRUNVM_ROSETTA_FILE: &str = ".krunvm-rosetta"; + +/// Create a new microVM +#[derive(Args, Debug)] +pub struct CreateCmd { + /// OCI image to use as template + image: String, + + /// Assign a name to the VM + #[arg(long)] + name: Option, + + /// Number of vCPUs + #[arg(long)] + cpus: Option, + + /// Amount of RAM in MiB + #[arg(long)] + mem: Option, + + /// DNS server to use in the microVM + #[arg(long)] + dns: Option, + + /// Working directory inside the microVM + #[arg(short, long, default_value = "")] + workdir: String, + + /// Volume(s) in form "host_path:guest_path" to be exposed to the guest + #[arg(short, long = "volume")] + volumes: Vec, + + /// Port(s) in format "host_port:guest_port" to be exposed to the host + #[arg(long = "port")] + ports: Vec, + + /// Network connection mode to use + #[arg(long)] + net: Option, + + /// Create a x86_64 microVM even on an Aarch64 host + #[arg(short, long)] + #[cfg(target_os = "macos")] + x86: bool, +} + +impl CreateCmd { + pub fn run(self, cfg: &mut KrunvmConfig) { + #[allow(unused_mut)] + let mut cpus = self.cpus.unwrap_or(cfg.default_cpus); + let mem = self.mem.unwrap_or(cfg.default_mem); + let dns = self.dns.unwrap_or_else(|| cfg.default_dns.clone()); + let workdir = self.workdir; + let mapped_volumes = path_pairs_to_hash_map(self.volumes); + let mapped_ports = port_pairs_to_hash_map(self.ports); + let image = self.image; + let name = self.name; + let network_mode = self.net.unwrap_or_else(|| cfg.default_network_mode.clone()); + + if let Some(ref name) = name { + if cfg.vmconfig_map.contains_key(name) { + println!("A VM with this name already exists"); + std::process::exit(-1); + } + } + + let mut args = get_buildah_args(cfg, BuildahCommand::From); + + #[cfg(target_os = "macos")] + let force_x86 = self.x86; + + #[cfg(target_os = "macos")] + if force_x86 { + let home = match std::env::var("HOME") { + Err(e) => { + println!("Error reading \"HOME\" enviroment variable: {}", e); + std::process::exit(-1); + } + Ok(home) => home, + }; + + let path = format!("{}/{}", home, KRUNVM_ROSETTA_FILE); + if !Path::new(&path).is_file() { + println!( + " +To use Rosetta for Linux you need to create the file... + +{} + +...with the contents that the \"rosetta\" binary expects to be served from +its specific ioctl. + +For more information, please refer to this post: +https://threedots.ovh/blog/2022/06/quick-look-at-rosetta-on-linux/ +", + path + ); + std::process::exit(-1); + } + + if cpus != 1 { + println!("x86 microVMs on Aarch64 are restricted to 1 CPU"); + cpus = 1; + } + args.push("--arch".to_string()); + args.push("x86_64".to_string()); + } + + args.push(image.to_string()); + + let output = match Command::new("buildah") + .args(&args) + .stderr(std::process::Stdio::inherit()) + .output() + { + Ok(output) => output, + Err(err) => { + if err.kind() == std::io::ErrorKind::NotFound { + println!("{} requires buildah to manage the OCI images, and it wasn't found on this system.", APP_NAME); + } else { + println!("Error executing buildah: {}", err); + } + std::process::exit(-1); + } + }; + + let exit_code = output.status.code().unwrap_or(-1); + if exit_code != 0 { + println!( + "buildah returned an error: {}", + std::str::from_utf8(&output.stdout).unwrap() + ); + std::process::exit(-1); + } + + let container = std::str::from_utf8(&output.stdout).unwrap().trim(); + let name = if let Some(name) = name { + name.to_string() + } else { + container.to_string() + }; + let vmcfg = VmConfig { + name: name.clone(), + cpus, + mem, + dns: dns.to_string(), + container: container.to_string(), + workdir: workdir.to_string(), + mapped_volumes, + mapped_ports, + network_mode, + }; + + let rootfs = mount_container(cfg, &vmcfg).unwrap(); + export_container_config(cfg, &rootfs, &image).unwrap(); + fix_resolv_conf(&rootfs, &dns).unwrap(); + #[cfg(target_os = "macos")] + if force_x86 { + _ = fs::create_dir(format!("{}/.rosetta", rootfs)); + } + umount_container(cfg, &vmcfg).unwrap(); + + cfg.vmconfig_map.insert(name.clone(), vmcfg); + confy::store(APP_NAME, cfg).unwrap(); + + println!("microVM created with name: {}", name); + } +} + +fn fix_resolv_conf(rootfs: &str, dns: &str) -> Result<(), std::io::Error> { + let resolvconf_dir = format!("{}/etc/", rootfs); + fs::create_dir_all(resolvconf_dir)?; + let resolvconf = format!("{}/etc/resolv.conf", rootfs); + let mut file = fs::File::create(resolvconf)?; + file.write_all(b"options use-vc\nnameserver ")?; + file.write_all(dns.as_bytes())?; + file.write_all(b"\n")?; + Ok(()) +} + +fn export_container_config( + cfg: &KrunvmConfig, + rootfs: &str, + image: &str, +) -> Result<(), std::io::Error> { + let mut args = get_buildah_args(cfg, BuildahCommand::Inspect); + args.push(image.to_string()); + + let output = match Command::new("buildah") + .args(&args) + .stderr(std::process::Stdio::inherit()) + .output() + { + Ok(output) => output, + Err(err) => { + if err.kind() == std::io::ErrorKind::NotFound { + println!("{} requires buildah to manage the OCI images, and it wasn't found on this system.", APP_NAME); + } else { + println!("Error executing buildah: {}", err); + } + std::process::exit(-1); + } + }; + + let exit_code = output.status.code().unwrap_or(-1); + if exit_code != 0 { + println!( + "buildah returned an error: {}", + std::str::from_utf8(&output.stdout).unwrap() + ); + std::process::exit(-1); + } + + let mut file = fs::File::create(format!("{}/.krun_config.json", rootfs))?; + file.write_all(&output.stdout)?; + + Ok(()) +} diff --git a/src/commands/delete.rs b/src/commands/delete.rs new file mode 100644 index 0000000..d027a92 --- /dev/null +++ b/src/commands/delete.rs @@ -0,0 +1,32 @@ +// Copyright 2021 Red Hat, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::config; +use crate::config::KrunvmConfig; +use clap::Args; + +use crate::utils::{remove_container, umount_container}; + +/// Delete an existing microVM +#[derive(Args, Debug)] +pub struct DeleteCmd { + /// Name of the microVM to be deleted + name: String, +} + +impl DeleteCmd { + pub fn run(self, cfg: &mut KrunvmConfig) { + let vmcfg = match cfg.vmconfig_map.remove(&self.name) { + None => { + println!("No VM found with that name"); + std::process::exit(-1); + } + Some(vmcfg) => vmcfg, + }; + + umount_container(cfg, &vmcfg).unwrap(); + remove_container(cfg, &vmcfg).unwrap(); + + config::save(cfg).unwrap() + } +} diff --git a/src/commands/list.rs b/src/commands/list.rs new file mode 100644 index 0000000..7ca03cf --- /dev/null +++ b/src/commands/list.rs @@ -0,0 +1,39 @@ +// Copyright 2021 Red Hat, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::{KrunvmConfig, VmConfig}; +use clap::Args; + +/// List microVMs +#[derive(Args, Debug)] +pub struct ListCmd { + /// Print debug information verbosely + #[arg(short)] + pub debug: bool, //TODO: implement or remove this +} + +impl ListCmd { + pub fn run(self, cfg: &KrunvmConfig) { + if cfg.vmconfig_map.is_empty() { + println!("No microVMs found"); + } else { + for (_name, vm) in cfg.vmconfig_map.iter() { + println!(); + printvm(vm); + } + println!(); + } + } +} + +pub fn printvm(vm: &VmConfig) { + println!("{}", vm.name); + println!(" CPUs: {}", vm.cpus); + println!(" RAM (MiB): {}", vm.mem); + println!(" DNS server: {}", vm.dns); + println!(" Buildah container: {}", vm.container); + println!(" Workdir: {}", vm.workdir); + println!(" Network mode: {:?}", vm.network_mode); + println!(" Mapped volumes: {:?}", vm.mapped_volumes); + println!(" Mapped ports: {:?}", vm.mapped_ports); +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..256e206 --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,13 @@ +mod changevm; +mod config; +mod create; +mod delete; +mod list; +mod start; + +pub use changevm::ChangeVmCmd; +pub use config::ConfigCmd; +pub use create::CreateCmd; +pub use delete::DeleteCmd; +pub use list::ListCmd; +pub use start::StartCmd; diff --git a/src/start.rs b/src/commands/start.rs similarity index 55% rename from src/start.rs rename to src/commands/start.rs index 7dc5f02..11641a5 100644 --- a/src/start.rs +++ b/src/commands/start.rs @@ -1,17 +1,126 @@ // Copyright 2021 Red Hat, Inc. // SPDX-License-Identifier: Apache-2.0 +use clap::Args; +use libc::{c_char, c_int}; +use nix::errno::Errno; +use nix::sys::socket::{socketpair, AddressFamily, SockFlag, SockType}; +use std::collections::HashMap; use std::ffi::CString; + use std::fs::File; #[cfg(target_os = "linux")] use std::io::{Error, ErrorKind}; + +use std::os::fd::{IntoRawFd, OwnedFd}; use std::os::unix::io::AsRawFd; + #[cfg(target_os = "macos")] use std::path::Path; +use std::process::Stdio; + +use nix::fcntl::{fcntl, FcntlArg, FdFlag}; + +use crate::bindings; +use crate::bindings::krun_set_passt_fd; +use crate::config::{KrunvmConfig, NetworkMode, VmConfig}; +use crate::utils::{mount_container, umount_container}; + +#[derive(Args, Debug)] +/// Start an existing microVM +pub struct StartCmd { + /// Name of the microVM + name: String, + + /// Command to run inside the VM + command: Option, + + /// Arguments to be passed to the command executed in the VM + args: Vec, + + /// Number of vCPUs + #[arg(long)] + cpus: Option, // TODO: implement or remove this + + /// Amount of RAM in MiB + #[arg(long)] + mem: Option, // TODO: implement or remove this +} + +fn start_passt(mapped_ports: &HashMap) -> Result { + let (passt_fd, krun_fd) = socketpair( + AddressFamily::Unix, + SockType::Stream, + None, + SockFlag::empty(), + ) + .map_err(|e| { + eprint!("Failed to create socket pair for passt: {e}"); + })?; + + if let Err(e) = fcntl(krun_fd.as_raw_fd(), FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC)) { + eprint!("Failed to set FD_CLOEXEC: {e}"); + } -use super::bindings; -use super::utils::{mount_container, umount_container}; -use crate::{ArgMatches, KrunvmConfig, VmConfig}; + let mut cmd = std::process::Command::new("passt"); + cmd.arg("-q") + .arg("-f") + .arg("-F") + .arg(passt_fd.as_raw_fd().to_string()); + + if !mapped_ports.is_empty() { + let comma_separated_ports = mapped_ports + .iter() + .map(|(host_port, guest_port)| format!("{}:{}", host_port, guest_port)) + .collect::>() + .join(","); + + cmd.arg("-t").arg(comma_separated_ports); + } + + cmd.stdout(Stdio::null()) + .stderr(Stdio::null()) + .stdin(Stdio::null()); + + if let Err(e) = cmd.spawn() { + eprintln!("Failed to start passt: {e}"); + return Err(()); + } + + Ok(krun_fd) +} + +impl StartCmd { + pub fn run(self, cfg: &KrunvmConfig) { + let vmcfg = match cfg.vmconfig_map.get(&self.name) { + None => { + println!("No VM found with name {}", self.name); + std::process::exit(-1); + } + Some(vmcfg) => vmcfg, + }; + + umount_container(cfg, vmcfg).expect("Error unmounting container"); + let rootfs = mount_container(cfg, vmcfg).expect("Error mounting container"); + + let vm_args: Vec = if self.command.is_some() { + self.args + .into_iter() + .map(|val| CString::new(val).unwrap()) + .collect() + } else { + Vec::new() + }; + + set_rlimits(); + + let _file = set_lock(&rootfs); + + unsafe { exec_vm(vmcfg, &rootfs, self.command.as_deref(), vm_args) }; + + umount_container(cfg, vmcfg).expect("Error unmounting container"); + } +} #[cfg(target_os = "linux")] fn map_volumes(_ctx: u32, vmcfg: &VmConfig, rootfs: &str) { @@ -91,15 +200,35 @@ unsafe fn exec_vm(vmcfg: &VmConfig, rootfs: &str, cmd: Option<&str>, args: Vec = Vec::new(); + let mut ps: Vec<*const c_char> = Vec::new(); for port in ports.iter() { ps.push(port.as_ptr()); } ps.push(std::ptr::null()); - let ret = bindings::krun_set_port_map(ctx, ps.as_ptr()); - if ret < 0 { - println!("Error setting VM port map"); - std::process::exit(-1); + + match vmcfg.network_mode { + NetworkMode::Tsi => { + let ret = bindings::krun_set_port_map(ctx, ps.as_ptr()); + if ret < 0 { + println!("Error setting VM port map"); + std::process::exit(-1); + } + } + NetworkMode::Passt => { + let Ok(passt_fd) = start_passt(&vmcfg.mapped_ports) else { + std::process::exit(-1); + }; + let ret = krun_set_passt_fd(ctx, passt_fd.into_raw_fd() as c_int); + if ret < 0 { + let errno = Errno::from_i32(-ret); + if errno == Errno::ENOTSUP { + println!("Failed to set passt fd: your libkrun build does not support virtio-net/passt mode."); + } else { + println!("Failed to set passt fd: {}", errno); + } + std::process::exit(-1); + } + } } if !vmcfg.workdir.is_empty() { @@ -113,10 +242,10 @@ unsafe fn exec_vm(vmcfg: &VmConfig, rootfs: &str, cmd: Option<&str>, args: Vec = Vec::new(); + let mut argv: Vec<*const c_char> = Vec::new(); for a in args.iter() { argv.push(a.as_ptr()); } @@ -173,36 +302,3 @@ fn set_lock(rootfs: &str) -> File { file } - -pub fn start(cfg: &KrunvmConfig, matches: &ArgMatches) { - let cmd = matches.value_of("COMMAND"); - let name = matches.value_of("NAME").unwrap(); - - let vmcfg = match cfg.vmconfig_map.get(name) { - None => { - println!("No VM found with name {}", name); - std::process::exit(-1); - } - Some(vmcfg) => vmcfg, - }; - - umount_container(cfg, vmcfg).expect("Error unmounting container"); - let rootfs = mount_container(cfg, vmcfg).expect("Error mounting container"); - - let args: Vec = if cmd.is_some() { - match matches.values_of("ARGS") { - Some(a) => a.map(|val| CString::new(val).unwrap()).collect(), - None => Vec::new(), - } - } else { - Vec::new() - }; - - set_rlimits(); - - let _file = set_lock(&rootfs); - - unsafe { exec_vm(vmcfg, &rootfs, cmd, args) }; - - umount_container(cfg, vmcfg).expect("Error unmounting container"); -} diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index 1852f28..0000000 --- a/src/config.rs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2021 Red Hat, Inc. -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ArgMatches, KrunvmConfig, APP_NAME}; - -pub fn config(cfg: &mut KrunvmConfig, matches: &ArgMatches) { - let mut cfg_changed = false; - - if let Some(cpus_str) = matches.value_of("cpus") { - match cpus_str.parse::() { - Err(_) => println!("Invalid value for \"cpus\""), - Ok(cpus) => { - if cpus > 8 { - println!("Error: the maximum number of CPUs supported is 8"); - } else { - cfg.default_cpus = cpus; - cfg_changed = true; - } - } - } - } - - if let Some(mem_str) = matches.value_of("mem") { - match mem_str.parse::() { - Err(_) => println!("Invalid value for \"mem\""), - Ok(mem) => { - if mem > 16384 { - println!("Error: the maximum amount of RAM supported is 16384 MiB"); - } else { - cfg.default_mem = mem; - cfg_changed = true; - } - } - } - } - - if let Some(dns) = matches.value_of("dns") { - cfg.default_dns = dns.to_string(); - cfg_changed = true; - } - - if cfg_changed { - confy::store(APP_NAME, &cfg).unwrap(); - } - - println!("Global configuration:"); - println!( - "Default number of CPUs for newly created VMs: {}", - cfg.default_cpus - ); - println!( - "Default amount of RAM (MiB) for newly created VMs: {}", - cfg.default_mem - ); - println!( - "Default DNS server for newly created VMs: {}", - cfg.default_dns - ); -} diff --git a/src/config/migrate.rs b/src/config/migrate.rs new file mode 100644 index 0000000..f3d8d8b --- /dev/null +++ b/src/config/migrate.rs @@ -0,0 +1,138 @@ +use crate::config::{v1, v2}; +use confy::ConfyError; + +pub fn migrate_and_load_impl( + load_v2: impl FnOnce() -> Result, + load_v1: impl FnOnce() -> Result, + save_v2: impl FnOnce(&v2::KrunvmConfig) -> Result<(), ()>, +) -> Result { + fn check_version(got: u8, expected: u8) -> Result<(), ()> { + if expected != got { + eprintln!( + "Invalid config version number {} expected {}", + got, expected + ); + Err(()) + } else { + Ok(()) + } + } + + let v2_load_err = match load_v2() { + Ok(conf) => { + check_version(conf.version, 2)?; + return Ok(conf); + } + Err(e) => e, + }; + + let v1_load_err = match load_v1() { + Ok(cfg) => { + check_version(cfg.version, 1)?; + let v2_config = cfg.into(); + save_v2(&v2_config)?; + return Ok(v2_config); + } + Err(e) => e, + }; + + eprintln!("Failed to load config: "); + eprintln!("Tried to load as as v2 config, got error: {v2_load_err}"); + eprintln!("Tried to load as as v1 config, got error: {v1_load_err}"); + Err(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::NetworkMode; + use std::collections::HashMap; + + #[test] + fn load_without_migrate() { + let cfg = v2::KrunvmConfig { + default_dns: "8.8.8.8".into(), + ..v2::KrunvmConfig::default() + }; + + let returned_cfg = migrate_and_load_impl( + || Ok(cfg.clone()), + || panic!("Loading v1 should not be attempted"), + |_| panic!("Migration should not occur"), + ) + .unwrap(); + assert_eq!(returned_cfg, cfg); + } + + #[test] + fn load_migrating_to_v2() { + let v1_vms = [( + "fedora".to_string(), + v1::VmConfig { + name: "fedora".to_string(), + mapped_ports: Default::default(), + cpus: 2, + dns: "1.1.1.1".to_string(), + mapped_volumes: Default::default(), + workdir: "/".to_string(), + container: "fedora".to_string(), + mem: 8192, + }, + )]; + + let v1_cfg = v1::KrunvmConfig { + default_dns: "8.8.8.8".into(), + vmconfig_map: HashMap::from(v1_vms), + ..v1::KrunvmConfig::default() + }; + + let result_v2_vms = [( + "fedora".to_string(), + v2::VmConfig { + name: "fedora".to_string(), + mapped_ports: Default::default(), + cpus: 2, + dns: "1.1.1.1".to_string(), + mapped_volumes: Default::default(), + workdir: "/".to_string(), + container: "fedora".to_string(), + mem: 8192, + network_mode: NetworkMode::Tsi, + }, + )]; + + let result_v2_cfg = v2::KrunvmConfig { + default_dns: "8.8.8.8".into(), + vmconfig_map: HashMap::from(result_v2_vms), + default_network_mode: NetworkMode::Tsi, + ..v2::KrunvmConfig::default() + }; + + let mut load_v2_called = false; + let mut load_v1_called = false; + let mut save_called = false; + + let returned_cfg = migrate_and_load_impl( + || { + load_v2_called = true; + Err(ConfyError::BadConfigDirectoryStr) + }, + || { + load_v1_called = true; + Ok(v1_cfg) + }, + |migrated| { + save_called = true; + assert_eq!(migrated, &result_v2_cfg); + Ok(()) + }, + ) + .unwrap(); + + assert!(load_v2_called, "Load v2 must be called"); + assert!(load_v1_called, "Load v1 must be called"); + assert!(save_called, "Save must be called"); + + assert_eq!(returned_cfg, result_v2_cfg); + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..f50873c --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,20 @@ +mod migrate; +mod v1; +mod v2; + +use crate::APP_NAME; + +use crate::config::migrate::migrate_and_load_impl; +pub use v2::{KrunvmConfig, NetworkMode, VmConfig}; + +pub fn save(cfg: &KrunvmConfig) -> Result<(), ()> { + confy::store(APP_NAME, cfg).map_err(|e| eprintln!("Failed to load config: {e}")) +} + +pub fn load() -> Result { + migrate_and_load_impl( + || confy::load::(APP_NAME), + || confy::load::(APP_NAME), + save, + ) +} diff --git a/src/config/v1.rs b/src/config/v1.rs new file mode 100644 index 0000000..877f034 --- /dev/null +++ b/src/config/v1.rs @@ -0,0 +1,37 @@ +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Serialize, Deserialize)] +pub struct KrunvmConfig { + pub version: u8, + pub default_cpus: u32, + pub default_mem: u32, + pub default_dns: String, + pub storage_volume: String, + pub vmconfig_map: HashMap, +} + +impl Default for KrunvmConfig { + fn default() -> KrunvmConfig { + KrunvmConfig { + version: 1, + default_cpus: 2, + default_mem: 1024, + default_dns: "1.1.1.1".to_string(), + storage_volume: String::new(), + vmconfig_map: HashMap::new(), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct VmConfig { + pub name: String, + pub cpus: u32, + pub mem: u32, + pub container: String, + pub workdir: String, + pub dns: String, + pub mapped_volumes: HashMap, + pub mapped_ports: HashMap, +} diff --git a/src/config/v2.rs b/src/config/v2.rs new file mode 100644 index 0000000..c48e3d7 --- /dev/null +++ b/src/config/v2.rs @@ -0,0 +1,98 @@ +use crate::config::v1; + +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::str::FromStr; + +#[derive(Clone, Serialize, Deserialize, Debug, Default, Eq, PartialEq)] +pub enum NetworkMode { + #[default] + Tsi, + Passt, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct KrunvmConfig { + pub version: u8, + pub default_cpus: u32, + pub default_mem: u32, + pub default_dns: String, + pub default_network_mode: NetworkMode, + pub storage_volume: String, + pub vmconfig_map: HashMap, +} + +impl Default for KrunvmConfig { + fn default() -> KrunvmConfig { + KrunvmConfig { + version: 2, + default_cpus: 2, + default_mem: 1024, + default_dns: "1.1.1.1".to_string(), + default_network_mode: NetworkMode::default(), + storage_volume: String::new(), + vmconfig_map: HashMap::new(), + } + } +} + +impl From for KrunvmConfig { + fn from(old: v1::KrunvmConfig) -> Self { + KrunvmConfig { + version: 2, + default_cpus: old.default_cpus, + default_mem: old.default_mem, + default_dns: old.default_dns, + default_network_mode: NetworkMode::default(), + storage_volume: old.storage_volume, + vmconfig_map: old + .vmconfig_map + .into_iter() + .map(|(key, value)| (key, value.into())) + .collect(), + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct VmConfig { + pub name: String, + pub cpus: u32, + pub mem: u32, + pub container: String, + pub workdir: String, + pub dns: String, + pub network_mode: NetworkMode, + pub mapped_volumes: HashMap, + pub mapped_ports: HashMap, +} + +impl From for VmConfig { + fn from(old: v1::VmConfig) -> Self { + VmConfig { + name: old.name, + cpus: old.cpus, + mem: old.mem, + container: old.container, + workdir: old.workdir, + dns: old.dns, + mapped_volumes: old.mapped_volumes, + mapped_ports: old.mapped_ports, + network_mode: NetworkMode::default(), + } + } +} + +impl FromStr for NetworkMode { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + if s.eq_ignore_ascii_case("tsi") { + Ok(NetworkMode::Tsi) + } else if s.eq_ignore_ascii_case("passt") { + Ok(NetworkMode::Passt) + } else { + Err("Invalid network mode") + } + } +} diff --git a/src/create.rs b/src/create.rs deleted file mode 100644 index 3b88f7e..0000000 --- a/src/create.rs +++ /dev/null @@ -1,220 +0,0 @@ -// Copyright 2021 Red Hat, Inc. -// SPDX-License-Identifier: Apache-2.0 - -use std::fs; -use std::io::Write; -#[cfg(target_os = "macos")] -use std::path::Path; -use std::process::Command; - -use super::utils::{ - get_buildah_args, mount_container, parse_mapped_ports, parse_mapped_volumes, umount_container, - BuildahCommand, -}; -use crate::{ArgMatches, KrunvmConfig, VmConfig, APP_NAME}; - -#[cfg(target_os = "macos")] -const KRUNVM_ROSETTA_FILE: &str = ".krunvm-rosetta"; - -fn fix_resolv_conf(rootfs: &str, dns: &str) -> Result<(), std::io::Error> { - let resolvconf_dir = format!("{}/etc/", rootfs); - fs::create_dir_all(resolvconf_dir)?; - let resolvconf = format!("{}/etc/resolv.conf", rootfs); - let mut file = fs::File::create(resolvconf)?; - file.write_all(b"options use-vc\nnameserver ")?; - file.write_all(dns.as_bytes())?; - file.write_all(b"\n")?; - Ok(()) -} - -fn export_container_config( - cfg: &KrunvmConfig, - rootfs: &str, - image: &str, -) -> Result<(), std::io::Error> { - let mut args = get_buildah_args(cfg, BuildahCommand::Inspect); - args.push(image.to_string()); - - let output = match Command::new("buildah") - .args(&args) - .stderr(std::process::Stdio::inherit()) - .output() - { - Ok(output) => output, - Err(err) => { - if err.kind() == std::io::ErrorKind::NotFound { - println!("{} requires buildah to manage the OCI images, and it wasn't found on this system.", APP_NAME); - } else { - println!("Error executing buildah: {}", err); - } - std::process::exit(-1); - } - }; - - let exit_code = output.status.code().unwrap_or(-1); - if exit_code != 0 { - println!( - "buildah returned an error: {}", - std::str::from_utf8(&output.stdout).unwrap() - ); - std::process::exit(-1); - } - - let mut file = fs::File::create(format!("{}/.krun_config.json", rootfs))?; - file.write_all(&output.stdout)?; - - Ok(()) -} - -pub fn create(cfg: &mut KrunvmConfig, matches: &ArgMatches) { - let cpus = match matches.value_of("cpus") { - Some(c) => match c.parse::() { - Err(_) => { - println!("Invalid value for \"cpus\""); - std::process::exit(-1); - } - Ok(cpus) => cpus, - }, - None => cfg.default_cpus, - }; - let mem = match matches.value_of("mem") { - Some(m) => match m.parse::() { - Err(_) => { - println!("Invalid value for \"mem\""); - std::process::exit(-1); - } - Ok(mem) => mem, - }, - None => cfg.default_mem, - }; - let dns = match matches.value_of("dns") { - Some(d) => d, - None => &cfg.default_dns, - }; - - let workdir = matches.value_of("workdir").unwrap(); - - let volume_matches = if matches.is_present("volume") { - matches.values_of("volume").unwrap().collect() - } else { - vec![] - }; - let mapped_volumes = parse_mapped_volumes(volume_matches); - - let port_matches = if matches.is_present("port") { - matches.values_of("port").unwrap().collect() - } else { - vec![] - }; - let mapped_ports = parse_mapped_ports(port_matches); - - let image = matches.value_of("IMAGE").unwrap(); - - let name = matches.value_of("name"); - if let Some(name) = name { - if cfg.vmconfig_map.contains_key(name) { - println!("A VM with this name already exists"); - std::process::exit(-1); - } - } - - let mut args = get_buildah_args(cfg, BuildahCommand::From); - - #[cfg(target_os = "macos")] - let force_x86 = matches.is_present("x86"); - - #[cfg(target_os = "macos")] - if force_x86 { - let home = match std::env::var("HOME") { - Err(e) => { - println!("Error reading \"HOME\" enviroment variable: {}", e); - std::process::exit(-1); - } - Ok(home) => home, - }; - - let path = format!("{}/{}", home, KRUNVM_ROSETTA_FILE); - if !Path::new(&path).is_file() { - println!( - " -To use Rosetta for Linux you need to create the file... - -{} - -...with the contents that the \"rosetta\" binary expects to be served from -its specific ioctl. - -For more information, please refer to this post: -https://threedots.ovh/blog/2022/06/quick-look-at-rosetta-on-linux/ -", - path - ); - std::process::exit(-1); - } - - if cpus != 1 { - println!("x86 microVMs on Aarch64 are restricted to 1 CPU"); - cpus = 1; - } - args.push("--arch".to_string()); - args.push("x86_64".to_string()); - } - - args.push(image.to_string()); - - let output = match Command::new("buildah") - .args(&args) - .stderr(std::process::Stdio::inherit()) - .output() - { - Ok(output) => output, - Err(err) => { - if err.kind() == std::io::ErrorKind::NotFound { - println!("{} requires buildah to manage the OCI images, and it wasn't found on this system.", APP_NAME); - } else { - println!("Error executing buildah: {}", err); - } - std::process::exit(-1); - } - }; - - let exit_code = output.status.code().unwrap_or(-1); - if exit_code != 0 { - println!( - "buildah returned an error: {}", - std::str::from_utf8(&output.stdout).unwrap() - ); - std::process::exit(-1); - } - - let container = std::str::from_utf8(&output.stdout).unwrap().trim(); - let name = if let Some(name) = name { - name.to_string() - } else { - container.to_string() - }; - let vmcfg = VmConfig { - name: name.clone(), - cpus, - mem, - dns: dns.to_string(), - container: container.to_string(), - workdir: workdir.to_string(), - mapped_volumes, - mapped_ports, - }; - - let rootfs = mount_container(cfg, &vmcfg).unwrap(); - export_container_config(cfg, &rootfs, image).unwrap(); - fix_resolv_conf(&rootfs, dns).unwrap(); - #[cfg(target_os = "macos")] - if force_x86 { - _ = fs::create_dir(format!("{}/.rosetta", rootfs)); - } - umount_container(cfg, &vmcfg).unwrap(); - - cfg.vmconfig_map.insert(name.clone(), vmcfg); - confy::store(APP_NAME, cfg).unwrap(); - - println!("microVM created with name: {}", name); -} diff --git a/src/delete.rs b/src/delete.rs deleted file mode 100644 index 7a79932..0000000 --- a/src/delete.rs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2021 Red Hat, Inc. -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ArgMatches, KrunvmConfig, APP_NAME}; - -use super::utils::{remove_container, umount_container}; - -pub fn delete(cfg: &mut KrunvmConfig, matches: &ArgMatches) { - let name = matches.value_of("NAME").unwrap(); - - let vmcfg = match cfg.vmconfig_map.remove(name) { - None => { - println!("No VM found with that name"); - std::process::exit(-1); - } - Some(vmcfg) => vmcfg, - }; - - umount_container(cfg, &vmcfg).unwrap(); - remove_container(cfg, &vmcfg).unwrap(); - - confy::store(APP_NAME, &cfg).unwrap(); -} diff --git a/src/list.rs b/src/list.rs deleted file mode 100644 index 41d68d3..0000000 --- a/src/list.rs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 Red Hat, Inc. -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ArgMatches, KrunvmConfig, VmConfig}; - -pub fn printvm(vm: &VmConfig) { - println!("{}", vm.name); - println!(" CPUs: {}", vm.cpus); - println!(" RAM (MiB): {}", vm.mem); - println!(" DNS server: {}", vm.dns); - println!(" Buildah container: {}", vm.container); - println!(" Workdir: {}", vm.workdir); - println!(" Mapped volumes: {:?}", vm.mapped_volumes); - println!(" Mapped ports: {:?}", vm.mapped_ports); -} - -pub fn list(cfg: &KrunvmConfig, _matches: &ArgMatches) { - if cfg.vmconfig_map.is_empty() { - println!("No microVMs found"); - } else { - for (_name, vm) in cfg.vmconfig_map.iter() { - println!(); - printvm(vm); - } - println!(); - } -} diff --git a/src/main.rs b/src/main.rs index 4fa6a84..c35d619 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,64 +1,26 @@ // Copyright 2021 Red Hat, Inc. // SPDX-License-Identifier: Apache-2.0 -use std::collections::HashMap; #[cfg(target_os = "macos")] use std::fs::File; #[cfg(target_os = "macos")] use std::io::{self, Read, Write}; -use clap::{crate_version, App, Arg, ArgMatches}; -use serde_derive::{Deserialize, Serialize}; +use crate::commands::{ChangeVmCmd, ConfigCmd, CreateCmd, DeleteCmd, ListCmd, StartCmd}; +#[cfg(target_os = "macos")] +use crate::config::KrunvmConfig; +use clap::{Parser, Subcommand}; #[cfg(target_os = "macos")] use text_io::read; #[allow(unused)] mod bindings; -mod changevm; +mod commands; mod config; -mod create; -mod delete; -mod list; -mod start; mod utils; const APP_NAME: &str = "krunvm"; -#[derive(Default, Debug, Serialize, Deserialize)] -pub struct VmConfig { - name: String, - cpus: u32, - mem: u32, - container: String, - workdir: String, - dns: String, - mapped_volumes: HashMap, - mapped_ports: HashMap, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct KrunvmConfig { - version: u8, - default_cpus: u32, - default_mem: u32, - default_dns: String, - storage_volume: String, - vmconfig_map: HashMap, -} - -impl Default for KrunvmConfig { - fn default() -> KrunvmConfig { - KrunvmConfig { - version: 1, - default_cpus: 2, - default_mem: 1024, - default_dns: "1.1.1.1".to_string(), - storage_volume: String::new(), - vmconfig_map: HashMap::new(), - } - } -} - #[cfg(target_os = "macos")] fn check_case_sensitivity(volume: &str) -> Result { let first_path = format!("{}/krunvm_test", volume); @@ -149,236 +111,42 @@ fn check_unshare() { } } -fn main() { - let mut cfg: KrunvmConfig = confy::load(APP_NAME).unwrap(); - - let mut app = App::new("krunvm") - .version(crate_version!()) - .author("Sergio Lopez ") - .about("Manage microVMs created from OCI images") - .arg( - Arg::with_name("v") - .short("v") - .multiple(true) - .help("Sets the level of verbosity"), - ) - .subcommand( - App::new("changevm") - .about("Change the configuration of a microVM") - .arg( - Arg::with_name("cpus") - .long("cpus") - .help("Number of vCPUs") - .takes_value(true), - ) - .arg( - Arg::with_name("mem") - .long("mem") - .help("Amount of RAM in MiB") - .takes_value(true), - ) - .arg( - Arg::with_name("workdir") - .long("workdir") - .short("w") - .help("Working directory inside the microVM") - .takes_value(true), - ) - .arg( - Arg::with_name("remove-volumes") - .long("remove-volumes") - .help("Remove all volume mappings"), - ) - .arg( - Arg::with_name("volume") - .long("volume") - .short("v") - .help("Volume in form \"host_path:guest_path\" to be exposed to the guest") - .takes_value(true) - .multiple(true) - .number_of_values(1), - ) - .arg( - Arg::with_name("remove-ports") - .long("remove-ports") - .help("Remove all port mappings"), - ) - .arg( - Arg::with_name("port") - .long("port") - .short("p") - .help("Port in format \"host_port:guest_port\" to be exposed to the host") - .takes_value(true) - .multiple(true) - .number_of_values(1), - ) - .arg( - Arg::with_name("new-name") - .long("name") - .help("Assign a new name to the VM") - .takes_value(true), - ) - .arg( - Arg::with_name("NAME") - .help("Name of the VM to be modified") - .required(true), - ), - ) - .subcommand( - App::new("config") - .about("Configure global values") - .arg( - Arg::with_name("cpus") - .long("cpus") - .help("Default number of vCPUs for newly created VMs") - .takes_value(true), - ) - .arg( - Arg::with_name("mem") - .long("mem") - .help("Default amount of RAM in MiB for newly created VMs") - .takes_value(true), - ) - .arg( - Arg::with_name("dns") - .long("dns") - .help("DNS server to use in the microVM") - .takes_value(true), - ), - ) - .subcommand( - App::new("delete").about("Delete an existing microVM").arg( - Arg::with_name("NAME") - .help("Name of the microVM to be deleted") - .required(true) - .index(1), - ), - ) - .subcommand( - App::new("list").about("List microVMs").arg( - Arg::with_name("debug") - .short("d") - .help("print debug information verbosely"), - ), - ) - .subcommand( - App::new("start") - .about("Start an existing microVM") - .arg(Arg::with_name("cpus").long("cpus").help("Number of vCPUs")) - .arg( - Arg::with_name("mem") - .long("mem") - .help("Amount of RAM in MiB"), - ) - .arg( - Arg::with_name("NAME") - .help("Name of the microVM") - .required(true) - .index(1), - ) - .arg( - Arg::with_name("COMMAND") - .help("Command to run inside the VM") - .index(2), - ) - .arg( - Arg::with_name("ARGS") - .help("Arguments to be passed to the command executed in the VM") - .multiple(true) - .last(true), - ), - ); - - let mut create = App::new("create") - .about("Create a new microVM") - .arg( - Arg::with_name("cpus") - .long("cpus") - .help("Number of vCPUs") - .takes_value(true), - ) - .arg( - Arg::with_name("mem") - .long("mem") - .help("Amount of RAM in MiB") - .takes_value(true), - ) - .arg( - Arg::with_name("dns") - .long("dns") - .help("DNS server to use in the microVM") - .takes_value(true), - ) - .arg( - Arg::with_name("workdir") - .long("workdir") - .short("w") - .help("Working directory inside the microVM") - .takes_value(true) - .default_value(""), - ) - .arg( - Arg::with_name("volume") - .long("volume") - .short("v") - .help("Volume in form \"host_path:guest_path\" to be exposed to the guest") - .takes_value(true) - .multiple(true) - .number_of_values(1), - ) - .arg( - Arg::with_name("port") - .long("port") - .short("p") - .help("Port in format \"host_port:guest_port\" to be exposed to the host") - .takes_value(true) - .multiple(true) - .number_of_values(1), - ) - .arg( - Arg::with_name("name") - .long("name") - .help("Assign a name to the VM") - .takes_value(true), - ) - .arg( - Arg::with_name("IMAGE") - .help("OCI image to use as template") - .required(true), - ); - - if cfg!(target_os = "macos") { - create = create.arg( - Arg::with_name("x86") - .long("x86") - .short("x") - .help("Create a x86_64 microVM even on an Aarch64 host"), - ); - } +#[derive(Parser, Debug)] +#[command(author, version, about)] +struct Cli { + /// Sets the level of verbosity + #[arg(short)] + verbosity: Option, //TODO: implement or remove this + #[command(subcommand)] + command: Command, +} - app = app.subcommand(create); +#[derive(Subcommand, Debug)] +enum Command { + Start(StartCmd), + Create(CreateCmd), + List(ListCmd), + Delete(DeleteCmd), + #[command(name = "changevm")] + ChangeVm(ChangeVmCmd), + Config(ConfigCmd), +} - let matches = app.clone().get_matches(); +fn main() { + let mut cfg = config::load().unwrap(); + let cli_args = Cli::parse(); #[cfg(target_os = "macos")] check_volume(&mut cfg); #[cfg(target_os = "linux")] check_unshare(); - if let Some(matches) = matches.subcommand_matches("changevm") { - changevm::changevm(&mut cfg, matches); - } else if let Some(matches) = matches.subcommand_matches("config") { - config::config(&mut cfg, matches); - } else if let Some(matches) = matches.subcommand_matches("create") { - create::create(&mut cfg, matches); - } else if let Some(matches) = matches.subcommand_matches("delete") { - delete::delete(&mut cfg, matches); - } else if let Some(matches) = matches.subcommand_matches("list") { - list::list(&cfg, matches); - } else if let Some(matches) = matches.subcommand_matches("start") { - start::start(&cfg, matches); - } else { - app.print_long_help().unwrap(); - println!(); + match cli_args.command { + Command::Start(cmd) => cmd.run(&cfg), + Command::Create(cmd) => cmd.run(&mut cfg), + Command::List(cmd) => cmd.run(&cfg), + Command::Delete(cmd) => cmd.run(&mut cfg), + Command::ChangeVm(cmd) => cmd.run(&mut cfg), + Command::Config(cmd) => cmd.run(&mut cfg), } } diff --git a/src/utils.rs b/src/utils.rs index 46bd2e4..003f621 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,11 +1,13 @@ // Copyright 2021 Red Hat, Inc. // SPDX-License-Identifier: Apache-2.0 +use crate::APP_NAME; use std::collections::HashMap; use std::path::Path; use std::process::Command; +use std::str::FromStr; -use crate::{KrunvmConfig, VmConfig, APP_NAME}; +use crate::config::{KrunvmConfig, VmConfig}; pub enum BuildahCommand { From, @@ -71,70 +73,93 @@ pub fn get_buildah_args(cfg: &KrunvmConfig, cmd: BuildahCommand) -> Vec args } -pub fn parse_mapped_ports(port_matches: Vec<&str>) -> HashMap { - let mut mapped_ports = HashMap::new(); - for port in port_matches.iter() { - let vtuple: Vec<&str> = port.split(':').collect(); +#[derive(Debug, Clone)] +pub struct PortPair { + pub host_port: String, + pub guest_port: String, +} + +pub fn port_pairs_to_hash_map( + port_pairs: impl IntoIterator, +) -> HashMap { + port_pairs + .into_iter() + .map(|pair: PortPair| (pair.host_port, pair.guest_port)) + .collect() +} + +impl FromStr for PortPair { + type Err = &'static str; + + fn from_str(input: &str) -> Result { + let vtuple: Vec<&str> = input.split(':').collect(); if vtuple.len() != 2 { - println!("Invalid value for \"port\""); - std::process::exit(-1); + return Err("Too many ':' separators"); } let host_port: u16 = match vtuple[0].parse() { Ok(p) => p, Err(_) => { - println!("Invalid host port"); - std::process::exit(-1); + return Err("Invalid host port"); } }; let guest_port: u16 = match vtuple[1].parse() { Ok(p) => p, Err(_) => { - println!("Invalid guest port"); - std::process::exit(-1); + return Err("Invalid guest port"); } }; - - mapped_ports.insert(host_port.to_string(), guest_port.to_string()); + Ok(PortPair { + host_port: host_port.to_string(), + guest_port: guest_port.to_string(), + }) } +} + +#[derive(Debug, Clone)] +pub struct PathPair { + pub host_path: String, + pub guest_path: String, +} - mapped_ports +pub fn path_pairs_to_hash_map( + volume_pairs: impl IntoIterator, +) -> HashMap { + volume_pairs + .into_iter() + .map(|pair: PathPair| (pair.host_path, pair.guest_path)) + .collect() } -pub fn parse_mapped_volumes(volume_matches: Vec<&str>) -> HashMap { - let mut mapped_volumes = HashMap::new(); - for volume in volume_matches.iter() { - let vtuple: Vec<&str> = volume.split(':').collect(); +impl FromStr for PathPair { + type Err = &'static str; + + fn from_str(input: &str) -> Result { + let vtuple: Vec<&str> = input.split(':').collect(); if vtuple.len() != 2 { - println!("Invalid value for \"volume\""); - std::process::exit(-1); + return Err("Too many ':' separators"); } + let host_path = Path::new(vtuple[0]); if !host_path.is_absolute() { - println!("Invalid volume, host_path is not an absolute path"); - std::process::exit(-1); + return Err("Invalid volume, host_path is not an absolute path"); } if !host_path.exists() { - println!("Invalid volume, host_path does not exists"); - std::process::exit(-1); + return Err("Invalid volume, host_path does not exists"); } let guest_path = Path::new(vtuple[1]); if !guest_path.is_absolute() { - println!("Invalid volume, guest_path is not an absolute path"); - std::process::exit(-1); + return Err("Invalid volume, guest_path is not an absolute path"); } if guest_path.components().count() != 2 { - println!( - "Invalid volume, only single direct root children are supported as guest_path" + return Err( + "Invalid volume, only single direct root children are supported as guest_path", ); - std::process::exit(-1); } - mapped_volumes.insert( - host_path.to_str().unwrap().to_string(), - guest_path.to_str().unwrap().to_string(), - ); + Ok(Self { + host_path: vtuple[0].to_string(), + guest_path: vtuple[1].to_string(), + }) } - - mapped_volumes } #[cfg(target_os = "macos")] @@ -198,7 +223,7 @@ pub fn mount_container(cfg: &KrunvmConfig, vmcfg: &VmConfig) -> Result