diff --git a/README.md b/README.md index fc382365ba..fa4b476670 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,11 @@ The [osbuilder](tools/osbuilder/README.md) tool can create a rootfs and a "mini O/S" image. This image is used by the hypervisor to setup the environment before switching to the workload. +#### `kata-agent-ctl` + +[`kata-agent-ctl`](tools/agent-ctl) is a low-level test tool for +interacting with the agent. + ### Web content The diff --git a/src/agent/logging/Cargo.toml b/pkg/logging/Cargo.toml similarity index 93% rename from src/agent/logging/Cargo.toml rename to pkg/logging/Cargo.toml index ab73679223..baa21ede89 100644 --- a/src/agent/logging/Cargo.toml +++ b/pkg/logging/Cargo.toml @@ -16,5 +16,6 @@ slog = { version = "2.5.2", features = ["dynamic-keys", "max_level_trace", "rele slog-json = "2.3.0" slog-async = "2.3.0" slog-scope = "4.1.2" -# for testing -tempfile = "3.1.0" \ No newline at end of file + +[dev-dependencies] +tempfile = "3.1.0" diff --git a/src/agent/logging/src/lib.rs b/pkg/logging/src/lib.rs similarity index 57% rename from src/agent/logging/src/lib.rs rename to pkg/logging/src/lib.rs index bd798379ae..d22fd59e05 100644 --- a/src/agent/logging/src/lib.rs +++ b/pkg/logging/src/lib.rs @@ -1,11 +1,9 @@ -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2019-2020 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 // -#[macro_use] -extern crate slog; -use slog::{BorrowedKV, Drain, Key, OwnedKV, OwnedKVList, Record, KV}; +use slog::{o, record_static, BorrowedKV, Drain, Key, OwnedKV, OwnedKVList, Record, KV}; use std::collections::HashMap; use std::io; use std::io::Write; @@ -13,6 +11,15 @@ use std::process; use std::result; use std::sync::Mutex; +const LOG_LEVELS: &[(&str, slog::Level)] = &[ + ("trace", slog::Level::Trace), + ("debug", slog::Level::Debug), + ("info", slog::Level::Info), + ("warn", slog::Level::Warning), + ("error", slog::Level::Error), + ("critical", slog::Level::Critical), +]; + // XXX: 'writer' param used to make testing possible. pub fn create_logger(name: &str, source: &str, level: slog::Level, writer: W) -> slog::Logger where @@ -43,7 +50,34 @@ where ) } +pub fn get_log_levels() -> Vec<&'static str> { + let result: Vec<&str> = LOG_LEVELS.iter().map(|value| value.0).collect(); + + result +} + +pub fn level_name_to_slog_level(level_name: &str) -> Result { + for tuple in LOG_LEVELS { + if tuple.0 == level_name { + return Ok(tuple.1); + } + } + + Err("invalid level name".to_string()) +} + +pub fn slog_level_to_level_name(level: slog::Level) -> Result<&'static str, &'static str> { + for tuple in LOG_LEVELS { + if tuple.1 == level { + return Ok(tuple.0); + } + } + + Err("invalid slog level") +} + // Used to convert an slog::OwnedKVList into a hash map. +#[derive(Debug)] struct HashSerializer { fields: HashMap, } @@ -125,8 +159,8 @@ where .log(&new_record, &OwnedKVList::from(logger_owned_kv)); match result { - Ok(_t) => Ok(()), - Err(_e) => Err(std::io::Error::new( + Ok(_) => Ok(()), + Err(_) => Err(std::io::Error::new( std::io::ErrorKind::Other, "failed to drain log".to_string(), )), @@ -148,6 +182,12 @@ impl RuntimeLevelFilter { level: Mutex::new(level), } } + + fn set_level(&self, level: slog::Level) { + let mut log_level = self.level.lock().unwrap(); + + *log_level = level; + } } impl Drain for RuntimeLevelFilter @@ -176,9 +216,149 @@ where mod tests { use super::*; use serde_json::Value; + use slog::info; use std::io::prelude::*; use tempfile::NamedTempFile; + #[test] + fn test_get_log_levels() { + let expected = vec!["trace", "debug", "info", "warn", "error", "critical"]; + + let log_levels = get_log_levels(); + assert_eq!(log_levels, expected); + } + + #[test] + fn test_level_name_to_slog_level() { + #[derive(Debug)] + struct TestData<'a> { + name: &'a str, + result: Result, + } + + let invalid_msg = "invalid level name"; + + let tests = &[ + TestData { + name: "", + result: Err(invalid_msg), + }, + TestData { + name: "foo", + result: Err(invalid_msg), + }, + TestData { + name: "x", + result: Err(invalid_msg), + }, + TestData { + name: ".", + result: Err(invalid_msg), + }, + TestData { + name: "trace", + result: Ok(slog::Level::Trace), + }, + TestData { + name: "debug", + result: Ok(slog::Level::Debug), + }, + TestData { + name: "info", + result: Ok(slog::Level::Info), + }, + TestData { + name: "warn", + result: Ok(slog::Level::Warning), + }, + TestData { + name: "error", + result: Ok(slog::Level::Error), + }, + TestData { + name: "critical", + result: Ok(slog::Level::Critical), + }, + ]; + + for (i, d) in tests.iter().enumerate() { + let msg = format!("test[{}]: {:?}", i, d); + + let result = level_name_to_slog_level(d.name); + + let msg = format!("{}, result: {:?}", msg, result); + + if d.result.is_ok() { + assert!(result.is_ok()); + + let result_level = result.unwrap(); + let expected_level = d.result.unwrap(); + + assert!(result_level == expected_level, msg); + continue; + } else { + assert!(result.is_err(), msg); + } + + let expected_error = format!("{}", d.result.as_ref().unwrap_err()); + let actual_error = format!("{}", result.unwrap_err()); + assert!(actual_error == expected_error, msg); + } + } + + #[test] + fn test_slog_level_to_level_name() { + #[derive(Debug)] + struct TestData<'a> { + level: slog::Level, + result: Result<&'a str, &'a str>, + } + + let tests = &[ + TestData { + level: slog::Level::Trace, + result: Ok("trace"), + }, + TestData { + level: slog::Level::Debug, + result: Ok("debug"), + }, + TestData { + level: slog::Level::Info, + result: Ok("info"), + }, + TestData { + level: slog::Level::Warning, + result: Ok("warn"), + }, + TestData { + level: slog::Level::Error, + result: Ok("error"), + }, + TestData { + level: slog::Level::Critical, + result: Ok("critical"), + }, + ]; + + for (i, d) in tests.iter().enumerate() { + let msg = format!("test[{}]: {:?}", i, d); + + let result = slog_level_to_level_name(d.level); + + let msg = format!("{}, result: {:?}", msg, result); + + if d.result.is_ok() { + assert!(result == d.result, msg); + continue; + } + + let expected_error = format!("{}", d.result.as_ref().unwrap_err()); + let actual_error = format!("{}", result.unwrap_err()); + assert!(actual_error == expected_error, msg); + } + } + #[test] fn test_create_logger_write_to_tmpfile() { // Create a writer for the logger drain to use diff --git a/src/agent/Cargo.toml b/src/agent/Cargo.toml index 57664b8629..97f6f33792 100644 --- a/src/agent/Cargo.toml +++ b/src/agent/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" [dependencies] oci = { path = "oci" } -logging = { path = "logging" } +logging = { path = "../../pkg/logging" } rustjail = { path = "rustjail" } protocols = { path = "protocols" } netlink = { path = "netlink" } @@ -33,7 +33,6 @@ tempfile = "3.1.0" [workspace] members = [ - "logging", "netlink", "oci", "protocols", diff --git a/tools/agent-ctl/Cargo.toml b/tools/agent-ctl/Cargo.toml new file mode 100644 index 0000000000..6815c96424 --- /dev/null +++ b/tools/agent-ctl/Cargo.toml @@ -0,0 +1,37 @@ +# Copyright (c) 2020 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 +# + +[package] +name = "kata-agent-ctl" +version = "0.0.1" +authors = ["James O. D. Hunt "] +edition = "2018" + +[dependencies] +protocols = { path = "../../src/agent/protocols" } +rustjail = { path = "../../src/agent/rustjail" } +oci = { path = "../../src/agent/oci" } + +clap = "2.33.0" +lazy_static = "1.4.0" +anyhow = "1.0.31" + +logging = { path = "../../pkg/logging" } +slog = "2.5.2" +slog-scope = "4.3.0" +rand = "0.7.3" +protobuf = "2.14.0" + +nix = "0.17.0" +libc = "0.2.69" +# XXX: Must be the same as the version used by the agent +ttrpc = { git = "https://github.com/containerd/ttrpc-rust", branch="0.3.0" } + +# For parsing timeouts +humantime = "2.0.0" + +# For Options (state passing) +serde = { version = "1.0.110", features = ["derive"] } +serde_json = "1.0.53" diff --git a/tools/agent-ctl/Makefile b/tools/agent-ctl/Makefile new file mode 100644 index 0000000000..51705a2a8c --- /dev/null +++ b/tools/agent-ctl/Makefile @@ -0,0 +1,16 @@ +# Copyright (c) 2020 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 +# + +default: build + +build: + cargo build -v + +clean: + cargo clean + +.PHONY: \ + build \ + clean diff --git a/tools/agent-ctl/README.md b/tools/agent-ctl/README.md new file mode 100644 index 0000000000..862a0e3351 --- /dev/null +++ b/tools/agent-ctl/README.md @@ -0,0 +1,39 @@ +# Agent Control tool + +* [Overview](#overview) +* [Audience and environment](#audience-and-environment) +* [Full details](#full-details) + +## Overview + +The Kata Containers agent control tool (`kata-agent-ctl`) is a low-level test +tool. It allows basic interaction with the Kata Containers agent, +`kata-agent`, that runs inside the virtual machine. + +Unlike the Kata Runtime, which only ever makes sequences of correctly ordered +and valid agent API calls, this tool allows users to make arbitrary agent API +calls and to control their parameters. + +## Audience and environment + +> **Warning:** +> +> This tool is for *advanced* users familiar with the low-level agent API calls. +> Further, it is designed to be run on test and development systems **only**: since +> the tool can make arbitrary API calls, it is possible to easily confuse +> irrevocably other parts of the system or even kill a running container or +> sandbox. + +## Full details + +For a usage statement, run: + +```sh +$ cargo run -- --help +``` + +To see some examples, run: + +```sh +$ cargo run -- examples +``` diff --git a/tools/agent-ctl/src/client.rs b/tools/agent-ctl/src/client.rs new file mode 100644 index 0000000000..fec54d2fb9 --- /dev/null +++ b/tools/agent-ctl/src/client.rs @@ -0,0 +1,1149 @@ +// Copyright (c) 2020 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +// Description: Client side of ttRPC comms + +use crate::types::{Config, Options}; +use crate::utils; +use anyhow::{anyhow, Result}; +use nix::sys::socket::{connect, socket, AddressFamily, SockAddr, SockFlag, SockType}; +use protocols::agent::*; +use protocols::agent_ttrpc::*; +use protocols::health::*; +use protocols::health_ttrpc::*; +use slog::{debug, info}; +use std::io; +use std::io::Write; // XXX: for flush() +use std::os::unix::io::RawFd; +use std::thread::sleep; +use std::time::Duration; +use ttrpc; + +// Agent command handler type +// +// Notes: +// +// - 'cmdline' is the command line (command name and optional space separate +// arguments). +// - 'options' can be read and written to, allowing commands to pass state to +// each other via well-known option names. +type AgentCmdFp = fn( + cfg: &Config, + client: &AgentServiceClient, + health: &HealthClient, + options: &mut Options, + args: &str, +) -> Result<()>; + +// Builtin command handler type +type BuiltinCmdFp = fn(cfg: &Config, options: &mut Options, args: &str) -> (Result<()>, bool); + +enum ServiceType { + Agent, + Health, +} + +// XXX: Agent command names *MUST* start with an upper-case letter. +struct AgentCmd { + name: &'static str, + st: ServiceType, + fp: AgentCmdFp, +} + +// XXX: Builtin command names *MUST* start with a lower-case letter. +struct BuiltinCmd { + name: &'static str, + descr: &'static str, + fp: BuiltinCmdFp, +} + +// Command that causes the agent to exit (iff tracing is enabled) +const SHUTDOWN_CMD: &'static str = "DestroySandbox"; + +// Command that requests this program ends +const CMD_QUIT: &'static str = "quit"; +const CMD_REPEAT: &'static str = "repeat"; + +const DEFAULT_PROC_SIGNAL: &'static str = "SIGKILL"; + +// Format is either "json" or "table". +const DEFAULT_PS_FORMAT: &str = "json"; + +const ERR_API_FAILED: &str = "API failed"; + +static AGENT_CMDS: &'static [AgentCmd] = &[ + AgentCmd { + name: "Check", + st: ServiceType::Health, + fp: agent_cmd_health_check, + }, + AgentCmd { + name: "Version", + st: ServiceType::Health, + fp: agent_cmd_health_version, + }, + AgentCmd { + name: "CreateContainer", + st: ServiceType::Agent, + fp: agent_cmd_container_create, + }, + AgentCmd { + name: "CreateSandbox", + st: ServiceType::Agent, + fp: agent_cmd_sandbox_create, + }, + AgentCmd { + name: "DestroySandbox", + st: ServiceType::Agent, + fp: agent_cmd_sandbox_destroy, + }, + AgentCmd { + name: "ExecProcess", + st: ServiceType::Agent, + fp: agent_cmd_container_exec, + }, + AgentCmd { + name: "GuestDetails", + st: ServiceType::Agent, + fp: agent_cmd_sandbox_guest_details, + }, + AgentCmd { + name: "ListInterfaces", + st: ServiceType::Agent, + fp: agent_cmd_sandbox_list_interfaces, + }, + AgentCmd { + name: "ListRoutes", + st: ServiceType::Agent, + fp: agent_cmd_sandbox_list_routes, + }, + AgentCmd { + name: "ListProcesses", + st: ServiceType::Agent, + fp: agent_cmd_container_list_processes, + }, + AgentCmd { + name: "PauseContainer", + st: ServiceType::Agent, + fp: agent_cmd_container_pause, + }, + AgentCmd { + name: "RemoveContainer", + st: ServiceType::Agent, + fp: agent_cmd_container_remove, + }, + AgentCmd { + name: "ResumeContainer", + st: ServiceType::Agent, + fp: agent_cmd_container_resume, + }, + AgentCmd { + name: "SignalProcess", + st: ServiceType::Agent, + fp: agent_cmd_container_signal_process, + }, + AgentCmd { + name: "StartContainer", + st: ServiceType::Agent, + fp: agent_cmd_container_start, + }, + AgentCmd { + name: "StartTracing", + st: ServiceType::Agent, + fp: agent_cmd_sandbox_tracing_start, + }, + AgentCmd { + name: "StatsContainer", + st: ServiceType::Agent, + fp: agent_cmd_container_stats, + }, + AgentCmd { + name: "StopTracing", + st: ServiceType::Agent, + fp: agent_cmd_sandbox_tracing_stop, + }, + AgentCmd { + name: "UpdateInterface", + st: ServiceType::Agent, + fp: agent_cmd_sandbox_update_interface, + }, + AgentCmd { + name: "UpdateRoutes", + st: ServiceType::Agent, + fp: agent_cmd_sandbox_update_routes, + }, + AgentCmd { + name: "WaitProcess", + st: ServiceType::Agent, + fp: agent_cmd_container_wait_process, + }, +]; + +static BUILTIN_CMDS: &'static [BuiltinCmd] = &[ + BuiltinCmd { + name: "echo", + descr: "Display the arguments", + fp: builtin_cmd_echo, + }, + BuiltinCmd { + name: "help", + descr: "Alias for 'list'", + fp: builtin_cmd_list, + }, + BuiltinCmd { + name: "list", + descr: "List all available commands", + fp: builtin_cmd_list, + }, + BuiltinCmd { + name: "repeat", + descr: "Repeat the next command 'n' times [-1 for forever]", + fp: builtin_cmd_repeat, + }, + BuiltinCmd { + name: "sleep", + descr: + "Pause for specified period number of nanoseconds (supports human-readable suffixes [no floating points numbers])", + fp: builtin_cmd_sleep, + }, + BuiltinCmd { + name: CMD_QUIT, + descr: "Exit this program", + fp: builtin_cmd_quit, + }, +]; + +fn get_agent_cmd_names() -> Vec { + let mut names = Vec::new(); + + for cmd in AGENT_CMDS { + names.push(cmd.name.to_string()); + } + + names +} + +fn get_agent_cmd_details() -> Vec { + let mut cmds = Vec::new(); + + for cmd in AGENT_CMDS { + let service = match cmd.st { + ServiceType::Agent => "agent", + ServiceType::Health => "health", + }; + + cmds.push(format!("{} ({} service)", cmd.name, service)); + } + + cmds +} + +fn get_agent_cmd_func(name: &str) -> Result { + for cmd in AGENT_CMDS { + if cmd.name == name { + return Ok(cmd.fp); + } + } + + Err(anyhow!(format!("Invalid command: {:?}", name))) +} + +fn get_builtin_cmd_details() -> Vec { + let mut cmds = Vec::new(); + + for cmd in BUILTIN_CMDS { + cmds.push(format!("{} ({})", cmd.name, cmd.descr)); + } + + cmds +} + +fn get_all_cmd_details() -> Vec { + let mut cmds = get_builtin_cmd_details(); + + cmds.append(&mut get_agent_cmd_names()); + + cmds +} + +fn get_builtin_cmd_func(name: &str) -> Result { + for cmd in BUILTIN_CMDS { + if cmd.name == name { + return Ok(cmd.fp); + } + } + + Err(anyhow!(format!("Invalid command: {:?}", name))) +} + +fn client_create_vsock_fd(cid: libc::c_uint, port: u32) -> Result { + let fd = socket( + AddressFamily::Vsock, + SockType::Stream, + SockFlag::SOCK_CLOEXEC, + None, + ) + .map_err(|e| anyhow!(e))?; + + let sock_addr = SockAddr::new_vsock(cid, port); + + connect(fd, &sock_addr).map_err(|e| anyhow!(e))?; + + Ok(fd) +} + +fn create_ttrpc_client(cid: libc::c_uint, port: u32) -> Result { + let fd = client_create_vsock_fd(cid, port).map_err(|e| { + anyhow!(format!( + "failed to create VSOCK connection (check agent is running): {:?}", + e + )) + })?; + + Ok(ttrpc::client::Client::new(fd)) +} + +fn kata_service_agent(cid: libc::c_uint, port: u32) -> Result { + let ttrpc_client = create_ttrpc_client(cid, port)?; + + Ok(AgentServiceClient::new(ttrpc_client)) +} + +fn kata_service_health(cid: libc::c_uint, port: u32) -> Result { + let ttrpc_client = create_ttrpc_client(cid, port)?; + + Ok(HealthClient::new(ttrpc_client)) +} + +fn announce(cfg: &Config) { + info!(sl!(), "announce"; "config" => format!("{:?}", cfg)); +} + +pub fn client(cfg: &Config, commands: Vec<&str>) -> Result<()> { + if commands.len() == 1 && commands[0] == "list" { + println!("Built-in commands:\n"); + + let mut builtin_cmds = get_builtin_cmd_details(); + builtin_cmds.sort(); + builtin_cmds.iter().for_each(|n| println!(" {}", n)); + + println!(); + + println!("Agent API commands:\n"); + + let mut agent_cmds = get_agent_cmd_details(); + agent_cmds.sort(); + agent_cmds.iter().for_each(|n| println!(" {}", n)); + + println!(); + + return Ok(()); + } + + announce(cfg); + + let cid = cfg.cid; + let port = cfg.port; + + let addr = format!("vsock://{}:{}", cid, port); + + // Create separate connections for each of the services provided + // by the agent. + let client = kata_service_agent(cid, port as u32)?; + let health = kata_service_health(cid, port as u32)?; + + let mut options = Options::new(); + + // Special-case loading the OCI config file so it is accessible + // to all commands. + let oci_spec_json = utils::get_oci_spec_json(cfg)?; + options.insert("spec".to_string(), oci_spec_json); + + // Convenience option + options.insert("bundle-dir".to_string(), cfg.bundle_dir.clone()); + + info!(sl!(), "client setup complete"; + "server-address" => addr); + + if cfg.interactive { + return interactive_client_loop(&cfg, &mut options, &client, &health); + } + + let mut repeat_count = 1; + + for cmd in commands { + if cmd.starts_with(CMD_REPEAT) { + repeat_count = get_repeat_count(cmd); + continue; + } + + let (result, shutdown) = + handle_cmd(&cfg, &client, &health, repeat_count, &mut options, &cmd); + if result.is_err() { + return result; + } + + if shutdown { + break; + } + + // Reset + repeat_count = 1; + } + + Ok(()) +} + +// Handle internal and agent API commands. +fn handle_cmd( + cfg: &Config, + client: &AgentServiceClient, + health: &HealthClient, + repeat_count: i64, + options: &mut Options, + cmdline: &str, +) -> (Result<()>, bool) { + let fields: Vec<&str> = cmdline.split_whitespace().collect(); + + let cmd = fields[0]; + + if cmd == "" { + // Ignore empty commands + return (Ok(()), false); + } + + let first = match cmd.chars().nth(0) { + Some(c) => c, + None => return (Err(anyhow!("failed to check command name")), false), + }; + + let args = if fields.len() > 1 { + fields[1..].join(" ") + } else { + String::new() + }; + + let mut count = 0; + + let mut count_msg = String::new(); + + if repeat_count < 0 { + count_msg = "forever".to_string(); + } + + let mut error_count: u64 = 0; + let mut result: (Result<()>, bool); + + loop { + if repeat_count > 0 { + count_msg = format!("{} of {}", count + 1, repeat_count); + } + + info!(sl!(), "Run command {:} ({})", cmd, count_msg); + + if first.is_lowercase() { + result = handle_builtin_cmd(cfg, options, cmd, &args); + } else { + result = handle_agent_cmd(cfg, client, health, options, cmd, &args); + } + + if result.0.is_err() { + if cfg.ignore_errors { + error_count += 1; + debug!(sl!(), "ignoring error for command {:}: {:?}", cmd, result.0); + } else { + return result; + } + } + + info!( + sl!(), + "Command {:} ({}) returned {:?}", cmd, count_msg, result + ); + + if repeat_count > 0 { + count += 1; + + if count == repeat_count { + break; + } + } + } + + if cfg.ignore_errors { + debug!(sl!(), "Error count for command {}: {}", cmd, error_count); + (Ok(()), result.1) + } else { + result + } +} + +fn handle_builtin_cmd( + cfg: &Config, + options: &mut Options, + cmd: &str, + args: &str, +) -> (Result<()>, bool) { + let f = match get_builtin_cmd_func(&cmd) { + Ok(fp) => fp, + Err(e) => return (Err(e), false), + }; + + f(cfg, options, &args) +} + +// Execute the ttRPC specified by the first field of "line". Return a result +// along with a bool which if set means the client should shutdown. +fn handle_agent_cmd( + cfg: &Config, + client: &AgentServiceClient, + health: &HealthClient, + options: &mut Options, + cmd: &str, + args: &str, +) -> (Result<()>, bool) { + let f = match get_agent_cmd_func(&cmd) { + Ok(fp) => fp, + Err(e) => return (Err(e), false), + }; + + let result = f(cfg, client, health, options, &args); + if result.is_err() { + return (result, false); + } + + let shutdown = cmd == SHUTDOWN_CMD; + + (Ok(()), shutdown) +} + +fn interactive_client_loop( + cfg: &Config, + options: &mut Options, + client: &AgentServiceClient, + health: &HealthClient, +) -> Result<()> { + let result = builtin_cmd_list(cfg, options, ""); + if result.0.is_err() { + return result.0; + } + + let mut repeat_count: i64 = 1; + + loop { + let cmdline = readline("Enter command") + .map_err(|e| anyhow!(format!("failed to read line: {}", e)))?; + + if cmdline == "" { + continue; + } + + if cmdline.starts_with(CMD_REPEAT) { + repeat_count = get_repeat_count(&cmdline); + continue; + } + + let (result, shutdown) = handle_cmd(cfg, client, health, repeat_count, options, &cmdline); + if result.is_err() { + return result; + } + + if shutdown { + break; + } + + // Reset + repeat_count = 1; + } + + Ok(()) +} + +fn readline(prompt: &str) -> std::result::Result { + print!("{}: ", prompt); + + io::stdout() + .flush() + .map_err(|e| format!("failed to flush: {:?}", e))?; + + let mut line = String::new(); + + std::io::stdin() + .read_line(&mut line) + .map_err(|e| format!("failed to read line: {:?}", e))?; + + // Remove NL + Ok(line.trim_end().to_string()) +} + +fn agent_cmd_health_check( + cfg: &Config, + _client: &AgentServiceClient, + health: &HealthClient, + _options: &mut Options, + _args: &str, +) -> Result<()> { + let mut req = CheckRequest::default(); + + // value unused + req.set_service("".to_string()); + + let reply = health + .check(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_health_version( + cfg: &Config, + _client: &AgentServiceClient, + health: &HealthClient, + _options: &mut Options, + _args: &str, +) -> Result<()> { + // XXX: Yes, the API is actually broken! + let mut req = CheckRequest::default(); + + // value unused + req.set_service("".to_string()); + + let reply = health + .version(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_sandbox_create( + cfg: &Config, + client: &AgentServiceClient, + _health: &HealthClient, + options: &mut Options, + args: &str, +) -> Result<()> { + let mut req = CreateSandboxRequest::default(); + + let sid = utils::get_option("sid", options, args); + req.set_sandbox_id(sid); + + let reply = client + .create_sandbox(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_sandbox_destroy( + cfg: &Config, + client: &AgentServiceClient, + _health: &HealthClient, + _options: &mut Options, + _args: &str, +) -> Result<()> { + let req = DestroySandboxRequest::default(); + + let reply = client + .destroy_sandbox(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_container_create( + cfg: &Config, + client: &AgentServiceClient, + _health: &HealthClient, + options: &mut Options, + args: &str, +) -> Result<()> { + let mut req = CreateContainerRequest::default(); + + let cid = utils::get_option("cid", options, args); + let exec_id = utils::get_option("exec_id", options, args); + + // FIXME: container create: add back "spec=file:///" support + + let grpc_spec = utils::get_grpc_spec(options, &cid).map_err(|e| anyhow!(e))?; + + req.set_container_id(cid); + req.set_exec_id(exec_id); + req.set_OCI(grpc_spec); + + let reply = client + .create_container(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_container_remove( + cfg: &Config, + client: &AgentServiceClient, + _health: &HealthClient, + options: &mut Options, + args: &str, +) -> Result<()> { + let mut req = RemoveContainerRequest::default(); + + let cid = utils::get_option("cid", options, args); + + req.set_container_id(cid); + + let reply = client + .remove_container(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_container_exec( + cfg: &Config, + client: &AgentServiceClient, + _health: &HealthClient, + options: &mut Options, + args: &str, +) -> Result<()> { + let mut req = ExecProcessRequest::default(); + + let cid = utils::get_option("cid", options, args); + let exec_id = utils::get_option("exec_id", options, args); + + let grpc_spec = utils::get_grpc_spec(options, &cid).map_err(|e| anyhow!(e))?; + + let process = grpc_spec + .Process + .into_option() + .ok_or(format!( + "failed to get process from OCI spec: {}", + cfg.bundle_dir + )) + .map_err(|e| anyhow!(e))?; + + req.set_container_id(cid); + req.set_exec_id(exec_id); + req.set_process(process); + + let reply = client + .exec_process(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_container_stats( + cfg: &Config, + client: &AgentServiceClient, + _health: &HealthClient, + options: &mut Options, + args: &str, +) -> Result<()> { + let mut req = StatsContainerRequest::default(); + + let cid = utils::get_option("cid", options, args); + + req.set_container_id(cid); + + let reply = client + .stats_container(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_container_pause( + cfg: &Config, + client: &AgentServiceClient, + _health: &HealthClient, + options: &mut Options, + args: &str, +) -> Result<()> { + let mut req = PauseContainerRequest::default(); + + let cid = utils::get_option("cid", options, args); + + req.set_container_id(cid); + + let reply = client + .pause_container(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_container_resume( + cfg: &Config, + client: &AgentServiceClient, + _health: &HealthClient, + options: &mut Options, + args: &str, +) -> Result<()> { + let mut req = ResumeContainerRequest::default(); + + let cid = utils::get_option("cid", options, args); + + req.set_container_id(cid); + + let reply = client + .resume_container(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_container_start( + cfg: &Config, + client: &AgentServiceClient, + _health: &HealthClient, + options: &mut Options, + args: &str, +) -> Result<()> { + let mut req = StartContainerRequest::default(); + + let cid = utils::get_option("cid", options, args); + + req.set_container_id(cid); + + let reply = client + .start_container(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_sandbox_guest_details( + cfg: &Config, + client: &AgentServiceClient, + _health: &HealthClient, + _options: &mut Options, + _args: &str, +) -> Result<()> { + let mut req = GuestDetailsRequest::default(); + + req.set_mem_block_size(true); + + let reply = client + .get_guest_details(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_container_list_processes( + cfg: &Config, + client: &AgentServiceClient, + _health: &HealthClient, + options: &mut Options, + args: &str, +) -> Result<()> { + let mut req = ListProcessesRequest::default(); + + let cid = utils::get_option("cid", options, args); + + let mut list_format = utils::get_option("format", options, args); + + if list_format == "" { + list_format = DEFAULT_PS_FORMAT.to_string(); + } + + req.set_container_id(cid); + req.set_format(list_format); + + let reply = client + .list_processes(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_container_wait_process( + cfg: &Config, + client: &AgentServiceClient, + _health: &HealthClient, + options: &mut Options, + args: &str, +) -> Result<()> { + let mut req = WaitProcessRequest::default(); + + let cid = utils::get_option("cid", options, args); + let exec_id = utils::get_option("exec_id", options, args); + + req.set_container_id(cid); + req.set_exec_id(exec_id); + + let reply = client + .wait_process(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_container_signal_process( + cfg: &Config, + client: &AgentServiceClient, + _health: &HealthClient, + options: &mut Options, + args: &str, +) -> Result<()> { + let mut req = SignalProcessRequest::default(); + + let cid = utils::get_option("cid", options, args); + let exec_id = utils::get_option("exec_id", options, args); + + let mut sigstr = utils::get_option("signal", options, args); + + // Convert to a numeric + if sigstr == "" { + sigstr = DEFAULT_PROC_SIGNAL.to_string(); + } + + let signum = utils::signame_to_signum(&sigstr).map_err(|e| anyhow!(e))?; + + req.set_container_id(cid); + req.set_exec_id(exec_id); + req.set_signal(signum as u32); + + let reply = client + .signal_process(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_sandbox_tracing_start( + cfg: &Config, + client: &AgentServiceClient, + _health: &HealthClient, + _options: &mut Options, + _args: &str, +) -> Result<()> { + let req = StartTracingRequest::default(); + + let reply = client + .start_tracing(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_sandbox_tracing_stop( + cfg: &Config, + client: &AgentServiceClient, + _health: &HealthClient, + _options: &mut Options, + _args: &str, +) -> Result<()> { + let req = StopTracingRequest::default(); + + let reply = client + .stop_tracing(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_sandbox_update_interface( + cfg: &Config, + client: &AgentServiceClient, + _health: &HealthClient, + _options: &mut Options, + _args: &str, +) -> Result<()> { + let req = UpdateInterfaceRequest::default(); + + let reply = client + .update_interface(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + // FIXME: Implement 'UpdateInterface' fully. + eprintln!("FIXME: 'UpdateInterface' not fully implemented"); + + // let if = ...; + // req.set_interface(if); + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_sandbox_update_routes( + cfg: &Config, + client: &AgentServiceClient, + _health: &HealthClient, + _options: &mut Options, + _args: &str, +) -> Result<()> { + let req = UpdateRoutesRequest::default(); + + let reply = client + .update_routes(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + // FIXME: Implement 'UpdateRoutes' fully. + eprintln!("FIXME: 'UpdateRoutes' not fully implemented"); + + // let routes = ...; + // req.set_routes(routes); + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_sandbox_list_interfaces( + cfg: &Config, + client: &AgentServiceClient, + _health: &HealthClient, + _options: &mut Options, + _args: &str, +) -> Result<()> { + let req = ListInterfacesRequest::default(); + + let reply = client + .list_interfaces(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +fn agent_cmd_sandbox_list_routes( + cfg: &Config, + client: &AgentServiceClient, + _health: &HealthClient, + _options: &mut Options, + _args: &str, +) -> Result<()> { + let req = ListRoutesRequest::default(); + + let reply = client + .list_routes(&req, cfg.timeout_nano) + .map_err(|e| anyhow!(format!("{}: {:?}", ERR_API_FAILED, e)))?; + + info!(sl!(), "response received"; + "response" => format!("{:?}", reply)); + + Ok(()) +} + +#[inline] +fn builtin_cmd_repeat(_cfg: &Config, _options: &mut Options, _args: &str) -> (Result<()>, bool) { + // XXX: NOP implementation. Due to the way repeat has to work, providing + // handler like this is "too late" to be useful. However, a handler + // is required as "repeat" is a valid command. + // + // A cleaner approach would be to make `AgentCmd.fp` an `Option` which for + // this command would be specified as `None`, but this is the only command + // which doesn't need an implementation, so this approach is simpler :) + + (Ok(()), false) +} + +fn builtin_cmd_sleep(_cfg: &Config, _options: &mut Options, args: &str) -> (Result<()>, bool) { + let ns = match utils::human_time_to_ns(args) { + Ok(t) => t, + Err(e) => return (Err(e), false), + }; + + sleep(Duration::from_nanos(ns as u64)); + + (Ok(()), false) +} + +fn builtin_cmd_echo(_cfg: &Config, _options: &mut Options, args: &str) -> (Result<()>, bool) { + println!("{}", args); + + (Ok(()), false) +} + +fn builtin_cmd_quit(_cfg: &Config, _options: &mut Options, _args: &str) -> (Result<()>, bool) { + (Ok(()), true) +} + +fn builtin_cmd_list(_cfg: &Config, _options: &mut Options, _args: &str) -> (Result<()>, bool) { + let cmds = get_all_cmd_details(); + + cmds.iter().for_each(|n| println!(" - {}", n)); + + println!(""); + + (Ok(()), false) +} + +fn get_repeat_count(cmdline: &str) -> i64 { + let default_repeat_count: i64 = 1; + + let fields: Vec<&str> = cmdline.split_whitespace().collect(); + + if fields.len() < 2 { + return default_repeat_count; + } + + if fields[0] != CMD_REPEAT { + return default_repeat_count; + } + + let count = fields[1]; + + match count.parse::() { + Ok(n) => return n, + Err(_) => return default_repeat_count, + } +} diff --git a/tools/agent-ctl/src/main.rs b/tools/agent-ctl/src/main.rs new file mode 100644 index 0000000000..2280f00873 --- /dev/null +++ b/tools/agent-ctl/src/main.rs @@ -0,0 +1,292 @@ +// Copyright (c) 2020 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +#[macro_use] +extern crate lazy_static; + +use anyhow::{anyhow, Result}; +use clap::{crate_name, crate_version, App, Arg, SubCommand}; +use std::io; +use std::process::exit; + +// Convenience macro to obtain the scope logger +#[macro_export] +macro_rules! sl { + () => { + slog_scope::logger() + }; +} + +mod client; +mod rpc; +mod types; +mod utils; + +const DEFAULT_LOG_LEVEL: slog::Level = slog::Level::Info; + +const DESCRIPTION_TEXT: &str = r#"DESCRIPTION: + Low-level test tool that allows basic interaction with + the Kata Containers agent using agent API calls."#; + +const ABOUT_TEXT: &str = "Kata Containers agent tool"; + +const WARNING_TEXT: &str = r#"WARNING: + This tool is for *advanced* users familiar with the low-level agent API calls. + Further, it is designed to be run on test and development systems **only**: + since the tool can make arbitrary API calls, it is possible to easily confuse + irrevocably other parts of the system or even kill a running container or + sandbox."#; + +fn make_examples_text(program_name: &str) -> String { + let bundle = "$bundle_dir"; + let cid = 3; + let container_id = "$container_id"; + let config_file_uri = "file:///tmp/config.json"; + let port = 1024; + let sandbox_id = "$sandbox_id"; + + format!( + r#"EXAMPLES: + +- Check if the agent is running: + + $ {program} connect --vsock-cid {cid} --vsock-port {port} --cmd Check + +- Query the agent environment: + + $ {program} connect --vsock-cid {cid} --vsock-port {port} --cmd GuestDetails + +- List all available (built-in and Kata Agent API) commands: + + $ {program} connect --vsock-cid {cid} --vsock-port {port} --cmd list + +- Generate a random container ID: + + $ {program} generate-cid + +- Generate a random sandbox ID: + + $ {program} generate-sid + +- Attempt to create 7 sandboxes, ignoring any errors: + + $ {program} connect --vsock-cid {cid} --vsock-port {port} --repeat 7 --cmd CreateSandbox + +- Query guest details forever: + + $ {program} connect --vsock-cid {cid} --vsock-port {port} --repeat -1 --cmd GuestDetails + +- Send a 'SIGUSR1' signal to a container process: + + $ {program} connect --vsock-cid {cid} --vsock-port {port} --cmd 'SignalProcess signal=usr1 sid={sandbox_id} cid={container_id}' + +- Create a sandbox with a single container, and then destroy everything: + + $ {program} connect --vsock-cid {cid} --vsock-port {port} --cmd CreateSandbox + $ {program} connect --vsock-cid {cid} --vsock-port {port} --bundle-dir {bundle:?} --cmd CreateContainer + $ {program} connect --vsock-cid {cid} --vsock-port {port} --cmd DestroySandbox + +- Create a Container using a custom configuration file: + + $ {program} connect --vsock-cid {cid} --vsock-port {port} --bundle-dir {bundle:?} --cmd 'CreateContainer spec={config_file_uri}' + "#, + bundle = bundle, + cid = cid, + config_file_uri = config_file_uri, + container_id = container_id, + port = port, + program = program_name, + sandbox_id = sandbox_id, + ) +} + +fn connect(name: &str, global_args: clap::ArgMatches) -> Result<()> { + let args = global_args + .subcommand_matches("connect") + .ok_or("BUG: missing sub-command arguments".to_string()) + .map_err(|e| anyhow!(e))?; + + let interactive = args.is_present("interactive"); + let ignore_errors = args.is_present("ignore-errors"); + + let cid_str = args + .value_of("vsock-cid") + .ok_or("need VSOCK cid".to_string()) + .map_err(|e| anyhow!(e))?; + + let port_str = args + .value_of("vsock-port") + .ok_or("need VSOCK port number".to_string()) + .map_err(|e| anyhow!(e))?; + + let cid: u32 = cid_str + .parse::() + .map_err(|e| anyhow!(format!("invalid VSOCK CID number: {}", e.to_string())))?; + + let port: u32 = port_str + .parse::() + .map_err(|e| anyhow!(format!("invalid VSOCK port number: {}", e)))?; + + let mut commands: Vec<&str> = Vec::new(); + + if !interactive { + commands = args + .values_of("cmd") + .ok_or("need commands to send to the server".to_string()) + .map_err(|e| anyhow!(e))? + .collect(); + } + + // Cannot fail as a default has been specified + let log_level_name = global_args.value_of("log-level").unwrap(); + + let log_level = logging::level_name_to_slog_level(log_level_name).map_err(|e| anyhow!(e))?; + + let writer = io::stdout(); + let logger = logging::create_logger(name, crate_name!(), log_level, writer); + + let timeout_nano: i64 = match args.value_of("timeout") { + Some(t) => utils::human_time_to_ns(t).map_err(|e| e)?, + None => 0, + }; + + let bundle_dir = args.value_of("bundle-dir").unwrap_or(""); + + let result = rpc::run( + &logger, + cid, + port, + bundle_dir, + interactive, + ignore_errors, + timeout_nano, + commands, + ); + if result.is_err() { + return result; + } + + Ok(()) +} + +fn real_main() -> Result<()> { + let name = crate_name!(); + + let app = App::new(name) + .version(crate_version!()) + .about(ABOUT_TEXT) + .long_about(DESCRIPTION_TEXT) + .after_help(WARNING_TEXT) + .arg( + Arg::with_name("log-level") + .long("log-level") + .short("l") + .help("specific log level") + .default_value(logging::slog_level_to_level_name(DEFAULT_LOG_LEVEL).unwrap()) + .possible_values(&logging::get_log_levels()) + .takes_value(true) + .required(false), + ) + .subcommand( + SubCommand::with_name("connect") + .about("Connect to agent") + .after_help(WARNING_TEXT) + .arg( + Arg::with_name("bundle-dir") + .long("bundle-dir") + .help("OCI bundle directory") + .takes_value(true) + .value_name("directory"), + ) + .arg( + Arg::with_name("vsock-cid") + .long("vsock-cid") + .help("VSOCK Context ID") + .takes_value(true) + .value_name("CID"), + ) + .arg( + Arg::with_name("cmd") + .long("cmd") + .short("c") + .takes_value(true) + .multiple(true) + .help("API command (with optional arguments) to send to the server"), + ) + .arg( + Arg::with_name("ignore-errors") + .long("ignore-errors") + .help("Don't exit on first error"), + ) + .arg( + Arg::with_name("interactive") + .short("i") + .long("interactive") + .help("Allow interactive client"), + ) + .arg( + Arg::with_name("vsock-port") + .long("vsock-port") + .help("VSOCK Port number") + .takes_value(true) + .value_name("port-number"), + ) + .arg( + Arg::with_name("timeout") + .long("timeout") + .help("timeout value as nanoseconds or using human-readable suffixes (0 [forever], 99ns, 30us, 2ms, 5s, 7m, etc)") + .takes_value(true) + .value_name("human-time"), + ) + ) + .subcommand( + SubCommand::with_name("generate-cid") + .about("Create a random container ID") + ) + .subcommand( + SubCommand::with_name("generate-sid") + .about("Create a random sandbox ID") + ) + .subcommand( + SubCommand::with_name("examples") + .about("Show usage examples") + ); + + let args = app.get_matches(); + + let subcmd = args + .subcommand_name() + .ok_or("need sub-command".to_string()) + .map_err(|e| anyhow!(e))?; + + match subcmd { + "generate-cid" => { + println!("{}", utils::random_container_id()); + return Ok(()); + } + "generate-sid" => { + println!("{}", utils::random_sandbox_id()); + return Ok(()); + } + "examples" => { + println!("{}", make_examples_text(name)); + return Ok(()); + } + "connect" => { + return connect(name, args); + } + _ => return Err(anyhow!(format!("invalid sub-command: {:?}", subcmd))), + } +} + +fn main() { + match real_main() { + Err(e) => { + eprintln!("ERROR: {}", e); + exit(1); + } + _ => (), + }; +} diff --git a/tools/agent-ctl/src/rpc.rs b/tools/agent-ctl/src/rpc.rs new file mode 100644 index 0000000000..3a4cd5a098 --- /dev/null +++ b/tools/agent-ctl/src/rpc.rs @@ -0,0 +1,37 @@ +// Copyright (c) 2020 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +// Description: ttRPC logic entry point + +use anyhow::Result; +use slog::{o, Logger}; + +use crate::client::client; +use crate::types::Config; + +pub fn run( + logger: &Logger, + cid: u32, + port: u32, + bundle_dir: &str, + interactive: bool, + ignore_errors: bool, + timeout_nano: i64, + commands: Vec<&str>, +) -> Result<()> { + let cfg = Config { + cid: cid, + port: port, + bundle_dir: bundle_dir.to_string(), + timeout_nano: timeout_nano, + interactive: interactive, + ignore_errors: ignore_errors, + }; + + // Maintain the global logger for the duration of the ttRPC comms + let _guard = slog_scope::set_global_logger(logger.new(o!("subsystem" => "rpc"))); + + client(&cfg, commands) +} diff --git a/tools/agent-ctl/src/types.rs b/tools/agent-ctl/src/types.rs new file mode 100644 index 0000000000..b9992d70e5 --- /dev/null +++ b/tools/agent-ctl/src/types.rs @@ -0,0 +1,20 @@ +// Copyright (c) 2020 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +// Type used to pass optional state between cooperating API calls. +pub type Options = HashMap; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub cid: u32, + pub port: u32, + pub bundle_dir: String, + pub timeout_nano: i64, + pub interactive: bool, + pub ignore_errors: bool, +} diff --git a/tools/agent-ctl/src/utils.rs b/tools/agent-ctl/src/utils.rs new file mode 100644 index 0000000000..3c5274e42a --- /dev/null +++ b/tools/agent-ctl/src/utils.rs @@ -0,0 +1,411 @@ +// 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!(format!("failed to construct config file path: {:?}", e))) +} + +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!(format!("failed to construct bundle path: {:?}", e)))? + }; + + 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)?) +}