container-pid-0.2.0/.cargo_vcs_info.json0000644000000001120000000000000135600ustar { "git": { "sha1": "0a0546d2085f0f0c7b12d9697ff7de77f15aab15" } } container-pid-0.2.0/.gitignore000064400000000000000000000000100000000000000143130ustar 00000000000000/target container-pid-0.2.0/Cargo.lock0000644000000011130000000000000115350ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "container-pid" version = "0.2.0" dependencies = [ "libc", "simple-error", ] [[package]] name = "libc" version = "0.2.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" [[package]] name = "simple-error" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc47a29ce97772ca5c927f75bac34866b16d64e07f330c3248e2d7226623901b" container-pid-0.2.0/Cargo.toml0000644000000015550000000000000115720ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] edition = "2018" name = "container-pid" version = "0.2.0" authors = ["Jörg Thalheim "] description = "Resolve container names to their PIDs" categories = ["os::linux-apis"] license = "MIT" repository = "https://github.com/Mic92/container-pid" [dependencies.libc] version = "~0" [dependencies.simple-error] version = "~0" container-pid-0.2.0/Cargo.toml.orig000064400000000000000000000005010000000000000152170ustar 00000000000000[package] name = "container-pid" version = "0.2.0" authors = ["Jörg Thalheim "] description = "Resolve container names to their PIDs" license = "MIT" categories = ["os::linux-apis"] repository = "https://github.com/Mic92/container-pid" edition = "2018" [dependencies] libc = "~0" simple-error = "~0" container-pid-0.2.0/README.md000064400000000000000000000000700000000000000136100ustar 00000000000000## container-pid Resolve container names to their PIDs container-pid-0.2.0/default.nix000064400000000000000000000001730000000000000145010ustar 00000000000000with import {}; mkShell { nativeBuildInputs = [ bashInteractive cargo cargo-watch rustc ]; } container-pid-0.2.0/examples/container-pid.rs000064400000000000000000000014550000000000000172610ustar 00000000000000use container_pid::{lookup_container_pid, lookup_container_type}; use std::env; use std::process::exit; pub fn main() { let args: Vec = env::args().collect(); if args.len() < 2 { eprintln!("USAGE: {} container-name [container-type]", args[0]); exit(1); } let types = if args.len() >= 3 { match lookup_container_type(&args[2]) { None => { eprintln!("unsupported container type: {}", args[2]); exit(1); } Some(c) => vec![c], } } else { vec![] }; let name = &args[1]; match lookup_container_pid(&name, &types) { Ok(pid) => { println!("{}", pid); } Err(e) => { eprintln!("{}", e); exit(1); } } } container-pid-0.2.0/src/cmd.rs000064400000000000000000000024170000000000000142400ustar 00000000000000use libc::c_char; use simple_error::bail; use std::env; use std::ffi::CStr; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use std::ptr; use crate::result::Result; fn access>(path: &P, amode: libc::c_int) -> Result<()> { let mut buf = [0u8; libc::PATH_MAX as usize]; let path = path.as_ref().as_os_str().as_bytes(); if path.len() >= libc::PATH_MAX as usize { bail!("invalid argument"); } // TODO: Replace with bytes::copy_memory. rust-lang/rust#24028 let cstr = unsafe { ptr::copy_nonoverlapping(path.as_ptr(), buf.as_mut_ptr(), path.len()); CStr::from_ptr(buf.as_ptr() as *const c_char) }; let res = unsafe { libc::access(cstr.as_ptr(), amode) }; if res < 0 { bail!("access failed: {}", res) } Ok(()) } pub fn which

