diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e5ac39 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +Cargo.lock +/build/ +target +*.rs.bk +*.iml +.idea +__pycache__ +*.pyc +*~ diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0453567 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "krunvm" +version = "0.1.0" +authors = ["Sergio Lopez "] +edition = "2018" +build = "build.rs" + +[dependencies] +clap = "2.33.3" +confy = "0.4.0" +libc = "0.2.82" +serde = "1.0.120" +serde_derive = "1.0.120" +text_io = "0.1.8" \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..64942c7 --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +OS = $(shell uname -s) +KRUNVM_RELEASE = target/release/krunvm +KRUNVM_DEBUG = target/debug/krunvm +INIT_BINARY = init/init + +ifeq ($(PREFIX),) + PREFIX := /usr/local +endif + +.PHONY: install clean + +all: $(KRUNVM_RELEASE) + +debug: $(KRUNVM_DEBUG) + +$(KRUNVM_RELEASE): + cargo build --release +ifeq ($(OS),Darwin) + codesign --entitlements krunvm.entitlements --force -s - $@ +endif + +$(KRUNVM_DEBUG): + cargo build --debug + +install: $(KRUNVM_RELEASE) + install -d $(DESTDIR)$(PREFIX)/bin + install -m 755 $(KRUNVM_RELEASE) $(DESTDIR)$(PREFIX)/bin + +clean: + cargo clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..0e59d5c --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# krunvm + +```krunvm``` is a CLI-based utility for managing lightweight VMs created from OCI images, using [libkrun](https://github.com/containers/libkrun) and [buildah](https://github.com/containers/buildah). diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..4bad54b --- /dev/null +++ b/build.rs @@ -0,0 +1,4 @@ +fn main() { + #[cfg(target_os = "macos")] + println!("cargo:rustc-link-search=/opt/homebrew/lib"); +} diff --git a/krunvm.entitlements b/krunvm.entitlements new file mode 100644 index 0000000..a967593 --- /dev/null +++ b/krunvm.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.hypervisor + + com.apple.security.cs.disable-library-validationr + + + diff --git a/src/bindings.rs b/src/bindings.rs new file mode 100644 index 0000000..60bc1f1 --- /dev/null +++ b/src/bindings.rs @@ -0,0 +1,21 @@ +// Copyright 2021 Red Hat, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[link(name = "krun")] +extern "C" { + pub fn krun_set_log_level(level: u32) -> i32; + pub fn krun_create_ctx() -> i32; + pub fn krun_free_ctx(ctx: u32) -> i32; + pub fn krun_set_vm_config(ctx: u32, num_vcpus: u8, ram_mib: u32) -> i32; + pub fn krun_set_root(ctx: u32, root_path: *const i8) -> i32; + pub fn krun_set_mapped_volumes(ctx: u32, mapped_volumes: *const *const i8) -> i32; + pub fn krun_set_port_map(ctx: u32, port_map: *const *const i8) -> i32; + pub fn krun_set_workdir(ctx: u32, workdir_path: *const i8) -> i32; + pub fn krun_set_exec( + ctx: u32, + exec_path: *const i8, + argv: *const *const i8, + envp: *const *const i8, + ) -> i32; + pub fn krun_start_enter(ctx: u32) -> i32; +} diff --git a/src/changevm.rs b/src/changevm.rs new file mode 100644 index 0000000..617c646 --- /dev/null +++ b/src/changevm.rs @@ -0,0 +1,119 @@ +// 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 mut 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/config.rs b/src/config.rs new file mode 100644 index 0000000..1852f28 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,59 @@ +// 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/create.rs b/src/create.rs new file mode 100644 index 0000000..7bccac0 --- /dev/null +++ b/src/create.rs @@ -0,0 +1,141 @@ +// Copyright 2021 Red Hat, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::fs::File; +use std::io::Write; +use std::process::Command; + +use super::utils::{mount_container, parse_mapped_ports, parse_mapped_volumes, umount_container}; +use crate::{ArgMatches, KrunvmConfig, VmConfig, APP_NAME}; + +fn fix_resolv_conf(rootfs: &str, dns: &str) -> Result<(), std::io::Error> { + let resolvconf = format!("{}/etc/resolv.conf", rootfs); + let mut file = File::create(resolvconf)?; + file.write_all(b"options use-vc\nnameserver ")?; + file.write_all(dns.as_bytes())?; + file.write_all(b"\n")?; + 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); + } + } + + #[cfg(target_os = "linux")] + let mut args = vec!["from"]; + #[cfg(target_os = "macos")] + let storage_root = format!("{}/root", cfg.storage_volume); + #[cfg(target_os = "macos")] + let storage_runroot = format!("{}/runroot", cfg.storage_volume); + #[cfg(target_os = "macos")] + let mut args = vec![ + "--root", + &storage_root, + "--runroot", + &storage_runroot, + "from", + "--os", + "linux", + ]; + + args.push(image); + + 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.to_string()); + } + 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(); + fix_resolv_conf(&rootfs, &dns).unwrap(); + umount_container(&cfg, &vmcfg).unwrap(); + + cfg.vmconfig_map.insert(name.clone(), vmcfg); + confy::store(APP_NAME, cfg).unwrap(); + + println!("Lightweight VM created with name: {}", name); +} diff --git a/src/delete.rs b/src/delete.rs new file mode 100644 index 0000000..598f1ad --- /dev/null +++ b/src/delete.rs @@ -0,0 +1,23 @@ +// 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 new file mode 100644 index 0000000..6c12d86 --- /dev/null +++ b/src/list.rs @@ -0,0 +1,27 @@ +// 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 lightweight VMs found"); + } else { + for (_name, vm) in cfg.vmconfig_map.iter() { + println!(); + printvm(vm); + } + println!(); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c0f1a2c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,351 @@ +// Copyright 2021 Red Hat, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use std::fs::File; +use std::io::{self, Read, Write}; + +use clap::{crate_version, App, Arg, ArgMatches}; +use serde_derive::{Deserialize, Serialize}; +use text_io::read; + +#[allow(unused)] +mod bindings; +mod changevm; +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(), + } + } +} + +fn check_case_sensitivity(volume: &str) -> Result { + let first_path = format!("{}/krunvm_test", volume); + let second_path = format!("{}/krunVM_test", volume); + { + let mut first = File::create(&first_path)?; + first.write_all(b"first")?; + } + { + let mut second = File::create(&second_path)?; + second.write_all(b"second")?; + } + let mut data = String::new(); + { + let mut test = File::open(&first_path)?; + + test.read_to_string(&mut data)?; + } + if data == "first" { + let _ = std::fs::remove_file(first_path); + let _ = std::fs::remove_file(second_path); + Ok(true) + } else { + let _ = std::fs::remove_file(first_path); + Ok(false) + } +} + +fn check_volume(cfg: &mut KrunvmConfig) { + if !cfg.storage_volume.is_empty() { + return; + } + + println!( + " +On macOS, krunvm requires a dedicated, case-sensitive volume. +You can easily such volume by executing something like this on +another terminal: + +diskutil apfs addVolume disk3 \"Case-sensitive APFS\" krunvm + +NOTE: APFS volume creation is a non-destructive action that +doesn't require a dedicated disk nor \"sudo\" privileges. The +new volume will share the disk space with the main container +volume. +" + ); + loop { + print!("Please enter the mountpoint for this volume [/Volumes/krunvm]: "); + io::stdout().flush().unwrap(); + let answer: String = read!("{}\n"); + + let volume = if answer.is_empty() { + "/Volumes/krunvm".to_string() + } else { + answer.to_string() + }; + + print!("Checking volume... "); + match check_case_sensitivity(&volume) { + Ok(res) => { + if res { + println!("success."); + println!("The volume has been configured. Please execute krunvm again"); + cfg.storage_volume = volume; + confy::store(APP_NAME, cfg).unwrap(); + std::process::exit(-1); + } else { + println!("failed."); + println!("This volume failed the case sensitivity test."); + } + } + Err(err) => { + println!("error."); + println!("There was an error running the test: {}", err); + } + } + } +} + +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 lightweight VMs 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 lightweight VM") + .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("workdir") + .long("workdir") + .short("w") + .help("Working directory inside the lightweight VM") + .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), + ) + .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), + ) + .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 lightweight VM") + .takes_value(true), + ), + ) + .subcommand( + App::new("create") + .about("Create a new lightweight VM") + .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 lightweight VM") + .takes_value(true), + ) + .arg( + Arg::with_name("workdir") + .long("workdir") + .short("w") + .help("Working directory inside the lightweight VM") + .takes_value(true) + .default_value("/root"), + ) + .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), + ) + .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), + ) + .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), + ), + ) + .subcommand( + App::new("delete") + .about("Delete an existing lightweight VM") + .arg( + Arg::with_name("NAME") + .help("Name of the lightweight VM to be deleted") + .required(true) + .index(1), + ), + ) + .subcommand( + App::new("list").about("List lightweight VMs").arg( + Arg::with_name("debug") + .short("d") + .help("print debug information verbosely"), + ), + ) + .subcommand( + App::new("start") + .about("Start an existing lightweight VM") + .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 lightweight VM") + .required(true) + .index(1), + ) + .arg( + Arg::with_name("COMMAND") + .help("Command to run inside the VM") + .index(2) + .default_value("/bin/sh"), + ) + .arg( + Arg::with_name("ARGS") + .help("Arguments to be passed to the command executed in the VM") + .multiple(true) + .last(true), + ), + ); + + let matches = app.clone().get_matches(); + + #[cfg(target_os = "macos")] + check_volume(&mut cfg); + + if let Some(ref matches) = matches.subcommand_matches("changevm") { + changevm::changevm(&mut cfg, matches); + } else if let Some(ref matches) = matches.subcommand_matches("config") { + config::config(&mut cfg, matches); + } else if let Some(ref matches) = matches.subcommand_matches("create") { + create::create(&mut cfg, matches); + } else if let Some(ref matches) = matches.subcommand_matches("delete") { + delete::delete(&mut cfg, matches); + } else if let Some(ref matches) = matches.subcommand_matches("list") { + list::list(&cfg, matches); + } else if let Some(ref matches) = matches.subcommand_matches("start") { + start::start(&cfg, matches); + } else { + app.print_long_help().unwrap(); + println!(); + } +} diff --git a/src/start.rs b/src/start.rs new file mode 100644 index 0000000..07d690a --- /dev/null +++ b/src/start.rs @@ -0,0 +1,164 @@ +// Copyright 2021 Red Hat, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::ffi::CString; +use std::fs::File; +use std::os::unix::io::AsRawFd; +use std::path::Path; + +use super::bindings; +use super::utils::{mount_container, umount_container}; +use crate::{ArgMatches, KrunvmConfig, VmConfig}; + +unsafe fn exec_vm(vmcfg: &VmConfig, rootfs: &str, cmd: &str, args: Vec) { + //bindings::krun_set_log_level(9); + + let ctx = bindings::krun_create_ctx() as u32; + + let ret = bindings::krun_set_vm_config(ctx, vmcfg.cpus as u8, vmcfg.mem); + if ret < 0 { + println!("Error setting VM config"); + std::process::exit(-1); + } + + let c_rootfs = CString::new(rootfs).unwrap(); + let ret = bindings::krun_set_root(ctx, c_rootfs.as_ptr()); + if ret < 0 { + println!("Error setting VM rootfs"); + std::process::exit(-1); + } + + let mut volumes = Vec::new(); + for (host_path, guest_path) in vmcfg.mapped_volumes.iter() { + let full_guest = format!("{}{}", &rootfs, guest_path); + let full_guest_path = Path::new(&full_guest); + if !full_guest_path.exists() { + std::fs::create_dir(full_guest_path) + .expect("Couldn't create guest_path for mapped volume"); + } + let map = format!("{}:{}", host_path, guest_path); + volumes.push(CString::new(map).unwrap()); + } + let mut vols: Vec<*const i8> = Vec::new(); + for vol in volumes.iter() { + vols.push(vol.as_ptr()); + } + vols.push(std::ptr::null()); + let ret = bindings::krun_set_mapped_volumes(ctx, vols.as_ptr()); + if ret < 0 { + println!("Error setting VM mapped volumes"); + std::process::exit(-1); + } + + let mut ports = Vec::new(); + for (host_port, guest_port) in vmcfg.mapped_ports.iter() { + let map = format!("{}:{}", host_port, guest_port); + ports.push(CString::new(map).unwrap()); + } + let mut ps: Vec<*const i8> = 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); + } + + let c_workdir = CString::new(vmcfg.workdir.clone()).unwrap(); + let ret = bindings::krun_set_workdir(ctx, c_workdir.as_ptr()); + if ret < 0 { + println!("Error setting VM workdir"); + std::process::exit(-1); + } + + let mut argv: Vec<*const i8> = Vec::new(); + for a in args.iter() { + argv.push(a.as_ptr()); + } + argv.push(std::ptr::null()); + + let hostname = CString::new(format!("HOSTNAME={}", vmcfg.name)).unwrap(); + let home = CString::new("HOME=/root").unwrap(); + let path = CString::new("PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin").unwrap(); + let env: [*const i8; 4] = [ + hostname.as_ptr(), + home.as_ptr(), + path.as_ptr(), + std::ptr::null(), + ]; + + let c_cmd = CString::new(cmd).unwrap(); + let ret = bindings::krun_set_exec(ctx, c_cmd.as_ptr(), argv.as_ptr(), env.as_ptr()); + if ret < 0 { + println!("Error setting VM config"); + std::process::exit(-1); + } + + let ret = bindings::krun_start_enter(ctx); + if ret < 0 { + println!("Error starting VM"); + std::process::exit(-1); + } +} + +fn set_rlimits() { + let mut limit = libc::rlimit { + rlim_cur: 0, + rlim_max: 0, + }; + + let ret = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) }; + if ret < 0 { + panic!("Couldn't get RLIMIT_NOFILE value"); + } + + limit.rlim_cur = limit.rlim_max; + let ret = unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &limit) }; + if ret < 0 { + panic!("Couldn't set RLIMIT_NOFILE value"); + } +} + +fn set_lock(rootfs: &str) -> File { + let lock_path = format!("{}/.krunvm.lock", rootfs); + let file = File::create(lock_path).expect("Couldn't create lock file"); + + let ret = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) }; + if ret < 0 { + println!("Couldn't acquire lock file. Is another instance of this VM already running?"); + std::process::exit(-1); + } + + file +} + +pub fn start(cfg: &KrunvmConfig, matches: &ArgMatches) { + let cmd = matches.value_of("COMMAND").unwrap(); + 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 = match matches.values_of("ARGS") { + Some(a) => a.map(|val| CString::new(val).unwrap()).collect(), + None => 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/utils.rs b/src/utils.rs new file mode 100644 index 0000000..09680e7 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,207 @@ +// Copyright 2021 Red Hat, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use std::path::Path; +use std::process::Command; + +use crate::{KrunvmConfig, VmConfig, APP_NAME}; + +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(); + if vtuple.len() != 2 { + println!("Invalid value for \"port\""); + std::process::exit(-1); + } + let host_port: u16 = match vtuple[0].parse() { + Ok(p) => p, + Err(_) => { + println!("Invalid host port"); + std::process::exit(-1); + } + }; + let guest_port: u16 = match vtuple[1].parse() { + Ok(p) => p, + Err(_) => { + println!("Invalid guest port"); + std::process::exit(-1); + } + }; + + mapped_ports.insert(host_port.to_string(), guest_port.to_string()); + } + + mapped_ports +} + +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(); + if vtuple.len() != 2 { + println!("Invalid value for \"volume\""); + std::process::exit(-1); + } + 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); + } + if !host_path.exists() { + println!("Invalid volume, host_path does not exists"); + std::process::exit(-1); + } + 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); + } + if guest_path.components().count() != 2 { + println!( + "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(), + ); + } + + mapped_volumes +} + +pub fn mount_container(cfg: &KrunvmConfig, vmcfg: &VmConfig) -> Result { + #[cfg(target_os = "macos")] + let storage_root = format!("{}/root", cfg.storage_volume); + #[cfg(target_os = "macos")] + let storage_runroot = format!("{}/runroot", cfg.storage_volume); + #[cfg(target_os = "macos")] + let mut args = vec![ + "--root", + &storage_root, + "--runroot", + &storage_runroot, + "mount", + ]; + #[cfg(target_os = "linux")] + let mut args = vec!["mount"]; + + args.push(&vmcfg.container); + + 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.to_string()); + } + 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 rootfs = std::str::from_utf8(&output.stdout).unwrap().trim(); + Ok(rootfs.to_string()) +} + +pub fn umount_container(cfg: &KrunvmConfig, vmcfg: &VmConfig) -> Result<(), std::io::Error> { + #[cfg(target_os = "macos")] + let storage_root = format!("{}/root", cfg.storage_volume); + #[cfg(target_os = "macos")] + let storage_runroot = format!("{}/runroot", cfg.storage_volume); + #[cfg(target_os = "macos")] + let mut args = vec![ + "--root", + &storage_root, + "--runroot", + &storage_runroot, + "umount", + ]; + #[cfg(target_os = "linux")] + let mut args = vec!["umount"]; + + args.push(&vmcfg.container); + + 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.to_string()); + } + 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); + } + + Ok(()) +} + +pub fn remove_container(cfg: &KrunvmConfig, vmcfg: &VmConfig) -> Result<(), std::io::Error> { + #[cfg(target_os = "macos")] + let storage_root = format!("{}/root", cfg.storage_volume); + #[cfg(target_os = "macos")] + let storage_runroot = format!("{}/runroot", cfg.storage_volume); + #[cfg(target_os = "macos")] + let mut args = vec!["--root", &storage_root, "--runroot", &storage_runroot, "rm"]; + #[cfg(target_os = "linux")] + let mut args = vec!["rm"]; + + args.push(&vmcfg.container); + + 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.to_string()); + } + 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); + } + + Ok(()) +}