krunvm 0.1.6

Create microVMs from OCI images
// 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};
#[cfg(target_os = "macos")]
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<String, String>,
    mapped_ports: HashMap<String, String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct KrunvmConfig {
    version: u8,
    default_cpus: u32,
    default_mem: u32,
    default_dns: String,
    storage_volume: String,
    vmconfig_map: HashMap<String, VmConfig>,
}

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<bool, io::Error> {
    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)
    }
}

#[cfg(target_os = "macos")]
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 create 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);
            }
        }
    }
}

#[cfg(target_os = "linux")]
fn check_unshare() {
    let uid = unsafe { libc::getuid() };
    if uid != 0 && !std::env::vars().any(|(key, _)| key == "BUILDAH_ISOLATION") {
        println!("Please re-run krunvm inside a \"buildah unshare\" session");
        std::process::exit(-1);
    }
}

fn main() {
    let mut cfg: KrunvmConfig = confy::load(APP_NAME).unwrap();

    let mut app = App::new("krunvm")
        .version(crate_version!())
        .author("Sergio Lopez <slp@redhat.com>")
        .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),
                )
                .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 microVM")
                        .takes_value(true),
                ),
        )
        .subcommand(
            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("/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 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)
                        .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);
    #[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!();
    }
}