From 5af16b0efaf9ab66025cb66572ddc64d508655dd Mon Sep 17 00:00:00 2001 From: amy null Date: Fri, 25 Mar 2022 17:08:35 -0400 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20feature:=20WIP=20driver?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test-unit.yaml | 25 ++++ .vscode/launch.json | 138 ++++++++++++++++++++++ Cargo.lock | 59 ++++++++++ daemon/Cargo.toml | 4 + daemon/src/engine/driver/cgroup.rs | 179 +++++++++++++++++++++++++++++ daemon/src/engine/driver/dbus.rs | 156 +++++++++++++++++++++++++ daemon/src/engine/driver/mod.rs | 2 + daemon/src/engine/mod.rs | 1 + daemon/src/util/mod.rs | 18 +++ 9 files changed, 582 insertions(+) create mode 100644 .github/workflows/test-unit.yaml create mode 100644 .vscode/launch.json create mode 100644 daemon/src/engine/driver/cgroup.rs create mode 100644 daemon/src/engine/driver/dbus.rs create mode 100644 daemon/src/engine/driver/mod.rs diff --git a/.github/workflows/test-unit.yaml b/.github/workflows/test-unit.yaml new file mode 100644 index 0000000..254e56a --- /dev/null +++ b/.github/workflows/test-unit.yaml @@ -0,0 +1,25 @@ +name: Run all tests on latest stable Rust +on: + push: + branches: + - mistress + pull_request: + branches: + - mistress + +jobs: + run-tests: + if: github.actor != 'dependabot' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install latest stable Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - uses: Swatinem/rust-cache@v1 + with: + key: unit + - name: Run unit tests + run: cargo test diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..08098e5 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,138 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'libsquish'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=libsquish" + ], + "filter": { + "name": "libsquish", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'daemon'", + "cargo": { + "args": [ + "build", + "--bin=daemon", + "--package=daemon" + ], + "filter": { + "name": "daemon", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'daemon'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=daemon", + "--package=daemon" + ], + "filter": { + "name": "daemon", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'cli'", + "cargo": { + "args": [ + "build", + "--bin=cli", + "--package=cli" + ], + "filter": { + "name": "cli", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'cli'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=cli", + "--package=cli" + ], + "filter": { + "name": "cli", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'pid1'", + "cargo": { + "args": [ + "build", + "--bin=pid1", + "--package=pid1" + ], + "filter": { + "name": "pid1", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'pid1'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=pid1", + "--package=pid1" + ], + "filter": { + "name": "pid1", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f8c47df..944fce9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,28 @@ dependencies = [ "memchr", ] +[[package]] +name = "async-recursion" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atty" version = "0.2.14" @@ -161,6 +183,10 @@ dependencies = [ name = "daemon" version = "0.1.0" dependencies = [ + "async-recursion", + "async-trait", + "dbus", + "dbus-tokio", "flate2", "haikunator", "hex", @@ -180,6 +206,30 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "dbus" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0a745c25b32caa56b82a3950f5fec7893a960f4c10ca3b02060b0c38d8c2ce" +dependencies = [ + "futures-channel", + "futures-util", + "libc", + "libdbus-sys", + "winapi", +] + +[[package]] +name = "dbus-tokio" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12a1a74a0c53b22d7d994256cf3ecbeefa5eedce3cf9d362945ac523c4132180" +dependencies = [ + "dbus", + "libc", + "tokio", +] + [[package]] name = "derive-getters" version = "0.2.0" @@ -635,6 +685,15 @@ version = "0.2.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103" +[[package]] +name = "libdbus-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c185b5b7ad900923ef3a8ff594083d4d9b5aea80bb4f32b8342363138c0d456b" +dependencies = [ + "pkg-config", +] + [[package]] name = "libsquish" version = "0.1.0" diff --git a/daemon/Cargo.toml b/daemon/Cargo.toml index b438254..16a182d 100644 --- a/daemon/Cargo.toml +++ b/daemon/Cargo.toml @@ -9,7 +9,11 @@ edition = "2018" [dependencies] libsquish = { path = "../libsquish" } +async-recursion = "1.0.0" +async-trait = "0.1.52" flate2 = "1.0.22" +dbus-tokio = "0.7.5" +dbus = "0.9.5" haikunator = "0.1.2" hex = "0.4.3" hmac-sha256 = "1.1.2" diff --git a/daemon/src/engine/driver/cgroup.rs b/daemon/src/engine/driver/cgroup.rs new file mode 100644 index 0000000..1110484 --- /dev/null +++ b/daemon/src/engine/driver/cgroup.rs @@ -0,0 +1,179 @@ +use crate::util::{Result, SquishError}; + +use std::path::Path; + +use async_recursion::async_recursion; +use tokio::fs; + +/// Detects the current cgroup. This is done by reading `/proc/self/cgroup` and +/// parsing out the cgroup's name. +/// +/// The cgroup name we get out is formatted as such: +/// +/// ``` +/// hierarchy-ID:controller-list:cgroup-path +/// ``` +/// +/// For example: +/// +/// ``` +/// 5:cpuacct,cpu,cpuset:/daemons +/// ``` +/// +/// For more information, see [here](https://man7.org/linux/man-pages/man7/cgroups.7.html) +/// and search for "hierarchy-ID:controller-list:cgroup-path". +pub async fn detect_current_cgroup() -> Result { + let cgroup_info = fs::read_to_string("/proc/self/cgroup").await?; + let cgroup: Vec<&str> = cgroup_info.trim().split(':').collect(); + Ok(cgroup[2].to_string()) +} + +/// The path to the current cgroup on the filesystem. +pub async fn current_cgroup_path() -> Result { + Ok(format!("/sys/fs/cgroup{}", detect_current_cgroup().await?)) +} + +/// Detect the current cgroup slice name, if possible. +pub async fn detect_current_cgroup_slice() -> Result { + let cgroup = detect_current_cgroup().await?; + Ok(detect_current_cgroup_cgroup_slice_recursive(&cgroup)?) +} + +fn detect_current_cgroup_cgroup_slice_recursive(cgroup: &String) -> Result { + // TODO: Rewrite this to use Path + let mut iter = cgroup.split('/').rev(); + if let Some(item) = iter.next() { + let mut parts: Vec = vec![]; + if item.ends_with(".slice") { + parts.push(item.to_string()); + for item in &mut iter { + parts.push(item.to_string()); + } + parts.reverse(); + Ok(parts.join("/")) + } else { + for item in &mut iter { + parts.push(item.to_string()); + } + parts.reverse(); + detect_current_cgroup_cgroup_slice_recursive(&parts.join("/")) + } + } else { + Err(Box::new(SquishError::CgroupNoMoreSlices)) + } +} + +pub async fn detect_current_cgroup_slice_name() -> Result { + let cgroup = detect_current_cgroup_slice().await?; + let part = cgroup.split('/').rev().next(); + match part { + Some(part) => Ok(part.to_string()), + None => Err(Box::new(SquishError::CgroupNoMoreSlices)), + } +} + +/// The different types of cgroup controller. Some require privileges. +/// See also: https://wiki.archlinux.org/title/Cgroups#Controller_types +#[allow(dead_code)] +#[derive(Debug, PartialEq, Eq)] +pub enum Controller { + Cpu, + Cpuset, + Freezer, + Hugetlb, + Io, + Memory, + PerfEvent, + Pids, + Rdma, +} + +/// Detect all cgroup controller delegations for the current cgroup. We get the +/// current cgroup, then read delegations out of +/// `/sys/fs/cgroup/[cgroup]/cgroup.controllers`. If this file doesn't exist, +/// we recurse up the directory tree until we find valid cgroup controllers or +/// we've run out of the cgroup sysfs. +pub async fn detect_cgroup_controller_delegations() -> Result> { + detect_cgroup_controller_delegations_recursive(current_cgroup_path().await?).await +} + +#[async_recursion] +async fn detect_cgroup_controller_delegations_recursive(path: String) -> Result> { + let path = format!("{}/cgroup.controllers", path); + let exists = Path::new(&path).exists(); + if exists { + let delegations = fs::read_to_string(path).await?; + parse_cgroup_controller_delegations(delegations) + } else { + let mut split: Vec = path.split('/').map(|s| s.to_string()).collect(); + split.truncate(split.len() - 1); + detect_cgroup_controller_delegations_recursive(split.join("/")).await + } +} + +fn parse_cgroup_controller_delegations>(delegations: T) -> Result> { + let delegations: String = delegations.into(); + let delegations: Vec = delegations + .split_whitespace() + .map(|d| delegation_to_controller(d).unwrap()) + .collect(); + Ok(delegations) +} + +fn delegation_to_controller>(delegation: T) -> Result { + match delegation.into().as_ref() { + "cpu" => Ok(Controller::Cpu), + "cpuset" => Ok(Controller::Cpuset), + "freezer" => Ok(Controller::Freezer), + "hugetlb" => Ok(Controller::Hugetlb), + "io" => Ok(Controller::Io), + "memory" => Ok(Controller::Memory), + "perf_event" => Ok(Controller::PerfEvent), + "pids" => Ok(Controller::Pids), + "rdma" => Ok(Controller::Rdma), + _ => Err(Box::new(SquishError::CgroupDelegationInvalid)), + } +} + +#[cfg(test)] +mod test { + use super::Controller; + use nix::unistd::getuid; + + #[tokio::test] + pub async fn detects_current_cgroup() { + let uid = getuid().as_raw(); + assert_eq!( + format!("/user.slice/user-{}.slice/session-1.scope", uid), + super::detect_current_cgroup().await.unwrap() + ); + } + + #[tokio::test] + pub async fn parses_cgroup_delegations() { + assert_eq!( + vec![Controller::Memory, Controller::Pids], + super::parse_cgroup_controller_delegations("memory pids").unwrap() + ); + } + + #[tokio::test] + pub async fn parses_cgroup_delegations_from_fs() { + // memory and pids will always be valid controller delegations + let delegations = super::detect_cgroup_controller_delegations().await.unwrap(); + assert!(delegations.contains(&Controller::Memory)); + assert!(delegations.contains(&Controller::Pids)); + } + + #[tokio::test] + pub async fn test_cgroup_slice_detection() { + let slice = super::detect_current_cgroup_slice().await.unwrap(); + assert_eq!("/user.slice/user-1000.slice", slice); + } + + #[tokio::test] + pub async fn test_cgroup_slice_name_detection() { + let slice = super::detect_current_cgroup_slice_name().await.unwrap(); + assert_eq!("user-1000.slice", slice); + } +} diff --git a/daemon/src/engine/driver/dbus.rs b/daemon/src/engine/driver/dbus.rs new file mode 100644 index 0000000..453065d --- /dev/null +++ b/daemon/src/engine/driver/dbus.rs @@ -0,0 +1,156 @@ +use super::cgroup; +use crate::util::Result; + +use std::sync::Arc; +use std::time::Duration; + +use dbus::arg::{AppendAll, ReadAll, RefArg, Variant}; +use dbus::nonblock::{Proxy, SyncConnection}; + +pub struct DbusDriver { + conn: Arc, +} + +impl DbusDriver { + pub async fn new() -> Result { + use dbus_tokio::connection; + + let (resource, conn) = connection::new_session_sync()?; + + let _handle = tokio::spawn(async { + let err = resource.await; + panic!("Lost connection to D-Bus: {}", err); + }); + + Ok(Self { conn }) + } + + async fn method_call<'a, S, A, R>( + &self, + dest: S, + path: S, + interface: S, + method: S, + args: A, + ) -> Result + where + S: Into<&'a str>, + A: AppendAll, + R: ReadAll + 'static, + { + let proxy = Proxy::new( + dest.into(), + path.into(), + Duration::from_secs(2), + self.conn.clone(), + ); + let result = proxy + .method_call(interface.into(), method.into(), args) + .await?; + Ok(result) + } +} + +#[async_trait::async_trait] +trait SystemdDbusDriver + Send + Sync + 'static> { + async fn get_unit(&self, name: IS) -> Result; + async fn start_transient_unit( + &self, + name: IS, + description: IS, + pids: Vec, + ) -> Result; + async fn stop_unit(&self, name: IS, mode: IS) -> Result; +} + +#[async_trait::async_trait] +impl + Send + Sync + 'static> SystemdDbusDriver for DbusDriver { + async fn get_unit(&self, name: IS) -> Result { + let dbus_path: (dbus::Path<'static>,) = self + .method_call( + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + "GetUnit", + (name.into(),), + ) + .await?; + Ok(dbus_path.0.to_string()) + } + + async fn start_transient_unit( + &self, + name: IS, + description: IS, + pids: Vec, + ) -> Result { + let properties: Vec<(&str, Variant>)> = vec![ + ( + "Slice", + Variant(Box::new(cgroup::detect_current_cgroup_slice_name().await?)), + ), + ("Delegate", Variant(Box::new(true))), + ("PIDs", Variant(Box::new(pids.clone()))), + ("Description", Variant(Box::new(description.into()))), + ]; + #[allow(clippy::type_complexity)] + let aux: Vec<(&str, Vec<(&str, Variant>)>)> = vec![]; + let dbus_path: (dbus::Path<'static>,) = self + .method_call( + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + "StartTransientUnit", + (name.into(), "replace", properties, aux), + ) + .await?; + Ok(dbus_path.0.to_string()) + } + + async fn stop_unit(&self, name: IS, mode: IS) -> Result { + let dbus_path: (dbus::Path<'static>,) = self + .method_call( + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + "StopUnit", + (name.into(), mode.into()), + ) + .await?; + Ok(dbus_path.0.to_string()) + } +} + +#[cfg(test)] +mod test { + use super::{DbusDriver, SystemdDbusDriver}; + use crate::util::Result; + + const UNIT: &str = "squish-test.scope"; + + #[tokio::test] + pub async fn test_transient_unit_functionality() -> Result<()> { + let driver = DbusDriver::new().await?; + + let path = driver.get_unit(UNIT).await; + assert!(path.is_err()); + dbg!(&path); + + let path = driver + .start_transient_unit(UNIT, "gay", vec![nix::unistd::getpid().as_raw()]) + .await; + dbg!(&path); + assert!(path.is_ok()); + let path = path?; + dbg!(&path); + // assert_eq!("", path); + + let path = driver.get_unit(UNIT).await; + assert!(path.is_ok()); + let path = path?; + dbg!(&path); + + driver.stop_unit(UNIT, "replace").await?; + Ok(()) + } +} diff --git a/daemon/src/engine/driver/mod.rs b/daemon/src/engine/driver/mod.rs new file mode 100644 index 0000000..f1ec2e3 --- /dev/null +++ b/daemon/src/engine/driver/mod.rs @@ -0,0 +1,2 @@ +pub mod cgroup; +pub mod dbus; diff --git a/daemon/src/engine/mod.rs b/daemon/src/engine/mod.rs index 1e50ebf..4f96974 100644 --- a/daemon/src/engine/mod.rs +++ b/daemon/src/engine/mod.rs @@ -1,5 +1,6 @@ pub mod alpine; pub mod containers; +pub mod driver; pub mod slirp; use std::error::Error; diff --git a/daemon/src/util/mod.rs b/daemon/src/util/mod.rs index 1aa221e..4f6978b 100644 --- a/daemon/src/util/mod.rs +++ b/daemon/src/util/mod.rs @@ -1,5 +1,8 @@ +use std::any::{Any, TypeId}; use std::{error::Error, fmt::Display}; +pub type Result = std::result::Result>; + #[derive(Debug)] pub enum SquishError { GenericError(Box), @@ -9,6 +12,9 @@ pub enum SquishError { AlpineManifestInvalid, AlpineManifestMissing, AlpineManifestFileMissing, + + CgroupDelegationInvalid, + CgroupNoMoreSlices, } impl Display for SquishError { @@ -20,3 +26,15 @@ impl Display for SquishError { impl warp::reject::Reject for SquishError {} impl Error for SquishError {} + +// https://stackoverflow.com/a/52005668 +pub trait SameType +where + Self: Any, +{ + fn same_type(&self) -> bool { + TypeId::of::() == TypeId::of::() + } +} + +impl SameType for T {} From 765c1dd5c7fc9b9e5133c7ac87654f0646cff2ec Mon Sep 17 00:00:00 2001 From: amy null Date: Sat, 26 Mar 2022 14:29:55 -0400 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=9A=A7=20chore:=20Port=20to=20libsqui?= =?UTF-8?q?sh=20Result?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- daemon/src/engine/driver/cgroup.rs | 3 ++- daemon/src/engine/driver/dbus.rs | 16 ++++++++++++---- daemon/src/util/mod.rs | 2 -- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/daemon/src/engine/driver/cgroup.rs b/daemon/src/engine/driver/cgroup.rs index 1110484..51ed8c7 100644 --- a/daemon/src/engine/driver/cgroup.rs +++ b/daemon/src/engine/driver/cgroup.rs @@ -1,8 +1,9 @@ -use crate::util::{Result, SquishError}; +use crate::util::SquishError; use std::path::Path; use async_recursion::async_recursion; +use libsquish::Result; use tokio::fs; /// Detects the current cgroup. This is done by reading `/proc/self/cgroup` and diff --git a/daemon/src/engine/driver/dbus.rs b/daemon/src/engine/driver/dbus.rs index 453065d..fc6ad17 100644 --- a/daemon/src/engine/driver/dbus.rs +++ b/daemon/src/engine/driver/dbus.rs @@ -1,11 +1,11 @@ use super::cgroup; -use crate::util::Result; use std::sync::Arc; use std::time::Duration; use dbus::arg::{AppendAll, ReadAll, RefArg, Variant}; use dbus::nonblock::{Proxy, SyncConnection}; +use libsquish::Result; pub struct DbusDriver { conn: Arc, @@ -90,8 +90,16 @@ impl + Send + Sync + 'static> SystemdDbusDriver for DbusDri Variant(Box::new(cgroup::detect_current_cgroup_slice_name().await?)), ), ("Delegate", Variant(Box::new(true))), - ("PIDs", Variant(Box::new(pids.clone()))), + // ("PIDs", Variant(Box::new(pids.clone()))), ("Description", Variant(Box::new(description.into()))), + ( + "ExecStart", + Variant(Box::new(( + "ls".to_string(), + vec!["ls".to_string(), "-lah".to_string()], + true, + ))), + ), ]; #[allow(clippy::type_complexity)] let aux: Vec<(&str, Vec<(&str, Variant>)>)> = vec![]; @@ -124,9 +132,9 @@ impl + Send + Sync + 'static> SystemdDbusDriver for DbusDri #[cfg(test)] mod test { use super::{DbusDriver, SystemdDbusDriver}; - use crate::util::Result; + use libsquish::Result; - const UNIT: &str = "squish-test.scope"; + const UNIT: &str = "squish-test.service"; #[tokio::test] pub async fn test_transient_unit_functionality() -> Result<()> { diff --git a/daemon/src/util/mod.rs b/daemon/src/util/mod.rs index 4f6978b..0020dbb 100644 --- a/daemon/src/util/mod.rs +++ b/daemon/src/util/mod.rs @@ -1,8 +1,6 @@ use std::any::{Any, TypeId}; use std::{error::Error, fmt::Display}; -pub type Result = std::result::Result>; - #[derive(Debug)] pub enum SquishError { GenericError(Box),