Files
kata-containers/tools/agent-ctl/src/utils.rs
James O. D. Hunt 10b1deb274 tools: Add Unix socket support to agentl-ctl
Rather than specifying the VSOCK address as two CLI options
(`--vsock-cid` and `--vsock-port`), allow the agent's ttRPC server
address to be specified to the `agent-ctl` tool using a single URI
`--server-address` CLI option. Since the ttrpc crate supports VSOCK and
UNIX schemes, this allows the tool to be run inside the VM by specifying
a UNIX address.

Fixes: #549.

Signed-off-by: James O. D. Hunt <james.o.hunt@intel.com>
2020-08-28 08:06:11 +01:00

412 lines
12 KiB
Rust

// 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<Mutex<HashMap<&'static str, u8>>> = {
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<u8> {
if name == "" {
return Err(anyhow!("invalid signal"));
}
match name.parse::<u8>() {
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<i64> {
if human_time == "" || human_time == "0" {
return Ok(0);
}
let d: humantime::Duration = human_time
.parse::<humantime::Duration>()
.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<String> = 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<String> {
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<grpcRoot> {
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<grpcSpec> {
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<String> {
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<String> {
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<String> {
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<grpcSpec> {
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)?)
}