// Copyright (c) 2020 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 // use crate::types::{Config, Options}; use anyhow::{anyhow, Result}; use oci::{Process as ociProcess, Root as ociRoot, Spec as ociSpec}; use protocols::oci::{ Box as grpcBox, Linux as grpcLinux, LinuxCapabilities as grpcLinuxCapabilities, POSIXRlimit as grpcPOSIXRlimit, Process as grpcProcess, Root as grpcRoot, Spec as grpcSpec, User as grpcUser, }; use rand::Rng; use slog::{debug, warn}; use std::collections::HashMap; use std::path::PathBuf; use std::sync::{Arc, Mutex}; // Length of a sandbox identifier const SANDBOX_ID_LEN: u8 = 64; const FILE_URI: &str = "file://"; // Length of the guests hostname const MIN_HOSTNAME_LEN: u8 = 8; // Name of the OCI configuration file found at the root of an OCI bundle. const CONFIG_FILE: &str = "config.json"; lazy_static! { // Create a mutable hash map statically static ref SIGNALS: Arc>> = { let mut m: HashMap<&'static str, u8> = HashMap::new(); m.insert("SIGHUP", 1); m.insert("SIGINT", 2); m.insert("SIGQUIT", 3); m.insert("SIGILL", 4); m.insert("SIGTRAP", 5); m.insert("SIGABRT", 6); m.insert("SIGBUS", 7); m.insert("SIGFPE", 8); m.insert("SIGKILL", 9); m.insert("SIGUSR1", 10); m.insert("SIGSEGV", 11); m.insert("SIGUSR2", 12); m.insert("SIGPIPE", 13); m.insert("SIGALRM", 14); m.insert("SIGTERM", 15); m.insert("SIGSTKFLT", 16); // XXX: m.insert("SIGCHLD", 17); m.insert("SIGCLD", 17); m.insert("SIGCONT", 18); m.insert("SIGSTOP", 19); m.insert("SIGTSTP", 20); m.insert("SIGTTIN", 21); m.insert("SIGTTOU", 22); m.insert("SIGURG", 23); m.insert("SIGXCPU", 24); m.insert("SIGXFSZ", 25); m.insert("SIGVTALRM", 26); m.insert("SIGPROF", 27); m.insert("SIGWINCH", 28); m.insert("SIGIO", 29); m.insert("SIGPWR", 30); m.insert("SIGSYS", 31); Arc::new(Mutex::new(m)) }; } pub fn signame_to_signum(name: &str) -> Result { if name == "" { return Err(anyhow!("invalid signal")); } match name.parse::() { Ok(n) => return Ok(n), // "fall through" on error as we assume the name is not a number, but // a signal name. Err(_) => (), } let mut search_term: String; if name.starts_with("SIG") { search_term = name.to_string(); } else { search_term = format!("SIG{}", name); } search_term = search_term.to_uppercase(); // Access the hashmap let signals_ref = SIGNALS.clone(); let m = signals_ref.lock().unwrap(); match m.get(&*search_term) { Some(value) => Ok(*value), None => Err(anyhow!(format!("invalid signal name: {:?}", name))), } } // Convert a human time fornat (like "2s") into the equivalent number // of nano seconds. pub fn human_time_to_ns(human_time: &str) -> Result { if human_time == "" || human_time == "0" { return Ok(0); } let d: humantime::Duration = human_time .parse::() .map_err(|e| anyhow!(e))? .into(); Ok(d.as_nanos() as i64) } // Look up the specified option name and return its value. // // - The function looks for the appropriate option value in the specified // 'args' first. // - 'args' is assumed to be a space-separated set of "name=value" pairs). // - If not found in the args, the function looks in the global options hash. // - If found in neither location, certain well-known options are auto-generated. // - All other options values default to an empty string. // - All options are saved in the global hash before being returned for future // use. pub fn get_option(name: &str, options: &mut Options, args: &str) -> String { let words: Vec<&str> = args.split_whitespace().collect(); for word in words { let fields: Vec = word.split("=").map(|s| s.to_string()).collect(); if fields.len() < 2 { continue; } if fields[0] == "" { continue; } let key = fields[0].clone(); let mut value = fields[1..].join("="); // Expand "spec=file:///some/where/config.json" if key == "spec" && value.starts_with(FILE_URI) { let spec_file = match uri_to_filename(&value) { Ok(file) => file, Err(e) => { warn!(sl!(), "failed to handle spec file URI: {:}", e); "".to_string() } }; if spec_file != "" { value = match spec_file_to_string(spec_file) { Ok(s) => s, Err(e) => { warn!(sl!(), "failed to load spec file: {:}", e); "".to_string() } }; } } // Command args take priority over any previous value, // so update the global set of options for this and all // subsequent commands. options.insert(key, value); } // Explains briefly how the option value was determined let mut msg = "cached"; // If the option exists in the hash, return it if let Some(value) = options.get(name) { debug!(sl!(), "using option {:?}={:?} ({})", name, value, msg); return value.to_string(); } msg = "generated"; // Handle option values that can be auto-generated let value = match name { "cid" => random_container_id(), "sid" => random_sandbox_id(), // Default to CID "exec_id" => { msg = "derived"; //derived = true; match options.get("cid") { Some(value) => value.to_string(), None => "".to_string(), } } _ => "".to_string(), }; debug!(sl!(), "using option {:?}={:?} ({})", name, value, msg); // Store auto-generated value options.insert(name.to_string(), value.to_string()); value } pub fn generate_random_hex_string(len: u32) -> String { const CHARSET: &[u8] = b"abcdef0123456789"; let mut rng = rand::thread_rng(); let str: String = (0..len) .map(|_| { let idx = rng.gen_range(0, CHARSET.len()); CHARSET[idx] as char }) .collect(); str } pub fn random_sandbox_id() -> String { generate_random_hex_string(SANDBOX_ID_LEN as u32) } pub fn random_container_id() -> String { // Containers and sandboxes have same ID types random_sandbox_id() } fn config_file_from_bundle_dir(bundle_dir: &str) -> Result { if bundle_dir == "" { return Err(anyhow!("missing bundle directory")); } let config_path = PathBuf::from(&bundle_dir).join(CONFIG_FILE); config_path .into_os_string() .into_string() .map_err(|e| anyhow!("{:?}", e).context("failed to construct config file path")) } fn root_oci_to_grpc(bundle_dir: &str, root: &ociRoot) -> Result { let root_dir = root.path.clone(); let path = if root_dir.starts_with("/") { root_dir.clone() } else { // Expand the root directory into an absolute value let abs_root_dir = PathBuf::from(&bundle_dir).join(&root_dir); abs_root_dir .into_os_string() .into_string() .map_err(|e| anyhow!("{:?}", e).context("failed to construct bundle path"))? }; let grpc_root = grpcRoot { Path: path, Readonly: root.readonly, unknown_fields: protobuf::UnknownFields::new(), cached_size: protobuf::CachedSize::default(), }; Ok(grpc_root) } fn process_oci_to_grpc(p: &ociProcess) -> grpcProcess { let console_size = match &p.console_size { Some(s) => { let mut b = grpcBox::new(); b.set_Width(s.width); b.set_Height(s.height); protobuf::SingularPtrField::some(b) } None => protobuf::SingularPtrField::none(), }; let oom_score_adj: i64 = match p.oom_score_adj { Some(s) => s.into(), None => 0, }; let mut user = grpcUser::new(); user.set_UID(p.user.uid); user.set_GID(p.user.gid); user.set_AdditionalGids(p.user.additional_gids.clone()); // FIXME: Implement RLimits OCI spec handling (copy from p.rlimits) //let rlimits = vec![grpcPOSIXRlimit::new()]; let rlimits = protobuf::RepeatedField::new(); // FIXME: Implement Capabilities OCI spec handling (copy from p.capabilities) let capabilities = grpcLinuxCapabilities::new(); // FIXME: Implement Env OCI spec handling (copy from p.env) let env = protobuf::RepeatedField::new(); grpcProcess { Terminal: p.terminal, ConsoleSize: console_size, User: protobuf::SingularPtrField::some(user), Args: protobuf::RepeatedField::from_vec(p.args.clone()), Env: env, Cwd: p.cwd.clone(), Capabilities: protobuf::SingularPtrField::some(capabilities), Rlimits: rlimits, NoNewPrivileges: p.no_new_privileges, ApparmorProfile: p.apparmor_profile.clone(), OOMScoreAdj: oom_score_adj, SelinuxLabel: p.selinux_label.clone(), unknown_fields: protobuf::UnknownFields::new(), cached_size: protobuf::CachedSize::default(), } } fn oci_to_grpc(bundle_dir: &str, cid: &str, oci: &ociSpec) -> Result { let process = match &oci.process { Some(p) => protobuf::SingularPtrField::some(process_oci_to_grpc(&p)), None => protobuf::SingularPtrField::none(), }; let root = match &oci.root { Some(r) => { let grpc_root = root_oci_to_grpc(bundle_dir, &r).map_err(|e| e)?; protobuf::SingularPtrField::some(grpc_root) } None => protobuf::SingularPtrField::none(), }; // FIXME: Implement Linux OCI spec handling let linux = grpcLinux::new(); if cid.len() < MIN_HOSTNAME_LEN as usize { return Err(anyhow!("container ID too short for hostname")); } // FIXME: Implement setting a custom (and unique!) hostname (requires uts ns setup) //let hostname = cid[0..MIN_HOSTNAME_LEN as usize].to_string(); let hostname = "".to_string(); let grpc_spec = grpcSpec { Version: oci.version.clone(), Process: process, Root: root, Hostname: hostname, Mounts: protobuf::RepeatedField::new(), Hooks: protobuf::SingularPtrField::none(), Annotations: HashMap::new(), Linux: protobuf::SingularPtrField::some(linux), Solaris: protobuf::SingularPtrField::none(), Windows: protobuf::SingularPtrField::none(), unknown_fields: protobuf::UnknownFields::new(), cached_size: protobuf::CachedSize::default(), }; Ok(grpc_spec) } fn uri_to_filename(uri: &str) -> Result { if !uri.starts_with(FILE_URI) { return Err(anyhow!(format!("invalid URI: {:?}", uri))); } let fields: Vec<&str> = uri.split(FILE_URI).collect(); if fields.len() != 2 { return Err(anyhow!(format!("invalid URI: {:?}", uri))); } Ok(fields[1].to_string()) } pub fn spec_file_to_string(spec_file: String) -> Result { let oci_spec = ociSpec::load(&spec_file).map_err(|e| anyhow!(e))?; serde_json::to_string(&oci_spec).map_err(|e| anyhow!(e)) } pub fn get_oci_spec_json(cfg: &Config) -> Result { let spec_file = config_file_from_bundle_dir(&cfg.bundle_dir)?; spec_file_to_string(spec_file) } pub fn get_grpc_spec(options: &mut Options, cid: &str) -> Result { let bundle_dir = get_option("bundle-dir", options, ""); let json_spec = get_option("spec", options, ""); assert_ne!(json_spec, ""); let oci_spec: ociSpec = serde_json::from_str(&json_spec).map_err(|e| anyhow!(e))?; Ok(oci_to_grpc(&bundle_dir, cid, &oci_spec)?) }