1
0
mirror of https://github.com/kata-containers/kata-containers.git synced 2025-05-06 07:27:28 +00:00

tools: Add agent-ctl tool

Add a low-level agent control tool that can manipulate the agent
via ttRPC.

Fixes: .

Signed-off-by: James O. D. Hunt <james.o.hunt@intel.com>
This commit is contained in:
James O. D. Hunt 2020-05-19 17:07:23 +01:00
parent 2e53d237ce
commit 8a1949546c
8 changed files with 2001 additions and 0 deletions

View File

@ -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 <james.o.hunt@intel.com>"]
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"

16
tools/agent-ctl/Makefile Normal file
View File

@ -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

39
tools/agent-ctl/README.md Normal file
View File

@ -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
```

File diff suppressed because it is too large Load Diff

292
tools/agent-ctl/src/main.rs Normal file
View File

@ -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::<u32>()
.map_err(|e| anyhow!(format!("invalid VSOCK CID number: {}", e.to_string())))?;
let port: u32 = port_str
.parse::<u32>()
.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);
}
_ => (),
};
}

View File

@ -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)
}

View File

@ -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<String, String>;
#[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,
}

View File

@ -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<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!(format!("failed to construct config file path: {:?}", e)))
}
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!(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<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)?)
}