(exe_name: P) -> Option where P: AsRef, { env::var_os("PATH").and_then(|paths| { env::split_paths(&paths) .filter_map(|dir| { let full_path = dir.join(&exe_name); let res = access(&full_path, libc::X_OK); if res.is_ok() { Some(full_path) } else { None } }) .next() }) } container-pid-0.2.0/src/command.rs000064400000000000000000000030710000000000000151100ustar 00000000000000use simple_error::{bail, try_with}; use std::fs; use crate::result::Result; use crate::Container; #[derive(Clone, Debug)] pub struct Command {} impl Container for Command { fn lookup(&self, container_id: &str) -> Result { let needle = container_id.as_bytes(); let dir = try_with!(fs::read_dir("/proc"), "failed to read /proc directory"); let own_pid = std::process::id() as libc::pid_t; for entry in dir { let entry = try_with!(entry, "error while reading /proc"); let cmdline = entry.path().join("cmdline"); let pid = match entry.file_name().to_string_lossy().parse::() { Ok(pid) => pid, _ => { continue; } }; if pid == own_pid { continue; } // ignore error if process exits before we can read it if let Ok(mut arguments) = fs::read(cmdline.clone()) { // treat all arguments as one large string for byte in arguments.iter_mut() { if *byte == b'\0' { *byte = b' '; } } if arguments .windows(needle.len()) .any(|window| window == needle) { return Ok(pid); } } } bail!("No command found that matches {}", container_id) } fn check_required_tools(&self) -> Result<()> { Ok(()) } } container-pid-0.2.0/src/containerd.rs000064400000000000000000000037630000000000000156300ustar 00000000000000use libc::pid_t; use simple_error::{bail, try_with}; use std::process::Command; use crate::cmd; use crate::result::Result; use crate::Container; #[derive(Clone, Debug)] pub struct Containerd {} impl Container for Containerd { fn lookup(&self, container_id: &str) -> Result { let command = "ctr task list"; let output = try_with!( Command::new("ctr").args(&["task", "list"]).output(), "Running '{}' failed", command ); if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); bail!( "Failed to list containers. '{}' exited with {}: {}", command, output.status, stderr.trim_end() ); } // $ ctr task list // TASK PID STATUS // v2 17515 RUNNING // v1 14602 RUNNING let mut lines = output.stdout.split(|&c| c == b'\n'); lines.next(); // skip header let pid_str = lines.find_map(|line| { let line_str = String::from_utf8_lossy(&line); let cols = line_str.split_whitespace().collect::>(); if cols.len() != 3 { return None; } if cols[0] == container_id { Some(String::from(cols[1])) } else { None } }); match pid_str { Some(pid_str) => { let pid = try_with!( pid_str.parse::(), "read invalid pid from ctr task list: '{}'", pid_str ); Ok(pid) } None => { bail!("No container with id {} found", container_id) } } } fn check_required_tools(&self) -> Result<()> { if cmd::which("ctr").is_some() { Ok(()) } else { bail!("ctr not found") } } } container-pid-0.2.0/src/docker.rs000064400000000000000000000034620000000000000147450ustar 00000000000000use libc::pid_t; use simple_error::{bail, try_with}; use std::process::Command; use crate::cmd; use crate::result::Result; use crate::Container; #[derive(Clone, Debug)] pub struct Docker {} pub fn parse_docker_output(cmd: &[&str], container_id: &str) -> Result { let output = try_with!( Command::new(&cmd[0]).args(&cmd[1..]).output(), "Running '{}' failed", cmd.join(" ") ); if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); bail!( "Failed to list containers. '{}' exited with {}: {}", cmd.join(" "), output.status, stderr.trim_end() ); } let fields: Vec<&[u8]> = output.stdout.splitn(2, |c| *c == b';').collect(); assert!(fields.len() == 2); if fields[0] != b"true" { bail!("container '{}' is not running", container_id); } let pid = String::from_utf8_lossy(fields[1]); Ok(try_with!( pid.trim_end().parse::(), "expected valid process id from '{}', got: {}", cmd.join(" "), pid )) } impl Container for Docker { fn lookup(&self, container_id: &str) -> Result { let command = if cmd::which("docker-pid").is_some() { vec!["docker-pid", container_id] } else { vec![ "docker", "inspect", "--format", "{{.State.Running}};{{.State.Pid}}", container_id, ] }; parse_docker_output(command.as_slice(), container_id) } fn check_required_tools(&self) -> Result<()> { if cmd::which("docker-pid").is_some() || cmd::which("docker").is_some() { return Ok(()); } bail!("Neither docker or docker-pid was found") } } container-pid-0.2.0/src/kubernetes.rs000064400000000000000000000132360000000000000156450ustar 00000000000000//! This module uses `kubectl` to get a containerd id and then searches cgroups for one with that //! id as name. It returns any pid which is a member of that group. //! //! Possible container_id inputs: //! //! - `podname` to use default namespace and first container in that pod //! - one `/`: `namespace/podname` to override default namespace //! - two `/`: `namespace/podname/container` to be super explicit use crate::cmd; use crate::result::Result; use crate::Container; use simple_error::{bail, require_with, try_with}; use std::ffi::OsString; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; use std::str::from_utf8; use std::str::FromStr; #[derive(Clone, Debug)] pub struct Kubernetes {} pub const DEFAULT_NAMESPACE: &str = "default"; impl Container for Kubernetes { /// There is many ways to do this: /// - similar to command.rs: a bit looser pattern matching on /proc/$pid/cmdline /// - the following: fn lookup(&self, container_id: &str) -> Result { let (namespace, pod_name, container_name) = try_with!( parse_userinput(container_id), "cannot parse user given container_id" ); let containerdid = try_with!( get_containerd_id(namespace, pod_name, container_name), "containerd id lookup failed" ); let cgroup = try_with!(find_cgroup(containerdid), "cannot find matching cgroup"); let pid = try_with!( get_cgroup_pid(&cgroup), "cannot determine a singular pid owning the cgroup {:?}", cgroup ); Ok(pid) } fn check_required_tools(&self) -> Result<()> { if cmd::which("kubectl").is_some() { Ok(()) } else { bail!("kubectl not found") } } } /// allows the user to prepend the pod name with `custom-namespace/pod-name` to override the /// namespace (`default`). By default this will take the first container of the pod. That however /// can be overridden by appending it like `namespace/podname/container`. pub fn parse_userinput(container_id: &str) -> Result<(&str, &str, Option<&str>)> { let fields = container_id.splitn(3, '/').collect::>(); if fields.len() == 1 { return Ok((DEFAULT_NAMESPACE, container_id, None)); } else if fields.len() == 2 { return Ok((fields[0], fields[1], None)); } else if fields.len() == 3 { return Ok((fields[0], fields[1], Some(fields[2]))); } unreachable!(); } /// find `containerd://hash` id and return hash. /// Potentially vulnerable: passes unchecked user supplied strings to command. pub fn get_containerd_id( namespace: &str, pod_name: &str, container_name: Option<&str>, ) -> Result { let jsonpath = format!("jsonpath='{{range .items[?(@.metadata.name==\"{}\")].status.containerStatuses[*]}}{{.name}}{{\"\\t\"}}{{.containerID}}{{\"\\n\"}}{{end}}'", pod_name); let result = try_with!( Command::new("kubectl") .arg("get") .arg("pod") .arg("-o") .arg(jsonpath) .arg("-n") .arg(namespace) .output(), "kubectl command cannot be spawned" ); if !result.status.success() { let stderr = String::from_utf8_lossy(&result.stderr); bail!( "kubectl get pod request failed (ret code {:?}): {}", result.status.code(), stderr ); } let containers = try_with!(from_utf8(&result.stdout), "response contains non-utf8"); let containerid = containers.split('\n').find_map(|line| { // line = "containername\tcontainerdid" let cols: Vec<&str> = line.split('\t').collect(); if cols.len() != 2 { return None; } if let Some(name) = container_name { // return name-matching containerid if cols[0] == name { return Some(cols[1]); } } else { // return any containerid return Some(cols[1]); } None }); let containerid = require_with!( containerid, "no container found matching {:?}", container_name ); let containerid = require_with!( containerid.strip_prefix("containerd://"), "unexpected/unparsable containerd id" ); Ok(String::from(containerid)) } pub fn find_cgroup(containerdid: String) -> Result { let path = visit_dirs( &PathBuf::from("/sys/fs/cgroup"), &OsString::from(containerdid), )?; Ok(path) } // one possible implementation of walking a directory from // https://doc.rust-lang.org/std/fs/fn.read_dir.html fn visit_dirs(dir: &Path, containerdid: &OsString) -> Result { for entry in try_with!(std::fs::read_dir(dir), "cannot list {:?}", dir) { let entry = try_with!(entry, "cannot read entry in dir {:?}", dir); if &entry.file_name() == containerdid { return Ok(entry.path()); } let path = entry.path(); if path.is_dir() { if let Ok(path) = visit_dirs(&path, containerdid) { return Ok(path); } } } bail!("nothing found"); } /// return any pid part of this cgroup pub fn get_cgroup_pid(cgroup: &Path) -> Result { let path = cgroup.join("cgroup.procs"); let bytes = try_with!(fs::read(&path), "cannot read {:?}", &path); let pids = try_with!( String::from_utf8(bytes), "kernel does not respond with valid encoding" ); let pids = pids.splitn(2, '\n').collect::>()[0]; // first line let pid: u64 = try_with!(u64::from_str(pids), "cannot parse pid ({:?})", pids); Ok(pid as libc::pid_t) } container-pid-0.2.0/src/lib.rs000064400000000000000000000052460000000000000142460ustar 00000000000000use libc::pid_t; use simple_error::bail; use std::fmt::Debug; use crate::result::Result; mod cmd; mod command; mod containerd; mod docker; mod kubernetes; mod lxc; mod lxd; mod nspawn; mod podman; mod process_id; mod result; mod rkt; mod vhive; mod vhive_fc_vmid; pub trait Container: Debug { fn lookup(&self, id: &str) -> Result; fn check_required_tools(&self) -> Result<()>; } pub const AVAILABLE_CONTAINER_TYPES: &[&str] = &[ "process_id", "rkt", "podman", "docker", "nspawn", "lxc", "lxd", "command", "containerd", "kubernetes", "vhive", "vhive_fc_vmid", ]; fn default_order() -> Vec> { let containers: Vec> = vec![ Box::new(process_id::ProcessId {}), Box::new(rkt::Rkt {}), Box::new(podman::Podman {}), Box::new(docker::Docker {}), Box::new(nspawn::Nspawn {}), Box::new(lxc::Lxc {}), Box::new(lxd::Lxd {}), Box::new(containerd::Containerd {}), Box::new(kubernetes::Kubernetes {}), Box::new(vhive::Vhive {}), Box::new(vhive_fc_vmid::VhiveFcVmid {}), ]; containers .into_iter() .filter(|c| c.check_required_tools().is_ok()) .collect() } pub fn lookup_container_type(name: &str) -> Option> { Some(match name { "process_id" => Box::new(process_id::ProcessId {}), "rkt" => Box::new(rkt::Rkt {}), "podman" => Box::new(podman::Podman {}), "docker" => Box::new(docker::Docker {}), "nspawn" => Box::new(nspawn::Nspawn {}), "lxc" => Box::new(lxc::Lxc {}), "lxd" => Box::new(lxd::Lxd {}), "containerd" => Box::new(containerd::Containerd {}), "command" => Box::new(command::Command {}), "kubernetes" => Box::new(kubernetes::Kubernetes {}), "vhive" => Box::new(vhive::Vhive {}), "vhive_fc_vmid" => Box::new(vhive_fc_vmid::VhiveFcVmid {}), _ => return None, }) } pub fn lookup_container_pid( container_id: &str, container_types: &[Box], ) -> Result { for c in container_types { c.check_required_tools()?; } let fallback: Vec> = default_order(); let types = if container_types.is_empty() { fallback.as_slice() } else { container_types }; let mut message = String::from("no suitable container found, got the following errors:"); for t in types { match t.lookup(container_id) { Ok(pid) => return Ok(pid), Err(e) => { message += &format!("\n - {:?}: {}", t, e); } }; } bail!("{}", message) } container-pid-0.2.0/src/lxc.rs000064400000000000000000000024770000000000000142710ustar 00000000000000use libc::pid_t; use simple_error::{bail, try_with}; use std::process::Command; use crate::cmd; use crate::result::Result; use crate::Container; #[derive(Clone, Debug)] pub struct Lxc {} impl Container for Lxc { fn lookup(&self, container_id: &str) -> Result { let command = format!("lxc-info --no-humanize --pid --name {}", container_id); let output = try_with!( Command::new("lxc-info") .args(&["--no-humanize", "--pid", "--name", container_id]) .output(), "Running '{}' failed", command ); if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); bail!( "Failed to list containers. '{}' exited with {}: {}", command, output.status, stderr.trim_start() ); } let pid = String::from_utf8_lossy(&output.stdout); Ok(try_with!( pid.trim_start().parse::(), "expected valid process id from {}, got: {}", command, pid )) } fn check_required_tools(&self) -> Result<()> { if cmd::which("lxc-info").is_some() { Ok(()) } else { bail!("lxc-info not found") } } } container-pid-0.2.0/src/lxd.rs000064400000000000000000000034440000000000000142650ustar 00000000000000use libc::pid_t; use simple_error::{bail, try_with}; use std::process::Command; use crate::cmd; use crate::result::Result; use crate::Container; #[derive(Clone, Debug)] pub struct Lxd {} impl Container for Lxd { fn lookup(&self, container_id: &str) -> Result { let command = format!("lxc info {}", container_id); let output = try_with!( Command::new("lxc").args(&["info", container_id]).output(), "Running '{}' failed", command ); if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); bail!( "Failed to list containers. '{}' exited with {}: {}", command, output.status, stderr.trim_end() ); } let lines = output.stdout.split(|&c| c == b'\n'); let mut rows = lines.map(|line| { let cols: Vec<&[u8]> = line.splitn(2, |&c| c == b':').collect(); cols }); if let Some(pid_row) = rows.find(|cols| cols[0] == b"Pid") { assert!(pid_row.len() == 2); let pid = String::from_utf8_lossy(pid_row[1]); Ok(try_with!( pid.trim_start().parse::(), "expected valid process id from {}, got: {}", command, pid )) } else { let stdout = String::from_utf8_lossy(&output.stdout); bail!( "expected to find `pid=` field in output of '{}', got: {}", command, stdout ) } } fn check_required_tools(&self) -> Result<()> { if cmd::which("lxc").is_some() { Ok(()) } else { bail!("lxc not found") } } } container-pid-0.2.0/src/nspawn.rs000064400000000000000000000026620000000000000150050ustar 00000000000000use libc::pid_t; use simple_error::{bail, try_with}; use std::process::Command; use crate::cmd; use crate::result::Result; use crate::Container; #[derive(Clone, Debug)] pub struct Nspawn {} impl Container for Nspawn { fn lookup(&self, container_id: &str) -> Result { let command = format!("machinectl show --property=Leader {}", container_id); let output = try_with!( Command::new("machinectl") .args(&["show", "--property=Leader", container_id]) .output(), "Running '{}' failed", command ); if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); bail!( "Failed to list containers. '{}' exited with {}: {}", command, output.status, stderr.trim_end() ); } let fields: Vec<&[u8]> = output.stdout.splitn(2, |c| *c == b'=').collect(); assert!(fields.len() == 2); let pid = String::from_utf8_lossy(fields[1]); Ok(try_with!( pid.trim_end().parse::(), "expected valid process id from {}, got: {}", command, pid )) } fn check_required_tools(&self) -> Result<()> { if cmd::which("machinectl").is_some() { Ok(()) } else { bail!("machinectl not found") } } } container-pid-0.2.0/src/podman.rs000064400000000000000000000012750000000000000147540ustar 00000000000000use simple_error::bail; use crate::cmd; use crate::docker::parse_docker_output; use crate::result::Result; use crate::Container; #[derive(Clone, Debug)] pub struct Podman {} impl Container for Podman { fn lookup(&self, container_id: &str) -> Result { let cmd = vec![ "podman", "inspect", "--format", "{{.State.Running}};{{.State.Pid}}", container_id, ]; parse_docker_output(cmd.as_slice(), container_id) } fn check_required_tools(&self) -> Result<()> { if cmd::which("podman").is_some() { Ok(()) } else { bail!("podman not found") } } } container-pid-0.2.0/src/process_id.rs000064400000000000000000000021410000000000000156210ustar 00000000000000use libc::pid_t; use simple_error::{bail, try_with}; use std::env; use std::ffi::OsString; use std::fs; use std::io::ErrorKind; use std::path::PathBuf; use crate::result::Result; use crate::Container; #[derive(Clone, Debug)] pub struct ProcessId {} /// TODO make this configureable? fn get_path() -> PathBuf { PathBuf::from(&env::var_os("CNTR_PROC").unwrap_or_else(|| OsString::from("/proc"))) } impl Container for ProcessId { fn lookup(&self, container_id: &str) -> Result { let pid = match container_id.parse::() { Err(e) => try_with!(Err(e), "not a valid pid: `{}`", container_id), Ok(v) => v, }; match fs::metadata(get_path().join(pid.to_string())) { Err(e) => { if e.kind() == ErrorKind::NotFound { bail!("no process with pid {} found", pid) } else { try_with!(Err(e), "could not lookup process {}", pid) } } Ok(_) => Ok(pid), } } fn check_required_tools(&self) -> Result<()> { Ok(()) } } container-pid-0.2.0/src/result.rs000064400000000000000000000001460000000000000150100ustar 00000000000000use simple_error::SimpleError; use std::result; pub type Result = result::Result; container-pid-0.2.0/src/rkt.rs000064400000000000000000000057720000000000000143040ustar 00000000000000use libc::pid_t; use simple_error::{bail, try_with}; use std::fs::{self, File}; use std::io::prelude::*; use std::io::BufReader; use std::process::Command; use crate::cmd; use crate::result::Result; use crate::Container; #[derive(Clone, Debug)] pub struct Rkt {} fn find_child_processes(parent_pid: &str) -> Result { let dir = try_with!(fs::read_dir("/proc"), "failed to read /proc directory"); for entry in dir { let entry = try_with!(entry, "error while reading /proc"); let status_path = entry.path().join("status"); if let Ok(file) = File::open(status_path.clone()) { // ignore if process exits before we can read it let reader = BufReader::new(file); for line in reader.lines() { let line = try_with!(line, "could not read {}", status_path.display()); let columns: Vec<&str> = line.splitn(2, '\t').collect(); assert!(columns.len() == 2); if columns[0] == "PPid:" && columns[1] == parent_pid { let pid = try_with!( entry.file_name().to_string_lossy().parse::(), "read invalid pid from proc: '{}'", columns[1] ); return Ok(pid); } } } } bail!("no child process found for pid {}", parent_pid) } impl Container for Rkt { fn lookup(&self, container_id: &str) -> Result { let command = format!("rkt status {}", container_id); let output = try_with!( Command::new("rkt").args(&["status", container_id]).output(), "Running '{}' failed", command ); if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); bail!( "Failed to list containers. '{}' exited with {}: {}", command, output.status, stderr.trim_end() ); } let lines = output.stdout.split(|&c| c == b'\n'); let mut rows = lines.map(|line| { let cols: Vec<&[u8]> = line.splitn(2, |&c| c == b'=').collect(); cols }); if let Some(pid_row) = rows.find(|cols| cols[0] == b"pid") { assert!(pid_row.len() == 2); let ppid = String::from_utf8_lossy(pid_row[1]); Ok(try_with!( find_child_processes(&ppid), "could not find container process belonging to rkt container '{}'", container_id )) } else { let stdout = String::from_utf8_lossy(&output.stdout); bail!( "expected to find `pid=` field in output of '{}', got: {}", command, stdout ) } } fn check_required_tools(&self) -> Result<()> { if cmd::which("rkt").is_some() { Ok(()) } else { bail!("rkt not found") } } } container-pid-0.2.0/src/vhive.rs000064400000000000000000000062030000000000000146130ustar 00000000000000//! This module uses `kubectl` to get a containerd id, uses `journalctl` to match that id to a //! firecracker vm id and finally searches the open file-descriptors of relevant processes for one //! belonging to that vm id. //! //! The user may supply the same `container_id` as for the kubnetes module, the container always //! defaults to `user-container` because that is where the firecracker vm resides in vhive. //! //! Requires: //! - journald size longer than container lifetime //! - vhive with "debug log containerid->vmid" patch use crate::cmd; use crate::kubernetes as k8s; use crate::result::Result; use crate::vhive_fc_vmid::find_fc_pid; use crate::Container; use simple_error::{bail, require_with, try_with}; use std::process::Command; use std::str::from_utf8; #[derive(Clone, Debug)] pub struct Vhive {} const DEFAULT_CONTAINER: Option<&str> = Some("user-container"); impl Container for Vhive { fn lookup(&self, container_id: &str) -> Result { let (namespace, pod_name, _) = try_with!( k8s::parse_userinput(container_id), "cannot parse user given container_id" ); let containerdid = try_with!( k8s::get_containerd_id(namespace, pod_name, DEFAULT_CONTAINER), "containerd id lookup failed" ); let fcvmid = try_with!( get_fcvmid(&containerdid), "cannot get firecracker vm id for containerd://{}", containerdid ); let pid = try_with!( find_fc_pid(&fcvmid), "cannot find pid for firecracker vmID {}", fcvmid ); Ok(pid) } fn check_required_tools(&self) -> Result<()> { if cmd::which("kubectl").is_none() { bail!("kubectl not found") } if cmd::which("journalctl").is_none() { bail!("journalctl not found") } Ok(()) } } /// get firecracker vm id fn get_fcvmid(containerd_id: &str) -> Result { // get lines from journalctl let keyword = format!("user-containerID={}", containerd_id); let arg = format!("--grep={}", keyword); let result = try_with!( Command::new("journalctl") .arg("-u") .arg("vhive") .arg("--no-pager") .arg("--boot=0") // after a reboot all firecracker VMs of vhive stay dead .arg("--reverse") .arg("-o") .arg("cat") .arg(&arg) .output(), "cannot start journalctl" ); if !result.status.success() { let stderr = String::from_utf8_lossy(&result.stderr); let stdout = String::from_utf8_lossy(&result.stdout); bail!( "No such container? searching journal failed (ret code {:?}): {}, {}", result.status.code(), stderr, stdout ); } // parse lines let first = try_with!(from_utf8(&result.stdout), "journal contains non-utf8") .splitn(2, '\n') .collect::>()[0]; // first line let vmid = require_with!( first.split(' ').find_map(|kv| kv.strip_prefix("vmID=")), "foo" ); Ok(String::from(vmid)) } container-pid-0.2.0/src/vhive_fc_vmid.rs000064400000000000000000000045550000000000000163120ustar 00000000000000//! This module takes a firecracker vm id and searches a vhive setup for the open file-descriptors //! of relevant processes for one belonging to that vm id. use crate::result::Result; use crate::Container; use simple_error::{bail, try_with}; use std::fs; use std::path::PathBuf; use std::str::FromStr; #[derive(Clone, Debug)] pub struct VhiveFcVmid {} impl Container for VhiveFcVmid { fn lookup(&self, container_id: &str) -> Result { let pid = try_with!( find_fc_pid(&container_id), "cannot find pid for firecracker vmID {}", container_id ); Ok(pid) } fn check_required_tools(&self) -> Result<()> { Ok(()) } } /// search which process has known_file open pub fn find_fc_pid(vmid: &str) -> Result { let known_file = PathBuf::from(format!("/tmp/log_{}_start.logs", vmid)); // go trough all processes and seach for name=*firecracker* and open_filedescriptor=known_file let procs = PathBuf::from("/proc"); for proc in try_with!(std::fs::read_dir(&procs), "cannot list {:?}", procs) { let proc = try_with!(proc, "cannot read entry in dir {:?}", procs); let pid = proc.file_name(); let pid = pid.as_os_str().to_string_lossy(); let pid: u64 = match u64::from_str(&pid) { Ok(pid) => pid, Err(_) => continue, // skip proc, if not a proc }; // heuristic to continue early (~5%/10ms speedup) let cmdline = try_with!( fs::read(proc.path().join("cmdline")), "cannot read cmdline of process {}", pid ); let cmdline = String::from_utf8_lossy(&cmdline); if !cmdline.contains("firecracker") { continue; } // search fds let fds = proc.path().join("fd"); for fd in try_with!(std::fs::read_dir(&fds), "cannot list {:?}", &fds) { let fd = try_with!(fd, "cannot read entry in dir {:?}", &fds); // symlink dst = file that is open for this proc let open_file = try_with!( fs::read_link(fd.path()), "cannot read symlink {:?}", fd.path() ); if open_file == known_file { return Ok(pid as libc::pid_t); } } } bail!("no process found for firecracker vm id {}", vmid); }