From b8031f1a2166de0078bdeb34753492c9b17c7e4d Mon Sep 17 00:00:00 2001 From: Xuewei Niu Date: Tue, 1 Jul 2025 10:25:51 +0800 Subject: [PATCH 1/6] fs: Move all files to fs subdirectory Prepare to implement FsManager, which should be done in the following-up commits. Apart from that, no code is not modified. Signed-off-by: Xuewei Niu --- src/{ => fs}/blkio.rs | 14 +- src/{ => fs}/cgroup.rs | 12 +- src/{ => fs}/cgroup_builder.rs | 14 +- src/{ => fs}/cpu.rs | 8 +- src/{ => fs}/cpuacct.rs | 8 +- src/{ => fs}/cpuset.rs | 10 +- src/{ => fs}/devices.rs | 6 +- src/{ => fs}/error.rs | 0 src/{ => fs}/events.rs | 4 +- src/{ => fs}/freezer.rs | 6 +- src/{ => fs}/hierarchies.rs | 32 +- src/{ => fs}/hugetlb.rs | 8 +- src/{ => fs}/memory.rs | 14 +- src/fs/mod.rs | 1032 ++++++++++++++++++++++++++++++++ src/{ => fs}/net_cls.rs | 8 +- src/{ => fs}/net_prio.rs | 8 +- src/{ => fs}/perf_event.rs | 4 +- src/{ => fs}/pid.rs | 8 +- src/{ => fs}/rdma.rs | 8 +- src/{ => fs}/systemd.rs | 4 +- src/lib.rs | 1029 +------------------------------ tests/builder.rs | 32 +- tests/cgroup.rs | 42 +- tests/cpu.rs | 6 +- tests/cpuset.rs | 16 +- tests/devices.rs | 8 +- tests/hugetlb.rs | 10 +- tests/memory.rs | 11 +- tests/pids.rs | 17 +- tests/resources.rs | 6 +- 30 files changed, 1195 insertions(+), 1190 deletions(-) rename src/{ => fs}/blkio.rs (99%) rename src/{ => fs}/cgroup.rs (98%) rename src/{ => fs}/cgroup_builder.rs (97%) rename src/{ => fs}/cpu.rs (98%) rename src/{ => fs}/cpuacct.rs (96%) rename src/{ => fs}/cpuset.rs (99%) rename src/{ => fs}/devices.rs (99%) rename src/{ => fs}/error.rs (100%) rename src/{ => fs}/events.rs (97%) rename src/{ => fs}/freezer.rs (96%) rename src/{ => fs}/hierarchies.rs (94%) rename src/{ => fs}/hugetlb.rs (98%) rename src/{ => fs}/memory.rs (99%) create mode 100644 src/fs/mod.rs rename src/{ => fs}/net_cls.rs (96%) rename src/{ => fs}/net_prio.rs (97%) rename src/{ => fs}/perf_event.rs (94%) rename src/{ => fs}/pid.rs (97%) rename src/{ => fs}/rdma.rs (93%) rename src/{ => fs}/systemd.rs (93%) diff --git a/src/blkio.rs b/src/fs/blkio.rs similarity index 99% rename from src/blkio.rs rename to src/fs/blkio.rs index 452482b..66aff98 100644 --- a/src/blkio.rs +++ b/src/fs/blkio.rs @@ -11,11 +11,11 @@ use std::io::Write; use std::path::PathBuf; -use crate::error::ErrorKind::*; -use crate::error::*; +use crate::fs::error::ErrorKind::*; +use crate::fs::error::*; -use crate::{read_string_from, read_u64_from}; -use crate::{ +use crate::fs::{read_string_from, read_u64_from}; +use crate::fs::{ BlkIoResources, ControllIdentifier, ControllerInternal, Controllers, CustomizedAttribute, Resources, Subsystem, }; @@ -829,9 +829,9 @@ impl BlkIoController { impl CustomizedAttribute for BlkIoController {} #[cfg(test)] mod test { - use crate::blkio::{parse_blkio_data, BlkIoData}; - use crate::blkio::{parse_io_service, parse_io_service_total, IoService}; - use crate::error::*; + use crate::fs::blkio::{parse_blkio_data, BlkIoData}; + use crate::fs::blkio::{parse_io_service, parse_io_service_total, IoService}; + use crate::fs::error::*; static TEST_VALUE: &str = "\ 8:32 Read 4280320 diff --git a/src/cgroup.rs b/src/fs/cgroup.rs similarity index 98% rename from src/cgroup.rs rename to src/fs/cgroup.rs index 6e0b9d0..ef9aade 100644 --- a/src/cgroup.rs +++ b/src/fs/cgroup.rs @@ -6,11 +6,11 @@ //! This module handles cgroup operations. Start here! -use crate::error::ErrorKind::*; -use crate::error::*; +use crate::fs::error::ErrorKind::*; +use crate::fs::error::*; -use crate::hierarchies::V1; -use crate::{CgroupPid, ControllIdentifier, Controller, Hierarchy, Resources, Subsystem}; +use crate::fs::hierarchies::V1; +use crate::fs::{CgroupPid, ControllIdentifier, Controller, Hierarchy, Resources, Subsystem}; use std::collections::HashMap; use std::convert::From; @@ -51,7 +51,7 @@ impl Clone for Cgroup { fn clone(&self) -> Self { Cgroup { subsystems: self.subsystems.clone(), - hier: crate::hierarchies::auto(), + hier: crate::fs::hierarchies::auto(), path: self.path.clone(), specified_controllers: None, } @@ -62,7 +62,7 @@ impl Default for Cgroup { fn default() -> Self { Cgroup { subsystems: Vec::new(), - hier: crate::hierarchies::auto(), + hier: crate::fs::hierarchies::auto(), path: "".to_string(), specified_controllers: None, } diff --git a/src/cgroup_builder.rs b/src/fs/cgroup_builder.rs similarity index 97% rename from src/cgroup_builder.rs rename to src/fs/cgroup_builder.rs index a453d04..094e3d1 100644 --- a/src/cgroup_builder.rs +++ b/src/fs/cgroup_builder.rs @@ -16,10 +16,10 @@ //! by a call to `build()`. //! //! ```rust,no_run -//! # use cgroups_rs::*; -//! # use cgroups_rs::devices::*; -//! # use cgroups_rs::cgroup_builder::*; -//! let h = cgroups_rs::hierarchies::auto(); +//! # use cgroups_rs::fs::*; +//! # use cgroups_rs::fs::devices::*; +//! # use cgroups_rs::fs::cgroup_builder::*; +//! let h = cgroups_rs::fs::hierarchies::auto(); //! let cgroup: Cgroup = CgroupBuilder::new("hello") //! .memory() //! .kernel_memory_limit(1024 * 1024) @@ -60,7 +60,7 @@ //! .build(h).unwrap(); //! ``` -use crate::{ +use crate::fs::{ BlkIoDeviceResource, BlkIoDeviceThrottleResource, Cgroup, DeviceResource, Error, Hierarchy, HugePageResource, MaxValue, NetworkPriority, Resources, }; @@ -250,9 +250,9 @@ impl DeviceResourceBuilder { mut self, major: i64, minor: i64, - devtype: crate::devices::DeviceType, + devtype: crate::fs::devices::DeviceType, allow: bool, - access: Vec, + access: Vec, ) -> DeviceResourceBuilder { self.cgroup.resources.devices.devices.push(DeviceResource { allow, diff --git a/src/cpu.rs b/src/fs/cpu.rs similarity index 98% rename from src/cpu.rs rename to src/fs/cpu.rs index 50a334d..468ac5f 100644 --- a/src/cpu.rs +++ b/src/fs/cpu.rs @@ -13,11 +13,11 @@ use std::fs::File; use std::io::{Read, Write}; use std::path::PathBuf; -use crate::error::ErrorKind::*; -use crate::error::*; -use crate::{parse_max_value, read_i64_from, read_u64_from}; +use crate::fs::error::ErrorKind::*; +use crate::fs::error::*; +use crate::fs::{parse_max_value, read_i64_from, read_u64_from}; -use crate::{ +use crate::fs::{ ControllIdentifier, ControllerInternal, Controllers, CpuResources, CustomizedAttribute, MaxValue, Resources, Subsystem, }; diff --git a/src/cpuacct.rs b/src/fs/cpuacct.rs similarity index 96% rename from src/cpuacct.rs rename to src/fs/cpuacct.rs index ee6ba3e..aa67d37 100644 --- a/src/cpuacct.rs +++ b/src/fs/cpuacct.rs @@ -10,11 +10,11 @@ use std::io::Write; use std::path::PathBuf; -use crate::error::ErrorKind::*; -use crate::error::*; +use crate::fs::error::ErrorKind::*; +use crate::fs::error::*; -use crate::{read_string_from, read_u64_from}; -use crate::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem}; +use crate::fs::{read_string_from, read_u64_from}; +use crate::fs::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem}; /// A controller that allows controlling the `cpuacct` subsystem of a Cgroup. /// diff --git a/src/cpuset.rs b/src/fs/cpuset.rs similarity index 99% rename from src/cpuset.rs rename to src/fs/cpuset.rs index 5a9e7dc..71435a9 100644 --- a/src/cpuset.rs +++ b/src/fs/cpuset.rs @@ -13,11 +13,11 @@ use log::*; use std::io::Write; use std::path::PathBuf; -use crate::error::ErrorKind::*; -use crate::error::*; +use crate::fs::error::ErrorKind::*; +use crate::fs::error::*; -use crate::{read_string_from, read_u64_from}; -use crate::{ +use crate::fs::{read_string_from, read_u64_from}; +use crate::fs::{ ControllIdentifier, ControllerInternal, Controllers, CpuResources, Resources, Subsystem, }; @@ -591,7 +591,7 @@ impl CpuSetController { #[cfg(test)] mod tests { - use crate::cpuset; + use crate::fs::cpuset; #[test] fn test_parse_range() { let test_cases = vec![ diff --git a/src/devices.rs b/src/fs/devices.rs similarity index 99% rename from src/devices.rs rename to src/fs/devices.rs index 56c11c5..02a0cf3 100644 --- a/src/devices.rs +++ b/src/fs/devices.rs @@ -12,10 +12,10 @@ use std::path::PathBuf; use log::*; -use crate::error::ErrorKind::*; -use crate::error::*; +use crate::fs::error::ErrorKind::*; +use crate::fs::error::*; -use crate::{ +use crate::fs::{ ControllIdentifier, ControllerInternal, Controllers, DeviceResource, DeviceResources, Resources, Subsystem, }; diff --git a/src/error.rs b/src/fs/error.rs similarity index 100% rename from src/error.rs rename to src/fs/error.rs diff --git a/src/events.rs b/src/fs/events.rs similarity index 97% rename from src/events.rs rename to src/fs/events.rs index 65506c0..af93db0 100644 --- a/src/events.rs +++ b/src/fs/events.rs @@ -12,8 +12,8 @@ use std::path::Path; use std::sync::mpsc::{self, Receiver}; use std::thread; -use crate::error::ErrorKind::*; -use crate::error::*; +use crate::fs::error::ErrorKind::*; +use crate::fs::error::*; // notify_on_oom returns channel on which you can expect event about OOM, // if process died without OOM this channel will be closed. diff --git a/src/freezer.rs b/src/fs/freezer.rs similarity index 96% rename from src/freezer.rs rename to src/fs/freezer.rs index 0b1bf6c..907dffb 100644 --- a/src/freezer.rs +++ b/src/fs/freezer.rs @@ -11,10 +11,10 @@ use std::io::{Read, Write}; use std::path::PathBuf; -use crate::error::ErrorKind::*; -use crate::error::*; +use crate::fs::error::ErrorKind::*; +use crate::fs::error::*; -use crate::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem}; +use crate::fs::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem}; /// A controller that allows controlling the `freezer` subsystem of a Cgroup. /// diff --git a/src/hierarchies.rs b/src/fs/hierarchies.rs similarity index 94% rename from src/hierarchies.rs rename to src/fs/hierarchies.rs index 3c9911c..b0fb1c3 100644 --- a/src/hierarchies.rs +++ b/src/fs/hierarchies.rs @@ -11,23 +11,23 @@ use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; -use crate::blkio::BlkIoController; -use crate::cpu::CpuController; -use crate::cpuacct::CpuAcctController; -use crate::cpuset::CpuSetController; -use crate::devices::DevicesController; -use crate::freezer::FreezerController; -use crate::hugetlb::HugeTlbController; -use crate::memory::MemController; -use crate::net_cls::NetClsController; -use crate::net_prio::NetPrioController; -use crate::perf_event::PerfEventController; -use crate::pid::PidController; -use crate::rdma::RdmaController; -use crate::systemd::SystemdController; -use crate::{Controllers, Hierarchy, Subsystem}; +use crate::fs::blkio::BlkIoController; +use crate::fs::cpu::CpuController; +use crate::fs::cpuacct::CpuAcctController; +use crate::fs::cpuset::CpuSetController; +use crate::fs::devices::DevicesController; +use crate::fs::freezer::FreezerController; +use crate::fs::hugetlb::HugeTlbController; +use crate::fs::memory::MemController; +use crate::fs::net_cls::NetClsController; +use crate::fs::net_prio::NetPrioController; +use crate::fs::perf_event::PerfEventController; +use crate::fs::pid::PidController; +use crate::fs::rdma::RdmaController; +use crate::fs::systemd::SystemdController; +use crate::fs::{Controllers, Hierarchy, Subsystem}; -use crate::cgroup::Cgroup; +use crate::fs::cgroup::Cgroup; /// Process mounts information. /// diff --git a/src/hugetlb.rs b/src/fs/hugetlb.rs similarity index 98% rename from src/hugetlb.rs rename to src/fs/hugetlb.rs index 311dca1..06a7d28 100644 --- a/src/hugetlb.rs +++ b/src/fs/hugetlb.rs @@ -12,11 +12,11 @@ use log::warn; use std::io::Write; use std::path::PathBuf; -use crate::error::ErrorKind::*; -use crate::error::*; -use crate::{flat_keyed_to_vec, read_u64_from}; +use crate::fs::error::ErrorKind::*; +use crate::fs::error::*; +use crate::fs::{flat_keyed_to_vec, read_u64_from}; -use crate::{ +use crate::fs::{ ControllIdentifier, ControllerInternal, Controllers, HugePageResources, Resources, Subsystem, }; diff --git a/src/memory.rs b/src/fs/memory.rs similarity index 99% rename from src/memory.rs rename to src/fs/memory.rs index 653006d..6b0c376 100644 --- a/src/memory.rs +++ b/src/fs/memory.rs @@ -14,14 +14,14 @@ use std::io::Write; use std::path::PathBuf; use std::sync::mpsc::Receiver; -use crate::error::ErrorKind::*; -use crate::error::*; -use crate::events; -use crate::{read_i64_from, read_string_from, read_u64_from}; +use crate::fs::error::ErrorKind::*; +use crate::fs::error::*; +use crate::fs::events; +use crate::fs::{read_i64_from, read_string_from, read_u64_from}; -use crate::flat_keyed_to_hashmap; +use crate::fs::flat_keyed_to_hashmap; -use crate::{ +use crate::fs::{ ControllIdentifier, ControllerInternal, Controllers, CustomizedAttribute, MaxValue, MemoryResources, Resources, Subsystem, }; @@ -1002,7 +1002,7 @@ impl<'a> From<&'a Subsystem> for &'a MemController { #[cfg(test)] mod tests { - use crate::memory::{ + use crate::fs::memory::{ parse_memory_stat, parse_numa_stat, parse_oom_control, MemoryStat, NumaStat, OomControl, }; diff --git a/src/fs/mod.rs b/src/fs/mod.rs new file mode 100644 index 0000000..81aa115 --- /dev/null +++ b/src/fs/mod.rs @@ -0,0 +1,1032 @@ +// Copyright (c) 2018 Levente Kurusa +// Copyright (c) 2020 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +#![allow(clippy::unnecessary_unwrap)] +use log::*; + +use std::collections::HashMap; +use std::fmt; +use std::fs::{self, File}; +use std::io::{BufRead, BufReader, Read, Write}; +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +macro_rules! update_and_test { + ($self: ident, $set_func:ident, $value:expr, $get_func:ident) => { + if let Some(v) = $value { + $self.$set_func(v)?; + if $self.$get_func()? != v { + return Err(Error::new(Other)); + } + } + }; +} + +macro_rules! update { + ($self: ident, $set_func:ident, $value:expr) => { + if let Some(v) = $value { + let _ = $self.$set_func(v); + } + }; +} + +pub mod blkio; +pub mod cgroup; +pub mod cgroup_builder; +pub mod cpu; +pub mod cpuacct; +pub mod cpuset; +pub mod devices; +pub mod error; +pub mod events; +pub mod freezer; +pub mod hierarchies; +pub mod hugetlb; +pub mod memory; +pub mod net_cls; +pub mod net_prio; +pub mod perf_event; +pub mod pid; +pub mod rdma; +pub mod systemd; + +use crate::fs::blkio::BlkIoController; +use crate::fs::cpu::CpuController; +use crate::fs::cpuacct::CpuAcctController; +use crate::fs::cpuset::CpuSetController; +use crate::fs::devices::DevicesController; +use crate::fs::error::ErrorKind::*; +use crate::fs::error::*; +use crate::fs::freezer::FreezerController; +use crate::fs::hugetlb::HugeTlbController; +use crate::fs::memory::MemController; +use crate::fs::net_cls::NetClsController; +use crate::fs::net_prio::NetPrioController; +use crate::fs::perf_event::PerfEventController; +use crate::fs::pid::PidController; +use crate::fs::rdma::RdmaController; +use crate::fs::systemd::SystemdController; + +#[doc(inline)] +pub use crate::fs::cgroup::Cgroup; + +/// Contains all the subsystems that are available in this crate. +#[derive(Debug, Clone)] +pub enum Subsystem { + /// Controller for the `Pid` subsystem, see `PidController` for more information. + Pid(PidController), + /// Controller for the `Mem` subsystem, see `MemController` for more information. + Mem(MemController), + /// Controller for the `CpuSet subsystem, see `CpuSetController` for more information. + CpuSet(CpuSetController), + /// Controller for the `CpuAcct` subsystem, see `CpuAcctController` for more information. + CpuAcct(CpuAcctController), + /// Controller for the `Cpu` subsystem, see `CpuController` for more information. + Cpu(CpuController), + /// Controller for the `Devices` subsystem, see `DevicesController` for more information. + Devices(DevicesController), + /// Controller for the `Freezer` subsystem, see `FreezerController` for more information. + Freezer(FreezerController), + /// Controller for the `NetCls` subsystem, see `NetClsController` for more information. + NetCls(NetClsController), + /// Controller for the `BlkIo` subsystem, see `BlkIoController` for more information. + BlkIo(BlkIoController), + /// Controller for the `PerfEvent` subsystem, see `PerfEventController` for more information. + PerfEvent(PerfEventController), + /// Controller for the `NetPrio` subsystem, see `NetPrioController` for more information. + NetPrio(NetPrioController), + /// Controller for the `HugeTlb` subsystem, see `HugeTlbController` for more information. + HugeTlb(HugeTlbController), + /// Controller for the `Rdma` subsystem, see `RdmaController` for more information. + Rdma(RdmaController), + /// Controller for the `Systemd` subsystem, see `SystemdController` for more information. + Systemd(SystemdController), +} + +#[doc(hidden)] +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum Controllers { + Pids, + Mem, + CpuSet, + CpuAcct, + Cpu, + Devices, + Freezer, + NetCls, + BlkIo, + PerfEvent, + NetPrio, + HugeTlb, + Rdma, + Systemd, +} + +impl fmt::Display for Controllers { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Controllers::Pids => write!(f, "pids"), + Controllers::Mem => write!(f, "memory"), + Controllers::CpuSet => write!(f, "cpuset"), + Controllers::CpuAcct => write!(f, "cpuacct"), + Controllers::Cpu => write!(f, "cpu"), + Controllers::Devices => write!(f, "devices"), + Controllers::Freezer => write!(f, "freezer"), + Controllers::NetCls => write!(f, "net_cls"), + Controllers::BlkIo => write!(f, "blkio"), + Controllers::PerfEvent => write!(f, "perf_event"), + Controllers::NetPrio => write!(f, "net_prio"), + Controllers::HugeTlb => write!(f, "hugetlb"), + Controllers::Rdma => write!(f, "rdma"), + Controllers::Systemd => write!(f, "name=systemd"), + } + } +} + +mod sealed { + use super::*; + + pub trait ControllerInternal { + fn apply(&self, res: &Resources) -> Result<()>; + + // meta stuff + fn control_type(&self) -> Controllers; + fn get_path(&self) -> &PathBuf; + fn get_path_mut(&mut self) -> &mut PathBuf; + fn get_base(&self) -> &PathBuf; + + /// Hooks running after controller crated, if have + fn post_create(&self) {} + + fn is_v2(&self) -> bool { + false + } + + fn verify_path(&self) -> Result<()> { + if self.get_path().starts_with(self.get_base()) { + Ok(()) + } else { + Err(Error::new(ErrorKind::InvalidPath)) + } + } + + fn open_path(&self, p: &str, w: bool) -> Result { + let mut path = self.get_path().clone(); + path.push(p); + + self.verify_path()?; + + if w { + match File::create(&path) { + Err(e) => Err(Error::with_cause( + ErrorKind::WriteFailed( + path.display().to_string(), + "[CREATE FILE]".to_string(), + ), + e, + )), + Ok(file) => Ok(file), + } + } else { + match File::open(&path) { + Err(e) => Err(Error::with_cause( + ErrorKind::ReadFailed(path.display().to_string()), + e, + )), + Ok(file) => Ok(file), + } + } + } + + fn get_max_value(&self, f: &str) -> Result { + self.open_path(f, false).and_then(|mut file| { + let mut string = String::new(); + let res = file.read_to_string(&mut string); + match res { + Ok(_) => parse_max_value(&string), + Err(e) => Err(Error::with_cause(ReadFailed(f.to_string()), e)), + } + }) + } + + #[doc(hidden)] + fn path_exists(&self, p: &str) -> bool { + if self.verify_path().is_err() { + return false; + } + + std::path::Path::new(p).exists() + } + } + + pub trait CustomizedAttribute: ControllerInternal { + fn set(&self, key: &str, value: &str) -> Result<()> { + self.open_path(key, true).and_then(|mut file| { + file.write_all(value.as_ref()).map_err(|e| { + Error::with_cause(WriteFailed(key.to_string(), value.to_string()), e) + }) + }) + } + + #[allow(dead_code)] + fn get(&self, key: &str) -> Result { + self.open_path(key, false).and_then(|mut file: File| { + let mut string = String::new(); + match file.read_to_string(&mut string) { + Ok(_) => Ok(string.trim().to_owned()), + Err(e) => Err(Error::with_cause(ReadFailed(key.to_string()), e)), + } + }) + } + } +} + +pub(crate) use crate::fs::sealed::{ControllerInternal, CustomizedAttribute}; + +/// A Controller is a subsystem attached to the control group. +/// +/// Implementors are able to control certain aspects of a control group. +pub trait Controller { + #[doc(hidden)] + fn control_type(&self) -> Controllers; + + /// The file system path to the controller. + fn path(&self) -> &Path; + + /// Root path of the file system to the controller. + fn base(&self) -> &Path; + + /// Apply a set of resources to the Controller, invoking its internal functions to pass the + /// kernel the information. + fn apply(&self, res: &Resources) -> Result<()>; + + /// Create this controller + fn create(&self); + + /// Does this controller already exist? + fn exists(&self) -> bool; + + /// Set notify_on_release + fn set_notify_on_release(&self, enable: bool) -> Result<()>; + + /// Set release_agent + fn set_release_agent(&self, path: &str) -> Result<()>; + + /// Delete the controller. + fn delete(&self) -> Result<()>; + + /// Attach a task to this controller. + fn add_task(&self, pid: &CgroupPid) -> Result<()>; + + /// Attach a task to this controller. + fn add_task_by_tgid(&self, pid: &CgroupPid) -> Result<()>; + + /// set cgroup type. + fn set_cgroup_type(&self, cgroup_type: &str) -> Result<()>; + + /// get cgroup type. + fn get_cgroup_type(&self) -> Result; + + /// Get the list of tasks that this controller has. + fn tasks(&self) -> Vec; + + /// Get the list of procs that this controller has. + fn procs(&self) -> Vec; + + fn v2(&self) -> bool; +} + +impl Controller for T +where + T: ControllerInternal, +{ + fn control_type(&self) -> Controllers { + ControllerInternal::control_type(self) + } + + fn path(&self) -> &Path { + self.get_path() + } + + fn base(&self) -> &Path { + self.get_base() + } + + /// Apply a set of resources to the Controller, invoking its internal functions to pass the + /// kernel the information. + fn apply(&self, res: &Resources) -> Result<()> { + ControllerInternal::apply(self, res) + } + + /// Create this controller + fn create(&self) { + self.verify_path() + .unwrap_or_else(|_| panic!("path should be valid: {:?}", self.path())); + + match ::std::fs::create_dir_all(self.get_path()) { + Ok(_) => self.post_create(), + Err(e) => warn!("error create_dir: {:?} error: {:?}", self.get_path(), e), + } + } + + /// Set notify_on_release + fn set_notify_on_release(&self, enable: bool) -> Result<()> { + if self.is_v2() { + return Err(Error::new(ErrorKind::CgroupVersion)); + } + self.open_path("notify_on_release", true) + .and_then(|mut file| { + write!(file, "{}", enable as i32).map_err(|e| { + Error::with_cause( + ErrorKind::WriteFailed("notify_on_release".to_string(), enable.to_string()), + e, + ) + }) + }) + } + + /// Set release_agent + fn set_release_agent(&self, path: &str) -> Result<()> { + if self.is_v2() { + return Err(Error::new(ErrorKind::CgroupVersion)); + } + self.open_path("release_agent", true).and_then(|mut file| { + file.write_all(path.as_bytes()).map_err(|e| { + Error::with_cause( + ErrorKind::WriteFailed("release_agent".to_string(), path.to_string()), + e, + ) + }) + }) + } + /// Does this controller already exist? + fn exists(&self) -> bool { + self.get_path().exists() + } + + /// Delete the controller. + fn delete(&self) -> Result<()> { + if !self.get_path().exists() { + return Ok(()); + } + + // Compatible with runC for remove dir operation + // https://github.com/opencontainers/runc/blob/main/libcontainer/cgroups/utils.go#L272 + // + // We trying to remove all paths five times with increasing delay between tries. + // If after all there are not removed cgroups - appropriate error will be + // returned. + let mut delay = std::time::Duration::from_millis(10); + let cgroup_path = self.get_path(); + for _i in 0..4 { + if let Ok(()) = remove_dir(cgroup_path) { + return Ok(()); + } + std::thread::sleep(delay); + delay *= 2; + } + + remove_dir(cgroup_path) + } + + /// Attach a task to this controller. + fn add_task(&self, pid: &CgroupPid) -> Result<()> { + let mut file_name = "tasks"; + if self.is_v2() { + file_name = "cgroup.threads"; + } + self.open_path(file_name, true).and_then(|mut file| { + file.write_all(pid.pid.to_string().as_ref()).map_err(|e| { + Error::with_cause( + ErrorKind::WriteFailed(file_name.to_string(), pid.pid.to_string()), + e, + ) + }) + }) + } + + /// Attach a task to this controller by thread group id. + fn add_task_by_tgid(&self, pid: &CgroupPid) -> Result<()> { + let file_name = "cgroup.procs"; + self.open_path(file_name, true).and_then(|mut file| { + file.write_all(pid.pid.to_string().as_ref()).map_err(|e| { + Error::with_cause( + ErrorKind::WriteFailed(file_name.to_string(), pid.pid.to_string()), + e, + ) + }) + }) + } + + /// Get the list of procs that this controller has. + fn procs(&self) -> Vec { + let file_name = "cgroup.procs"; + self.open_path(file_name, false) + .map(|file| { + let bf = BufReader::new(file); + let mut v = Vec::new(); + for line in bf.lines() { + match line { + Ok(line) => { + let n = line.trim().parse().unwrap_or(0u64); + v.push(n); + } + Err(_) => break, + } + } + v.into_iter().map(CgroupPid::from).collect() + }) + .unwrap_or_default() + } + + /// Get the list of tasks that this controller has. + fn tasks(&self) -> Vec { + let mut file_name = "tasks"; + if self.is_v2() { + file_name = "cgroup.threads"; + } + self.open_path(file_name, false) + .map(|file| { + let bf = BufReader::new(file); + let mut v = Vec::new(); + for line in bf.lines() { + match line { + Ok(line) => { + let n = line.trim().parse().unwrap_or(0u64); + v.push(n); + } + Err(_) => break, + } + } + v.into_iter().map(CgroupPid::from).collect() + }) + .unwrap_or_default() + } + + /// set cgroup.type + fn set_cgroup_type(&self, cgroup_type: &str) -> Result<()> { + if !self.is_v2() { + return Err(Error::new(ErrorKind::CgroupVersion)); + } + let file_name = "cgroup.type"; + self.open_path(file_name, true).and_then(|mut file| { + file.write_all(cgroup_type.as_bytes()).map_err(|e| { + Error::with_cause( + ErrorKind::WriteFailed(file_name.to_string(), cgroup_type.to_string()), + e, + ) + }) + }) + } + + /// get cgroup.type + fn get_cgroup_type(&self) -> Result { + if !self.is_v2() { + return Err(Error::new(ErrorKind::CgroupVersion)); + } + let file_name = "cgroup.type"; + self.open_path(file_name, false).and_then(|mut file: File| { + let mut string = String::new(); + match file.read_to_string(&mut string) { + Ok(_) => Ok(string.trim().to_owned()), + Err(e) => Err(Error::with_cause( + ErrorKind::ReadFailed(file_name.to_string()), + e, + )), + } + }) + } + + fn v2(&self) -> bool { + self.is_v2() + } +} + +// remove_dir aims to remove cgroup path. It does so recursively, +// by removing any subdirectories (sub-cgroups) first. +fn remove_dir(dir: &Path) -> Result<()> { + // try the fast path first. + if fs::remove_dir(dir).is_ok() { + return Ok(()); + } + + if dir.exists() && dir.is_dir() { + for entry in fs::read_dir(dir) + .map_err(|e| Error::with_cause(ReadFailed(dir.display().to_string()), e))? + { + let entry = + entry.map_err(|e| Error::with_cause(ReadFailed(dir.display().to_string()), e))?; + let path = entry.path(); + if path.is_dir() { + remove_dir(&path)?; + } + } + fs::remove_dir(dir).map_err(|e| Error::with_cause(RemoveFailed, e))?; + } + + Ok(()) +} + +#[doc(hidden)] +pub trait ControllIdentifier { + fn controller_type() -> Controllers; +} + +/// Control group hierarchy (right now, only V1 is supported, but in the future Unified will be +/// implemented as well). +pub trait Hierarchy: std::fmt::Debug + Send + Sync { + /// Returns what subsystems are supported by the hierarchy. + fn subsystems(&self) -> Vec; + + /// Returns the root directory of the hierarchy. + fn root(&self) -> PathBuf; + + /// Return a handle to the root control group in the hierarchy. + fn root_control_group(&self) -> Cgroup; + + /// Return a handle to the parent control group in the hierarchy. + fn parent_control_group(&self, path: &str) -> Cgroup; + + fn v2(&self) -> bool; +} + +/// Resource limits for the memory subsystem. +#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct MemoryResources { + /// How much memory (in bytes) can the kernel consume. + pub kernel_memory_limit: Option, + /// Upper limit of memory usage of the control group's tasks. + pub memory_hard_limit: Option, + /// How much memory the tasks in the control group can use when the system is under memory + /// pressure. + pub memory_soft_limit: Option, + /// How much of the kernel's memory (in bytes) can be used for TCP-related buffers. + pub kernel_tcp_memory_limit: Option, + /// How much memory and swap together can the tasks in the control group use. + pub memory_swap_limit: Option, + /// Controls the tendency of the kernel to swap out parts of the address space of the tasks to + /// disk. Lower value implies less likely. + /// + /// Note, however, that a value of zero does not mean the process is never swapped out. Use the + /// traditional `mlock(2)` system call for that purpose. + pub swappiness: Option, + /// Customized key-value attributes + /// + /// # Usage: + /// ``` + /// let resource = &mut cgroups_rs::fs::Resources::default(); + /// resource.memory.attrs.insert("memory.numa_balancing".to_string(), "true".to_string()); + /// // apply here + /// ``` + pub attrs: HashMap, +} + +/// Resources limits on the number of processes. +#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct PidResources { + /// The maximum number of processes that can exist in the control group. + /// + /// Note that attaching processes to the control group will still succeed _even_ if the limit + /// would be violated, however forks/clones inside the control group will have with `EAGAIN` if + /// they would violate the limit set here. + pub maximum_number_of_processes: Option, +} + +/// Resources limits about how the tasks can use the CPU. +#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct CpuResources { + // cpuset + /// A comma-separated list of CPU IDs where the task in the control group can run. Dashes + /// between numbers indicate ranges. + pub cpus: Option, + /// Same syntax as the `cpus` field of this structure, but applies to memory nodes instead of + /// processors. + pub mems: Option, + // cpu + /// Weight of how much of the total CPU time should this control group get. Note that this is + /// hierarchical, so this is weighted against the siblings of this control group. + pub shares: Option, + /// In one `period`, how much can the tasks run in microseconds. + pub quota: Option, + /// Period of time in microseconds. + pub period: Option, + /// This is currently a no-operation. + pub realtime_runtime: Option, + /// This is currently a no-operation. + pub realtime_period: Option, + /// Customized key-value attributes + /// # Usage: + /// ``` + /// let resource = &mut cgroups_rs::fs::Resources::default(); + /// resource.cpu.attrs.insert("cpu.cfs_init_buffer_us".to_string(), "10".to_string()); + /// // apply here + /// ``` + pub attrs: HashMap, +} + +/// A device resource that can be allowed or denied access to. +#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct DeviceResource { + /// If true, access to the device is allowed, otherwise it's denied. + pub allow: bool, + /// `'c'` for character device, `'b'` for block device; or `'a'` for all devices. + pub devtype: crate::fs::devices::DeviceType, + /// The major number of the device. + pub major: i64, + /// The minor number of the device. + pub minor: i64, + /// Sequence of `'r'`, `'w'` or `'m'`, each denoting read, write or mknod permissions. + pub access: Vec, +} + +/// Limit the usage of devices for the control group's tasks. +#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct DeviceResources { + /// For each device in the list, the limits in the structure are applied. + pub devices: Vec, +} + +/// Assigned priority for a network device. +#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct NetworkPriority { + /// The name (as visible in `ifconfig`) of the interface. + pub name: String, + /// Assigned priority. + pub priority: u64, +} + +/// Collections of limits and tags that can be imposed on packets emitted by the tasks in the +/// control group. +#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct NetworkResources { + /// The networking class identifier to attach to the packets. + /// + /// This can then later be used in iptables and such to have special rules. + pub class_id: Option, + /// Priority of the egress traffic for each interface. + pub priorities: Vec, +} + +/// A hugepage type and its consumption limit for the control group. +#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct HugePageResource { + /// The size of the hugepage, i.e. `2MB`, `1GB`, etc. + pub size: String, + /// The amount of bytes (of memory consumed by the tasks) that are allowed to be backed by + /// hugepages. + pub limit: u64, +} + +/// Provides the ability to set consumption limit on each type of hugepages. +#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct HugePageResources { + /// Set a limit of consumption for each hugepages type. + pub limits: Vec, +} + +/// Weight for a particular block device. +#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BlkIoDeviceResource { + /// The major number of the device. + pub major: u64, + /// The minor number of the device. + pub minor: u64, + /// The weight of the device against the descendant nodes. + pub weight: Option, + /// The weight of the device against the sibling nodes. + pub leaf_weight: Option, +} + +/// Provides the ability to throttle a device (both byte/sec, and IO op/s) +#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BlkIoDeviceThrottleResource { + /// The major number of the device. + pub major: u64, + /// The minor number of the device. + pub minor: u64, + /// The rate. + pub rate: u64, +} + +/// General block I/O resource limits. +#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BlkIoResources { + /// The weight of the control group against descendant nodes. + pub weight: Option, + /// The weight of the control group against sibling nodes. + pub leaf_weight: Option, + /// For each device, a separate weight (both normal and leaf) can be provided. + pub weight_device: Vec, + /// Throttled read bytes/second can be provided for each device. + pub throttle_read_bps_device: Vec, + /// Throttled read IO operations per second can be provided for each device. + pub throttle_read_iops_device: Vec, + /// Throttled written bytes/second can be provided for each device. + pub throttle_write_bps_device: Vec, + /// Throttled write IO operations per second can be provided for each device. + pub throttle_write_iops_device: Vec, + + /// Customized key-value attributes + /// # Usage: + /// ``` + /// let resource = &mut cgroups_rs::fs::Resources::default(); + /// resource.blkio.attrs.insert("io.cost.weight".to_string(), "10".to_string()); + /// // apply here + /// ``` + pub attrs: HashMap, +} + +/// The resource limits and constraints that will be set on the control group. +#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Resources { + /// Memory usage related limits. + pub memory: MemoryResources, + /// Process identifier related limits. + pub pid: PidResources, + /// CPU related limits. + pub cpu: CpuResources, + /// Device related limits. + pub devices: DeviceResources, + /// Network related tags and limits. + pub network: NetworkResources, + /// Hugepages consumption related limits. + pub hugepages: HugePageResources, + /// Block device I/O related limits. + pub blkio: BlkIoResources, +} + +/// A structure representing a `pid`. Currently implementations exist for `u64` and +/// `std::process::Child`. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct CgroupPid { + /// The process identifier + pub pid: u64, +} + +impl From for CgroupPid { + fn from(u: u64) -> CgroupPid { + CgroupPid { pid: u } + } +} + +impl From<&std::process::Child> for CgroupPid { + fn from(u: &std::process::Child) -> CgroupPid { + CgroupPid { pid: u.id() as u64 } + } +} + +impl Subsystem { + fn enter(self, path: &Path) -> Self { + match self { + Subsystem::Pid(mut cont) => Subsystem::Pid({ + cont.get_path_mut().push(path); + cont + }), + Subsystem::Mem(mut cont) => Subsystem::Mem({ + cont.get_path_mut().push(path); + cont + }), + Subsystem::CpuSet(mut cont) => Subsystem::CpuSet({ + cont.get_path_mut().push(path); + cont + }), + Subsystem::CpuAcct(mut cont) => Subsystem::CpuAcct({ + cont.get_path_mut().push(path); + cont + }), + Subsystem::Cpu(mut cont) => Subsystem::Cpu({ + cont.get_path_mut().push(path); + cont + }), + Subsystem::Devices(mut cont) => Subsystem::Devices({ + cont.get_path_mut().push(path); + cont + }), + Subsystem::Freezer(mut cont) => Subsystem::Freezer({ + cont.get_path_mut().push(path); + cont + }), + Subsystem::NetCls(mut cont) => Subsystem::NetCls({ + cont.get_path_mut().push(path); + cont + }), + Subsystem::BlkIo(mut cont) => Subsystem::BlkIo({ + cont.get_path_mut().push(path); + cont + }), + Subsystem::PerfEvent(mut cont) => Subsystem::PerfEvent({ + cont.get_path_mut().push(path); + cont + }), + Subsystem::NetPrio(mut cont) => Subsystem::NetPrio({ + cont.get_path_mut().push(path); + cont + }), + Subsystem::HugeTlb(mut cont) => Subsystem::HugeTlb({ + cont.get_path_mut().push(path); + cont + }), + Subsystem::Rdma(mut cont) => Subsystem::Rdma({ + cont.get_path_mut().push(path); + cont + }), + Subsystem::Systemd(mut cont) => Subsystem::Systemd({ + cont.get_path_mut().push(path); + cont + }), + } + } + + pub fn to_controller(&self) -> &dyn Controller { + match self { + Subsystem::Pid(cont) => cont, + Subsystem::Mem(cont) => cont, + Subsystem::CpuSet(cont) => cont, + Subsystem::CpuAcct(cont) => cont, + Subsystem::Cpu(cont) => cont, + Subsystem::Devices(cont) => cont, + Subsystem::Freezer(cont) => cont, + Subsystem::NetCls(cont) => cont, + Subsystem::BlkIo(cont) => cont, + Subsystem::PerfEvent(cont) => cont, + Subsystem::NetPrio(cont) => cont, + Subsystem::HugeTlb(cont) => cont, + Subsystem::Rdma(cont) => cont, + Subsystem::Systemd(cont) => cont, + } + } + + pub fn controller_name(&self) -> String { + self.to_controller().control_type().to_string() + } +} + +/// The values for `memory.hight` or `pids.max` +#[derive(Eq, PartialEq, Copy, Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum MaxValue { + /// This value is returned when the text is `"max"`. + Max, + /// When the value is a numerical value, they are returned via this enum field. + Value(i64), +} + +#[allow(clippy::derivable_impls)] +impl Default for MaxValue { + fn default() -> Self { + MaxValue::Max + } +} + +impl MaxValue { + #[allow(clippy::should_implement_trait, clippy::wrong_self_convention)] + fn to_i64(&self) -> i64 { + match self { + MaxValue::Max => -1, + MaxValue::Value(num) => *num, + } + } +} + +impl fmt::Display for MaxValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MaxValue::Max => write!(f, "max"), + MaxValue::Value(num) => write!(f, "{}", num), + } + } +} + +pub fn parse_max_value(s: &str) -> Result { + if s.trim() == "max" { + return Ok(MaxValue::Max); + } + match s.trim().parse() { + Ok(val) => Ok(MaxValue::Value(val)), + Err(e) => Err(Error::with_cause(ParseError, e)), + } +} + +// Flat keyed +// KEY0 VAL0\n +// KEY1 VAL1\n +pub fn flat_keyed_to_vec(mut file: File) -> Result> { + let mut content = String::new(); + file.read_to_string(&mut content) + .map_err(|e| Error::with_cause(ReadFailed("FIXME: read_string_from".to_string()), e))?; + + let mut v = Vec::new(); + for line in content.lines() { + let parts: Vec<&str> = line.split(' ').collect(); + if parts.len() == 2 { + if let Ok(i) = parts[1].parse::() { + v.push((parts[0].to_string(), i)); + } + } + } + Ok(v) +} + +// Flat keyed +// KEY0 VAL0\n +// KEY1 VAL1\n +pub fn flat_keyed_to_hashmap(mut file: File) -> Result> { + let mut content = String::new(); + file.read_to_string(&mut content) + .map_err(|e| Error::with_cause(ReadFailed("FIXME: read_string_from".to_string()), e))?; + + let mut h = HashMap::new(); + for line in content.lines() { + let parts: Vec<&str> = line.split(' ').collect(); + if parts.len() == 2 { + if let Ok(i) = parts[1].parse::() { + h.insert(parts[0].to_string(), i); + } + } + } + Ok(h) +} + +// Nested keyed +// KEY0 SUB_KEY0=VAL00 SUB_KEY1=VAL01... +// KEY1 SUB_KEY0=VAL10 SUB_KEY1=VAL11... +pub fn nested_keyed_to_hashmap(mut file: File) -> Result>> { + let mut content = String::new(); + file.read_to_string(&mut content) + .map_err(|e| Error::with_cause(ReadFailed("FIXME: read_string_from".to_string()), e))?; + + let mut h = HashMap::new(); + for line in content.lines() { + let parts: Vec<&str> = line.split(' ').collect(); + if parts.is_empty() { + continue; + } + let mut th = HashMap::new(); + for item in parts[1..].iter() { + let fields: Vec<&str> = item.split('=').collect(); + if fields.len() == 2 { + if let Ok(i) = fields[1].parse::() { + th.insert(fields[0].to_string(), i); + } + } + } + h.insert(parts[0].to_string(), th); + } + + Ok(h) +} + +fn read_from(mut file: File) -> Result +where + T: FromStr, + ::Err: 'static + Send + Sync + std::error::Error, +{ + let mut string = String::new(); + match file.read_to_string(&mut string) { + Ok(_) => string + .trim() + .parse::() + .map_err(|e| Error::with_cause(ParseError, e)), + Err(e) => Err(Error::with_cause( + ReadFailed("FIXME: can't get path in fn read_from".to_string()), + e, + )), + } +} + +fn read_string_from(mut file: File) -> Result { + let mut string = String::new(); + match file.read_to_string(&mut string) { + Ok(_) => Ok(string.trim().to_string()), + Err(e) => Err(Error::with_cause( + ReadFailed("FIXME: can't get path in fn read_string_from".to_string()), + e, + )), + } +} + +/// read and parse an u64 data +fn read_u64_from(file: File) -> Result { + read_from::(file) +} + +/// read and parse an i64 data +fn read_i64_from(file: File) -> Result { + read_from::(file) +} diff --git a/src/net_cls.rs b/src/fs/net_cls.rs similarity index 96% rename from src/net_cls.rs rename to src/fs/net_cls.rs index a98ab8c..d87f8ee 100644 --- a/src/net_cls.rs +++ b/src/fs/net_cls.rs @@ -10,11 +10,11 @@ use std::io::Write; use std::path::PathBuf; -use crate::error::ErrorKind::*; -use crate::error::*; +use crate::fs::error::ErrorKind::*; +use crate::fs::error::*; -use crate::read_u64_from; -use crate::{ +use crate::fs::read_u64_from; +use crate::fs::{ ControllIdentifier, ControllerInternal, Controllers, NetworkResources, Resources, Subsystem, }; diff --git a/src/net_prio.rs b/src/fs/net_prio.rs similarity index 97% rename from src/net_prio.rs rename to src/fs/net_prio.rs index 51fede4..199866c 100644 --- a/src/net_prio.rs +++ b/src/fs/net_prio.rs @@ -11,11 +11,11 @@ use std::collections::HashMap; use std::io::{BufRead, BufReader, Write}; use std::path::PathBuf; -use crate::error::ErrorKind::*; -use crate::error::*; +use crate::fs::error::ErrorKind::*; +use crate::fs::error::*; -use crate::read_u64_from; -use crate::{ +use crate::fs::read_u64_from; +use crate::fs::{ ControllIdentifier, ControllerInternal, Controllers, NetworkResources, Resources, Subsystem, }; diff --git a/src/perf_event.rs b/src/fs/perf_event.rs similarity index 94% rename from src/perf_event.rs rename to src/fs/perf_event.rs index 002cff6..ea6c54a 100644 --- a/src/perf_event.rs +++ b/src/fs/perf_event.rs @@ -9,9 +9,9 @@ //! [tools/perf/Documentation/perf-record.txt](https://raw.githubusercontent.com/torvalds/linux/master/tools/perf/Documentation/perf-record.txt) use std::path::PathBuf; -use crate::error::*; +use crate::fs::error::*; -use crate::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem}; +use crate::fs::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem}; /// A controller that allows controlling the `perf_event` subsystem of a Cgroup. /// diff --git a/src/pid.rs b/src/fs/pid.rs similarity index 97% rename from src/pid.rs rename to src/fs/pid.rs index f98f4db..1cfbc81 100644 --- a/src/pid.rs +++ b/src/fs/pid.rs @@ -11,11 +11,11 @@ use std::io::{Read, Write}; use std::path::PathBuf; -use crate::error::ErrorKind::*; -use crate::error::*; +use crate::fs::error::ErrorKind::*; +use crate::fs::error::*; -use crate::read_u64_from; -use crate::{ +use crate::fs::read_u64_from; +use crate::fs::{ parse_max_value, ControllIdentifier, ControllerInternal, Controllers, MaxValue, PidResources, Resources, Subsystem, }; diff --git a/src/rdma.rs b/src/fs/rdma.rs similarity index 93% rename from src/rdma.rs rename to src/fs/rdma.rs index 5d76fb1..910191c 100644 --- a/src/rdma.rs +++ b/src/fs/rdma.rs @@ -10,11 +10,11 @@ use std::io::Write; use std::path::PathBuf; -use crate::error::ErrorKind::*; -use crate::error::*; +use crate::fs::error::ErrorKind::*; +use crate::fs::error::*; -use crate::read_string_from; -use crate::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem}; +use crate::fs::read_string_from; +use crate::fs::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem}; /// A controller that allows controlling the `rdma` subsystem of a Cgroup. /// diff --git a/src/systemd.rs b/src/fs/systemd.rs similarity index 93% rename from src/systemd.rs rename to src/fs/systemd.rs index 6002c1c..7f5ba8f 100644 --- a/src/systemd.rs +++ b/src/fs/systemd.rs @@ -7,9 +7,9 @@ //! use std::path::PathBuf; -use crate::error::*; +use crate::fs::error::*; -use crate::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem}; +use crate::fs::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem}; /// A controller that allows controlling the `systemd` subsystem of a Cgroup. /// diff --git a/src/lib.rs b/src/lib.rs index 54e940b..2528ecf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,1032 +1,7 @@ // Copyright (c) 2018 Levente Kurusa -// Copyright (c) 2020 Ant Group +// Copyright (c) 2020-2025 Ant Group // // SPDX-License-Identifier: Apache-2.0 or MIT // -#![allow(clippy::unnecessary_unwrap)] -use log::*; - -use std::collections::HashMap; -use std::fmt; -use std::fs::{self, File}; -use std::io::{BufRead, BufReader, Read, Write}; -use std::path::{Path, PathBuf}; -use std::str::FromStr; - -macro_rules! update_and_test { - ($self: ident, $set_func:ident, $value:expr, $get_func:ident) => { - if let Some(v) = $value { - $self.$set_func(v)?; - if $self.$get_func()? != v { - return Err(Error::new(Other)); - } - } - }; -} - -macro_rules! update { - ($self: ident, $set_func:ident, $value:expr) => { - if let Some(v) = $value { - let _ = $self.$set_func(v); - } - }; -} - -pub mod blkio; -pub mod cgroup; -pub mod cgroup_builder; -pub mod cpu; -pub mod cpuacct; -pub mod cpuset; -pub mod devices; -pub mod error; -pub mod events; -pub mod freezer; -pub mod hierarchies; -pub mod hugetlb; -pub mod memory; -pub mod net_cls; -pub mod net_prio; -pub mod perf_event; -pub mod pid; -pub mod rdma; -pub mod systemd; - -use crate::blkio::BlkIoController; -use crate::cpu::CpuController; -use crate::cpuacct::CpuAcctController; -use crate::cpuset::CpuSetController; -use crate::devices::DevicesController; -use crate::error::ErrorKind::*; -use crate::error::*; -use crate::freezer::FreezerController; -use crate::hugetlb::HugeTlbController; -use crate::memory::MemController; -use crate::net_cls::NetClsController; -use crate::net_prio::NetPrioController; -use crate::perf_event::PerfEventController; -use crate::pid::PidController; -use crate::rdma::RdmaController; -use crate::systemd::SystemdController; - -#[doc(inline)] -pub use crate::cgroup::Cgroup; - -/// Contains all the subsystems that are available in this crate. -#[derive(Debug, Clone)] -pub enum Subsystem { - /// Controller for the `Pid` subsystem, see `PidController` for more information. - Pid(PidController), - /// Controller for the `Mem` subsystem, see `MemController` for more information. - Mem(MemController), - /// Controller for the `CpuSet subsystem, see `CpuSetController` for more information. - CpuSet(CpuSetController), - /// Controller for the `CpuAcct` subsystem, see `CpuAcctController` for more information. - CpuAcct(CpuAcctController), - /// Controller for the `Cpu` subsystem, see `CpuController` for more information. - Cpu(CpuController), - /// Controller for the `Devices` subsystem, see `DevicesController` for more information. - Devices(DevicesController), - /// Controller for the `Freezer` subsystem, see `FreezerController` for more information. - Freezer(FreezerController), - /// Controller for the `NetCls` subsystem, see `NetClsController` for more information. - NetCls(NetClsController), - /// Controller for the `BlkIo` subsystem, see `BlkIoController` for more information. - BlkIo(BlkIoController), - /// Controller for the `PerfEvent` subsystem, see `PerfEventController` for more information. - PerfEvent(PerfEventController), - /// Controller for the `NetPrio` subsystem, see `NetPrioController` for more information. - NetPrio(NetPrioController), - /// Controller for the `HugeTlb` subsystem, see `HugeTlbController` for more information. - HugeTlb(HugeTlbController), - /// Controller for the `Rdma` subsystem, see `RdmaController` for more information. - Rdma(RdmaController), - /// Controller for the `Systemd` subsystem, see `SystemdController` for more information. - Systemd(SystemdController), -} - -#[doc(hidden)] -#[derive(Eq, PartialEq, Debug, Clone)] -pub enum Controllers { - Pids, - Mem, - CpuSet, - CpuAcct, - Cpu, - Devices, - Freezer, - NetCls, - BlkIo, - PerfEvent, - NetPrio, - HugeTlb, - Rdma, - Systemd, -} - -impl fmt::Display for Controllers { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Controllers::Pids => write!(f, "pids"), - Controllers::Mem => write!(f, "memory"), - Controllers::CpuSet => write!(f, "cpuset"), - Controllers::CpuAcct => write!(f, "cpuacct"), - Controllers::Cpu => write!(f, "cpu"), - Controllers::Devices => write!(f, "devices"), - Controllers::Freezer => write!(f, "freezer"), - Controllers::NetCls => write!(f, "net_cls"), - Controllers::BlkIo => write!(f, "blkio"), - Controllers::PerfEvent => write!(f, "perf_event"), - Controllers::NetPrio => write!(f, "net_prio"), - Controllers::HugeTlb => write!(f, "hugetlb"), - Controllers::Rdma => write!(f, "rdma"), - Controllers::Systemd => write!(f, "name=systemd"), - } - } -} - -mod sealed { - use super::*; - - pub trait ControllerInternal { - fn apply(&self, res: &Resources) -> Result<()>; - - // meta stuff - fn control_type(&self) -> Controllers; - fn get_path(&self) -> &PathBuf; - fn get_path_mut(&mut self) -> &mut PathBuf; - fn get_base(&self) -> &PathBuf; - - /// Hooks running after controller crated, if have - fn post_create(&self) {} - - fn is_v2(&self) -> bool { - false - } - - fn verify_path(&self) -> Result<()> { - if self.get_path().starts_with(self.get_base()) { - Ok(()) - } else { - Err(Error::new(ErrorKind::InvalidPath)) - } - } - - fn open_path(&self, p: &str, w: bool) -> Result { - let mut path = self.get_path().clone(); - path.push(p); - - self.verify_path()?; - - if w { - match File::create(&path) { - Err(e) => Err(Error::with_cause( - ErrorKind::WriteFailed( - path.display().to_string(), - "[CREATE FILE]".to_string(), - ), - e, - )), - Ok(file) => Ok(file), - } - } else { - match File::open(&path) { - Err(e) => Err(Error::with_cause( - ErrorKind::ReadFailed(path.display().to_string()), - e, - )), - Ok(file) => Ok(file), - } - } - } - - fn get_max_value(&self, f: &str) -> Result { - self.open_path(f, false).and_then(|mut file| { - let mut string = String::new(); - let res = file.read_to_string(&mut string); - match res { - Ok(_) => parse_max_value(&string), - Err(e) => Err(Error::with_cause(ReadFailed(f.to_string()), e)), - } - }) - } - - #[doc(hidden)] - fn path_exists(&self, p: &str) -> bool { - if self.verify_path().is_err() { - return false; - } - - std::path::Path::new(p).exists() - } - } - - pub trait CustomizedAttribute: ControllerInternal { - fn set(&self, key: &str, value: &str) -> Result<()> { - self.open_path(key, true).and_then(|mut file| { - file.write_all(value.as_ref()).map_err(|e| { - Error::with_cause(WriteFailed(key.to_string(), value.to_string()), e) - }) - }) - } - - #[allow(dead_code)] - fn get(&self, key: &str) -> Result { - self.open_path(key, false).and_then(|mut file: File| { - let mut string = String::new(); - match file.read_to_string(&mut string) { - Ok(_) => Ok(string.trim().to_owned()), - Err(e) => Err(Error::with_cause(ReadFailed(key.to_string()), e)), - } - }) - } - } -} - -pub(crate) use crate::sealed::{ControllerInternal, CustomizedAttribute}; - -/// A Controller is a subsystem attached to the control group. -/// -/// Implementors are able to control certain aspects of a control group. -pub trait Controller { - #[doc(hidden)] - fn control_type(&self) -> Controllers; - - /// The file system path to the controller. - fn path(&self) -> &Path; - - /// Root path of the file system to the controller. - fn base(&self) -> &Path; - - /// Apply a set of resources to the Controller, invoking its internal functions to pass the - /// kernel the information. - fn apply(&self, res: &Resources) -> Result<()>; - - /// Create this controller - fn create(&self); - - /// Does this controller already exist? - fn exists(&self) -> bool; - - /// Set notify_on_release - fn set_notify_on_release(&self, enable: bool) -> Result<()>; - - /// Set release_agent - fn set_release_agent(&self, path: &str) -> Result<()>; - - /// Delete the controller. - fn delete(&self) -> Result<()>; - - /// Attach a task to this controller. - fn add_task(&self, pid: &CgroupPid) -> Result<()>; - - /// Attach a task to this controller. - fn add_task_by_tgid(&self, pid: &CgroupPid) -> Result<()>; - - /// set cgroup type. - fn set_cgroup_type(&self, cgroup_type: &str) -> Result<()>; - - /// get cgroup type. - fn get_cgroup_type(&self) -> Result; - - /// Get the list of tasks that this controller has. - fn tasks(&self) -> Vec; - - /// Get the list of procs that this controller has. - fn procs(&self) -> Vec; - - fn v2(&self) -> bool; -} - -impl Controller for T -where - T: ControllerInternal, -{ - fn control_type(&self) -> Controllers { - ControllerInternal::control_type(self) - } - - fn path(&self) -> &Path { - self.get_path() - } - - fn base(&self) -> &Path { - self.get_base() - } - - /// Apply a set of resources to the Controller, invoking its internal functions to pass the - /// kernel the information. - fn apply(&self, res: &Resources) -> Result<()> { - ControllerInternal::apply(self, res) - } - - /// Create this controller - fn create(&self) { - self.verify_path() - .unwrap_or_else(|_| panic!("path should be valid: {:?}", self.path())); - - match ::std::fs::create_dir_all(self.get_path()) { - Ok(_) => self.post_create(), - Err(e) => warn!("error create_dir: {:?} error: {:?}", self.get_path(), e), - } - } - - /// Set notify_on_release - fn set_notify_on_release(&self, enable: bool) -> Result<()> { - if self.is_v2() { - return Err(Error::new(ErrorKind::CgroupVersion)); - } - self.open_path("notify_on_release", true) - .and_then(|mut file| { - write!(file, "{}", enable as i32).map_err(|e| { - Error::with_cause( - ErrorKind::WriteFailed("notify_on_release".to_string(), enable.to_string()), - e, - ) - }) - }) - } - - /// Set release_agent - fn set_release_agent(&self, path: &str) -> Result<()> { - if self.is_v2() { - return Err(Error::new(ErrorKind::CgroupVersion)); - } - self.open_path("release_agent", true).and_then(|mut file| { - file.write_all(path.as_bytes()).map_err(|e| { - Error::with_cause( - ErrorKind::WriteFailed("release_agent".to_string(), path.to_string()), - e, - ) - }) - }) - } - /// Does this controller already exist? - fn exists(&self) -> bool { - self.get_path().exists() - } - - /// Delete the controller. - fn delete(&self) -> Result<()> { - if !self.get_path().exists() { - return Ok(()); - } - - // Compatible with runC for remove dir operation - // https://github.com/opencontainers/runc/blob/main/libcontainer/cgroups/utils.go#L272 - // - // We trying to remove all paths five times with increasing delay between tries. - // If after all there are not removed cgroups - appropriate error will be - // returned. - let mut delay = std::time::Duration::from_millis(10); - let cgroup_path = self.get_path(); - for _i in 0..4 { - if let Ok(()) = remove_dir(cgroup_path) { - return Ok(()); - } - std::thread::sleep(delay); - delay *= 2; - } - - remove_dir(cgroup_path) - } - - /// Attach a task to this controller. - fn add_task(&self, pid: &CgroupPid) -> Result<()> { - let mut file_name = "tasks"; - if self.is_v2() { - file_name = "cgroup.threads"; - } - self.open_path(file_name, true).and_then(|mut file| { - file.write_all(pid.pid.to_string().as_ref()).map_err(|e| { - Error::with_cause( - ErrorKind::WriteFailed(file_name.to_string(), pid.pid.to_string()), - e, - ) - }) - }) - } - - /// Attach a task to this controller by thread group id. - fn add_task_by_tgid(&self, pid: &CgroupPid) -> Result<()> { - let file_name = "cgroup.procs"; - self.open_path(file_name, true).and_then(|mut file| { - file.write_all(pid.pid.to_string().as_ref()).map_err(|e| { - Error::with_cause( - ErrorKind::WriteFailed(file_name.to_string(), pid.pid.to_string()), - e, - ) - }) - }) - } - - /// Get the list of procs that this controller has. - fn procs(&self) -> Vec { - let file_name = "cgroup.procs"; - self.open_path(file_name, false) - .map(|file| { - let bf = BufReader::new(file); - let mut v = Vec::new(); - for line in bf.lines() { - match line { - Ok(line) => { - let n = line.trim().parse().unwrap_or(0u64); - v.push(n); - } - Err(_) => break, - } - } - v.into_iter().map(CgroupPid::from).collect() - }) - .unwrap_or_default() - } - - /// Get the list of tasks that this controller has. - fn tasks(&self) -> Vec { - let mut file_name = "tasks"; - if self.is_v2() { - file_name = "cgroup.threads"; - } - self.open_path(file_name, false) - .map(|file| { - let bf = BufReader::new(file); - let mut v = Vec::new(); - for line in bf.lines() { - match line { - Ok(line) => { - let n = line.trim().parse().unwrap_or(0u64); - v.push(n); - } - Err(_) => break, - } - } - v.into_iter().map(CgroupPid::from).collect() - }) - .unwrap_or_default() - } - - /// set cgroup.type - fn set_cgroup_type(&self, cgroup_type: &str) -> Result<()> { - if !self.is_v2() { - return Err(Error::new(ErrorKind::CgroupVersion)); - } - let file_name = "cgroup.type"; - self.open_path(file_name, true).and_then(|mut file| { - file.write_all(cgroup_type.as_bytes()).map_err(|e| { - Error::with_cause( - ErrorKind::WriteFailed(file_name.to_string(), cgroup_type.to_string()), - e, - ) - }) - }) - } - - /// get cgroup.type - fn get_cgroup_type(&self) -> Result { - if !self.is_v2() { - return Err(Error::new(ErrorKind::CgroupVersion)); - } - let file_name = "cgroup.type"; - self.open_path(file_name, false).and_then(|mut file: File| { - let mut string = String::new(); - match file.read_to_string(&mut string) { - Ok(_) => Ok(string.trim().to_owned()), - Err(e) => Err(Error::with_cause( - ErrorKind::ReadFailed(file_name.to_string()), - e, - )), - } - }) - } - - fn v2(&self) -> bool { - self.is_v2() - } -} - -// remove_dir aims to remove cgroup path. It does so recursively, -// by removing any subdirectories (sub-cgroups) first. -fn remove_dir(dir: &Path) -> Result<()> { - // try the fast path first. - if fs::remove_dir(dir).is_ok() { - return Ok(()); - } - - if dir.exists() && dir.is_dir() { - for entry in fs::read_dir(dir) - .map_err(|e| Error::with_cause(ReadFailed(dir.display().to_string()), e))? - { - let entry = - entry.map_err(|e| Error::with_cause(ReadFailed(dir.display().to_string()), e))?; - let path = entry.path(); - if path.is_dir() { - remove_dir(&path)?; - } - } - fs::remove_dir(dir).map_err(|e| Error::with_cause(RemoveFailed, e))?; - } - - Ok(()) -} - -#[doc(hidden)] -pub trait ControllIdentifier { - fn controller_type() -> Controllers; -} - -/// Control group hierarchy (right now, only V1 is supported, but in the future Unified will be -/// implemented as well). -pub trait Hierarchy: std::fmt::Debug + Send + Sync { - /// Returns what subsystems are supported by the hierarchy. - fn subsystems(&self) -> Vec; - - /// Returns the root directory of the hierarchy. - fn root(&self) -> PathBuf; - - /// Return a handle to the root control group in the hierarchy. - fn root_control_group(&self) -> Cgroup; - - /// Return a handle to the parent control group in the hierarchy. - fn parent_control_group(&self, path: &str) -> Cgroup; - - fn v2(&self) -> bool; -} - -/// Resource limits for the memory subsystem. -#[derive(Debug, Clone, Eq, PartialEq, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct MemoryResources { - /// How much memory (in bytes) can the kernel consume. - pub kernel_memory_limit: Option, - /// Upper limit of memory usage of the control group's tasks. - pub memory_hard_limit: Option, - /// How much memory the tasks in the control group can use when the system is under memory - /// pressure. - pub memory_soft_limit: Option, - /// How much of the kernel's memory (in bytes) can be used for TCP-related buffers. - pub kernel_tcp_memory_limit: Option, - /// How much memory and swap together can the tasks in the control group use. - pub memory_swap_limit: Option, - /// Controls the tendency of the kernel to swap out parts of the address space of the tasks to - /// disk. Lower value implies less likely. - /// - /// Note, however, that a value of zero does not mean the process is never swapped out. Use the - /// traditional `mlock(2)` system call for that purpose. - pub swappiness: Option, - /// Customized key-value attributes - /// - /// # Usage: - /// ``` - /// let resource = &mut cgroups_rs::Resources::default(); - /// resource.memory.attrs.insert("memory.numa_balancing".to_string(), "true".to_string()); - /// // apply here - /// ``` - pub attrs: HashMap, -} - -/// Resources limits on the number of processes. -#[derive(Debug, Clone, Eq, PartialEq, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct PidResources { - /// The maximum number of processes that can exist in the control group. - /// - /// Note that attaching processes to the control group will still succeed _even_ if the limit - /// would be violated, however forks/clones inside the control group will have with `EAGAIN` if - /// they would violate the limit set here. - pub maximum_number_of_processes: Option, -} - -/// Resources limits about how the tasks can use the CPU. -#[derive(Debug, Clone, Eq, PartialEq, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct CpuResources { - // cpuset - /// A comma-separated list of CPU IDs where the task in the control group can run. Dashes - /// between numbers indicate ranges. - pub cpus: Option, - /// Same syntax as the `cpus` field of this structure, but applies to memory nodes instead of - /// processors. - pub mems: Option, - // cpu - /// Weight of how much of the total CPU time should this control group get. Note that this is - /// hierarchical, so this is weighted against the siblings of this control group. - pub shares: Option, - /// In one `period`, how much can the tasks run in microseconds. - pub quota: Option, - /// Period of time in microseconds. - pub period: Option, - /// This is currently a no-operation. - pub realtime_runtime: Option, - /// This is currently a no-operation. - pub realtime_period: Option, - /// Customized key-value attributes - /// # Usage: - /// ``` - /// let resource = &mut cgroups_rs::Resources::default(); - /// resource.cpu.attrs.insert("cpu.cfs_init_buffer_us".to_string(), "10".to_string()); - /// // apply here - /// ``` - pub attrs: HashMap, -} - -/// A device resource that can be allowed or denied access to. -#[derive(Debug, Clone, Eq, PartialEq, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct DeviceResource { - /// If true, access to the device is allowed, otherwise it's denied. - pub allow: bool, - /// `'c'` for character device, `'b'` for block device; or `'a'` for all devices. - pub devtype: crate::devices::DeviceType, - /// The major number of the device. - pub major: i64, - /// The minor number of the device. - pub minor: i64, - /// Sequence of `'r'`, `'w'` or `'m'`, each denoting read, write or mknod permissions. - pub access: Vec, -} - -/// Limit the usage of devices for the control group's tasks. -#[derive(Debug, Clone, Eq, PartialEq, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct DeviceResources { - /// For each device in the list, the limits in the structure are applied. - pub devices: Vec, -} - -/// Assigned priority for a network device. -#[derive(Debug, Clone, Eq, PartialEq, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct NetworkPriority { - /// The name (as visible in `ifconfig`) of the interface. - pub name: String, - /// Assigned priority. - pub priority: u64, -} - -/// Collections of limits and tags that can be imposed on packets emitted by the tasks in the -/// control group. -#[derive(Debug, Clone, Eq, PartialEq, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct NetworkResources { - /// The networking class identifier to attach to the packets. - /// - /// This can then later be used in iptables and such to have special rules. - pub class_id: Option, - /// Priority of the egress traffic for each interface. - pub priorities: Vec, -} - -/// A hugepage type and its consumption limit for the control group. -#[derive(Debug, Clone, Eq, PartialEq, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct HugePageResource { - /// The size of the hugepage, i.e. `2MB`, `1GB`, etc. - pub size: String, - /// The amount of bytes (of memory consumed by the tasks) that are allowed to be backed by - /// hugepages. - pub limit: u64, -} - -/// Provides the ability to set consumption limit on each type of hugepages. -#[derive(Debug, Clone, Eq, PartialEq, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct HugePageResources { - /// Set a limit of consumption for each hugepages type. - pub limits: Vec, -} - -/// Weight for a particular block device. -#[derive(Debug, Clone, Eq, PartialEq, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct BlkIoDeviceResource { - /// The major number of the device. - pub major: u64, - /// The minor number of the device. - pub minor: u64, - /// The weight of the device against the descendant nodes. - pub weight: Option, - /// The weight of the device against the sibling nodes. - pub leaf_weight: Option, -} - -/// Provides the ability to throttle a device (both byte/sec, and IO op/s) -#[derive(Debug, Clone, Eq, PartialEq, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct BlkIoDeviceThrottleResource { - /// The major number of the device. - pub major: u64, - /// The minor number of the device. - pub minor: u64, - /// The rate. - pub rate: u64, -} - -/// General block I/O resource limits. -#[derive(Debug, Clone, Eq, PartialEq, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct BlkIoResources { - /// The weight of the control group against descendant nodes. - pub weight: Option, - /// The weight of the control group against sibling nodes. - pub leaf_weight: Option, - /// For each device, a separate weight (both normal and leaf) can be provided. - pub weight_device: Vec, - /// Throttled read bytes/second can be provided for each device. - pub throttle_read_bps_device: Vec, - /// Throttled read IO operations per second can be provided for each device. - pub throttle_read_iops_device: Vec, - /// Throttled written bytes/second can be provided for each device. - pub throttle_write_bps_device: Vec, - /// Throttled write IO operations per second can be provided for each device. - pub throttle_write_iops_device: Vec, - - /// Customized key-value attributes - /// # Usage: - /// ``` - /// let resource = &mut cgroups_rs::Resources::default(); - /// resource.blkio.attrs.insert("io.cost.weight".to_string(), "10".to_string()); - /// // apply here - /// ``` - pub attrs: HashMap, -} - -/// The resource limits and constraints that will be set on the control group. -#[derive(Debug, Clone, Eq, PartialEq, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Resources { - /// Memory usage related limits. - pub memory: MemoryResources, - /// Process identifier related limits. - pub pid: PidResources, - /// CPU related limits. - pub cpu: CpuResources, - /// Device related limits. - pub devices: DeviceResources, - /// Network related tags and limits. - pub network: NetworkResources, - /// Hugepages consumption related limits. - pub hugepages: HugePageResources, - /// Block device I/O related limits. - pub blkio: BlkIoResources, -} - -/// A structure representing a `pid`. Currently implementations exist for `u64` and -/// `std::process::Child`. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct CgroupPid { - /// The process identifier - pub pid: u64, -} - -impl From for CgroupPid { - fn from(u: u64) -> CgroupPid { - CgroupPid { pid: u } - } -} - -impl From<&std::process::Child> for CgroupPid { - fn from(u: &std::process::Child) -> CgroupPid { - CgroupPid { pid: u.id() as u64 } - } -} - -impl Subsystem { - fn enter(self, path: &Path) -> Self { - match self { - Subsystem::Pid(mut cont) => Subsystem::Pid({ - cont.get_path_mut().push(path); - cont - }), - Subsystem::Mem(mut cont) => Subsystem::Mem({ - cont.get_path_mut().push(path); - cont - }), - Subsystem::CpuSet(mut cont) => Subsystem::CpuSet({ - cont.get_path_mut().push(path); - cont - }), - Subsystem::CpuAcct(mut cont) => Subsystem::CpuAcct({ - cont.get_path_mut().push(path); - cont - }), - Subsystem::Cpu(mut cont) => Subsystem::Cpu({ - cont.get_path_mut().push(path); - cont - }), - Subsystem::Devices(mut cont) => Subsystem::Devices({ - cont.get_path_mut().push(path); - cont - }), - Subsystem::Freezer(mut cont) => Subsystem::Freezer({ - cont.get_path_mut().push(path); - cont - }), - Subsystem::NetCls(mut cont) => Subsystem::NetCls({ - cont.get_path_mut().push(path); - cont - }), - Subsystem::BlkIo(mut cont) => Subsystem::BlkIo({ - cont.get_path_mut().push(path); - cont - }), - Subsystem::PerfEvent(mut cont) => Subsystem::PerfEvent({ - cont.get_path_mut().push(path); - cont - }), - Subsystem::NetPrio(mut cont) => Subsystem::NetPrio({ - cont.get_path_mut().push(path); - cont - }), - Subsystem::HugeTlb(mut cont) => Subsystem::HugeTlb({ - cont.get_path_mut().push(path); - cont - }), - Subsystem::Rdma(mut cont) => Subsystem::Rdma({ - cont.get_path_mut().push(path); - cont - }), - Subsystem::Systemd(mut cont) => Subsystem::Systemd({ - cont.get_path_mut().push(path); - cont - }), - } - } - - pub fn to_controller(&self) -> &dyn Controller { - match self { - Subsystem::Pid(cont) => cont, - Subsystem::Mem(cont) => cont, - Subsystem::CpuSet(cont) => cont, - Subsystem::CpuAcct(cont) => cont, - Subsystem::Cpu(cont) => cont, - Subsystem::Devices(cont) => cont, - Subsystem::Freezer(cont) => cont, - Subsystem::NetCls(cont) => cont, - Subsystem::BlkIo(cont) => cont, - Subsystem::PerfEvent(cont) => cont, - Subsystem::NetPrio(cont) => cont, - Subsystem::HugeTlb(cont) => cont, - Subsystem::Rdma(cont) => cont, - Subsystem::Systemd(cont) => cont, - } - } - - pub fn controller_name(&self) -> String { - self.to_controller().control_type().to_string() - } -} - -/// The values for `memory.hight` or `pids.max` -#[derive(Eq, PartialEq, Copy, Clone, Debug)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum MaxValue { - /// This value is returned when the text is `"max"`. - Max, - /// When the value is a numerical value, they are returned via this enum field. - Value(i64), -} - -#[allow(clippy::derivable_impls)] -impl Default for MaxValue { - fn default() -> Self { - MaxValue::Max - } -} - -impl MaxValue { - #[allow(clippy::should_implement_trait, clippy::wrong_self_convention)] - fn to_i64(&self) -> i64 { - match self { - MaxValue::Max => -1, - MaxValue::Value(num) => *num, - } - } -} - -impl fmt::Display for MaxValue { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - MaxValue::Max => write!(f, "max"), - MaxValue::Value(num) => write!(f, "{}", num), - } - } -} - -pub fn parse_max_value(s: &str) -> Result { - if s.trim() == "max" { - return Ok(MaxValue::Max); - } - match s.trim().parse() { - Ok(val) => Ok(MaxValue::Value(val)), - Err(e) => Err(Error::with_cause(ParseError, e)), - } -} - -// Flat keyed -// KEY0 VAL0\n -// KEY1 VAL1\n -pub fn flat_keyed_to_vec(mut file: File) -> Result> { - let mut content = String::new(); - file.read_to_string(&mut content) - .map_err(|e| Error::with_cause(ReadFailed("FIXME: read_string_from".to_string()), e))?; - - let mut v = Vec::new(); - for line in content.lines() { - let parts: Vec<&str> = line.split(' ').collect(); - if parts.len() == 2 { - if let Ok(i) = parts[1].parse::() { - v.push((parts[0].to_string(), i)); - } - } - } - Ok(v) -} - -// Flat keyed -// KEY0 VAL0\n -// KEY1 VAL1\n -pub fn flat_keyed_to_hashmap(mut file: File) -> Result> { - let mut content = String::new(); - file.read_to_string(&mut content) - .map_err(|e| Error::with_cause(ReadFailed("FIXME: read_string_from".to_string()), e))?; - - let mut h = HashMap::new(); - for line in content.lines() { - let parts: Vec<&str> = line.split(' ').collect(); - if parts.len() == 2 { - if let Ok(i) = parts[1].parse::() { - h.insert(parts[0].to_string(), i); - } - } - } - Ok(h) -} - -// Nested keyed -// KEY0 SUB_KEY0=VAL00 SUB_KEY1=VAL01... -// KEY1 SUB_KEY0=VAL10 SUB_KEY1=VAL11... -pub fn nested_keyed_to_hashmap(mut file: File) -> Result>> { - let mut content = String::new(); - file.read_to_string(&mut content) - .map_err(|e| Error::with_cause(ReadFailed("FIXME: read_string_from".to_string()), e))?; - - let mut h = HashMap::new(); - for line in content.lines() { - let parts: Vec<&str> = line.split(' ').collect(); - if parts.is_empty() { - continue; - } - let mut th = HashMap::new(); - for item in parts[1..].iter() { - let fields: Vec<&str> = item.split('=').collect(); - if fields.len() == 2 { - if let Ok(i) = fields[1].parse::() { - th.insert(fields[0].to_string(), i); - } - } - } - h.insert(parts[0].to_string(), th); - } - - Ok(h) -} - -fn read_from(mut file: File) -> Result -where - T: FromStr, - ::Err: 'static + Send + Sync + std::error::Error, -{ - let mut string = String::new(); - match file.read_to_string(&mut string) { - Ok(_) => string - .trim() - .parse::() - .map_err(|e| Error::with_cause(ParseError, e)), - Err(e) => Err(Error::with_cause( - ReadFailed("FIXME: can't get path in fn read_from".to_string()), - e, - )), - } -} - -fn read_string_from(mut file: File) -> Result { - let mut string = String::new(); - match file.read_to_string(&mut string) { - Ok(_) => Ok(string.trim().to_string()), - Err(e) => Err(Error::with_cause( - ReadFailed("FIXME: can't get path in fn read_string_from".to_string()), - e, - )), - } -} - -/// read and parse an u64 data -fn read_u64_from(file: File) -> Result { - read_from::(file) -} - -/// read and parse an i64 data -fn read_i64_from(file: File) -> Result { - read_from::(file) -} +pub mod fs; diff --git a/tests/builder.rs b/tests/builder.rs index afce133..622f446 100644 --- a/tests/builder.rs +++ b/tests/builder.rs @@ -5,19 +5,19 @@ // //! Some simple tests covering the builder pattern for control groups. -use cgroups_rs::blkio::*; -use cgroups_rs::cgroup_builder::*; -use cgroups_rs::cpu::*; -use cgroups_rs::devices::*; -use cgroups_rs::hugetlb::*; -use cgroups_rs::memory::*; -use cgroups_rs::net_cls::*; -use cgroups_rs::pid::*; -use cgroups_rs::*; +use cgroups_rs::fs::blkio::*; +use cgroups_rs::fs::cgroup_builder::*; +use cgroups_rs::fs::cpu::*; +use cgroups_rs::fs::devices::*; +use cgroups_rs::fs::hugetlb::*; +use cgroups_rs::fs::memory::*; +use cgroups_rs::fs::net_cls::*; +use cgroups_rs::fs::pid::*; +use cgroups_rs::fs::*; #[test] pub fn test_cpu_res_build() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cg: Cgroup = CgroupBuilder::new("test_cpu_res_build") .cpu() .shares(85) @@ -36,7 +36,7 @@ pub fn test_cpu_res_build() { #[test] pub fn test_memory_res_build() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cg: Cgroup = CgroupBuilder::new("test_memory_res_build") .memory() .kernel_memory_limit(128 * 1024 * 1024) @@ -61,7 +61,7 @@ pub fn test_memory_res_build() { #[test] pub fn test_pid_res_build() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cg: Cgroup = CgroupBuilder::new("test_pid_res_build") .pid() .maximum_number_of_processes(MaxValue::Value(123)) @@ -81,7 +81,7 @@ pub fn test_pid_res_build() { #[test] #[ignore] // ignore this test for now, not sure why my kernel doesn't like it pub fn test_devices_res_build() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cg: Cgroup = CgroupBuilder::new("test_devices_res_build") .devices() .device(1, 6, DeviceType::Char, true, vec![DevicePermissions::Read]) @@ -108,7 +108,7 @@ pub fn test_devices_res_build() { #[test] pub fn test_network_res_build() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); if h.v2() { // FIXME add cases for v2 return; @@ -130,7 +130,7 @@ pub fn test_network_res_build() { #[test] pub fn test_hugepages_res_build() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); if h.v2() { // FIXME add cases for v2 return; @@ -153,7 +153,7 @@ pub fn test_hugepages_res_build() { #[test] #[ignore] // high version kernel not support `blkio.weight` pub fn test_blkio_res_build() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cg: Cgroup = CgroupBuilder::new("test_blkio_res_build") .blkio() .weight(100) diff --git a/tests/cgroup.rs b/tests/cgroup.rs index c63be25..7c743cb 100644 --- a/tests/cgroup.rs +++ b/tests/cgroup.rs @@ -5,20 +5,22 @@ // //! Simple unit tests about the control groups system. -use cgroups_rs::cgroup::{ - CGROUP_MODE_DOMAIN, CGROUP_MODE_DOMAIN_INVALID, CGROUP_MODE_DOMAIN_THREADED, - CGROUP_MODE_THREADED, -}; -use cgroups_rs::memory::MemController; -use cgroups_rs::Controller; -use cgroups_rs::{Cgroup, CgroupPid, Subsystem}; use std::process::Command; use std::thread::sleep; use std::time::Duration; +use cgroups_rs::fs::cgroup::{ + CGROUP_MODE_DOMAIN, CGROUP_MODE_DOMAIN_INVALID, CGROUP_MODE_DOMAIN_THREADED, + CGROUP_MODE_THREADED, +}; +use cgroups_rs::fs::memory::MemController; +use cgroups_rs::fs::Controller; +use cgroups_rs::fs::{Cgroup, Subsystem}; +use cgroups_rs::CgroupPid; + #[test] fn test_procs_iterator_cgroup() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let pid = libc::pid_t::from(nix::unistd::getpid()) as u64; let cg = Cgroup::new(h, String::from("test_procs_iterator_cgroup")).unwrap(); { @@ -42,10 +44,10 @@ fn test_procs_iterator_cgroup() { #[test] fn test_tasks_iterator_cgroup_v1() { - if cgroups_rs::hierarchies::is_cgroup2_unified_mode() { + if cgroups_rs::fs::hierarchies::is_cgroup2_unified_mode() { return; } - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let pid = libc::pid_t::from(nix::unistd::getpid()) as u64; let cg = Cgroup::new(h, String::from("test_tasks_iterator_cgroup_v1")).unwrap(); { @@ -69,23 +71,23 @@ fn test_tasks_iterator_cgroup_v1() { #[test] fn test_tasks_iterator_cgroup_threaded_mode() { - if !cgroups_rs::hierarchies::is_cgroup2_unified_mode() { + if !cgroups_rs::fs::hierarchies::is_cgroup2_unified_mode() { return; } let pid = libc::pid_t::from(nix::unistd::getpid()) as u64; let cg = Cgroup::new( - cgroups_rs::hierarchies::auto(), + cgroups_rs::fs::hierarchies::auto(), String::from("test_tasks_iterator_cgroup_threaded_mode"), ) .unwrap(); let cg_threaded_sub1 = Cgroup::new_with_specified_controllers( - cgroups_rs::hierarchies::auto(), + cgroups_rs::fs::hierarchies::auto(), String::from("test_tasks_iterator_cgroup_threaded_mode/threaded_sub1"), Some(vec![String::from("cpuset"), String::from("cpu")]), ) .unwrap(); let cg_threaded_sub2 = Cgroup::new_with_specified_controllers( - cgroups_rs::hierarchies::auto(), + cgroups_rs::fs::hierarchies::auto(), String::from("test_tasks_iterator_cgroup_threaded_mode/threaded_sub2"), Some(vec![String::from("cpuset"), String::from("cpu")]), ) @@ -163,10 +165,10 @@ fn test_tasks_iterator_cgroup_threaded_mode() { #[test] fn test_kill_cgroup() { - if !cgroups_rs::hierarchies::is_cgroup2_unified_mode() { + if !cgroups_rs::fs::hierarchies::is_cgroup2_unified_mode() { return; } - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cg = Cgroup::new(h, String::from("test_kill_cgroup")).unwrap(); { // Spawn a proc, don't want to getpid(2) here. @@ -206,10 +208,10 @@ fn test_kill_cgroup() { #[test] fn test_cgroup_with_relative_paths() { - if cgroups_rs::hierarchies::is_cgroup2_unified_mode() { + if cgroups_rs::fs::hierarchies::is_cgroup2_unified_mode() { return; } - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cgroup_root = h.root(); let cgroup_name = "test_cgroup_with_relative_paths"; @@ -247,10 +249,10 @@ fn test_cgroup_with_relative_paths() { #[test] fn test_cgroup_v2() { - if !cgroups_rs::hierarchies::is_cgroup2_unified_mode() { + if !cgroups_rs::fs::hierarchies::is_cgroup2_unified_mode() { return; } - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cg = Cgroup::new(h, String::from("test_v2")).unwrap(); let mem_controller: &MemController = cg.controller_of().unwrap(); diff --git a/tests/cpu.rs b/tests/cpu.rs index 2b6afb4..b3db5e1 100644 --- a/tests/cpu.rs +++ b/tests/cpu.rs @@ -4,12 +4,12 @@ // //! Simple unit tests about the CPU control groups system. -use cgroups_rs::cpu::CpuController; -use cgroups_rs::Cgroup; +use cgroups_rs::fs::cpu::CpuController; +use cgroups_rs::fs::Cgroup; #[test] fn test_cfs_quota_and_periods() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cg = Cgroup::new(h, String::from("test_cfs_quota_and_periods")).unwrap(); let cpu_controller: &CpuController = cg.controller_of().unwrap(); diff --git a/tests/cpuset.rs b/tests/cpuset.rs index cecfb63..3045827 100644 --- a/tests/cpuset.rs +++ b/tests/cpuset.rs @@ -3,16 +3,16 @@ // // SPDX-License-Identifier: Apache-2.0 or MIT // - -use cgroups_rs::cpuset::CpuSetController; -use cgroups_rs::error::ErrorKind; -use cgroups_rs::{Cgroup, CgroupPid}; - use std::fs; +use cgroups_rs::fs::cpuset::CpuSetController; +use cgroups_rs::fs::error::ErrorKind; +use cgroups_rs::fs::Cgroup; +use cgroups_rs::CgroupPid; + #[test] fn test_cpuset_memory_pressure_root_cg() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cg = Cgroup::new(h, String::from("test_cpuset_memory_pressure_root_cg")).unwrap(); { let cpuset: &CpuSetController = cg.controller_of().unwrap(); @@ -26,7 +26,7 @@ fn test_cpuset_memory_pressure_root_cg() { #[test] fn test_cpuset_set_cpus() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cg = Cgroup::new(h, String::from("test_cpuset_set_cpus")).unwrap(); { let cpuset: &CpuSetController = cg.controller_of().unwrap(); @@ -63,7 +63,7 @@ fn test_cpuset_set_cpus() { #[test] fn test_cpuset_set_cpus_add_task() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cg = Cgroup::new(h, String::from("test_cpuset_set_cpus_add_task/sub-dir")).unwrap(); let cpuset: &CpuSetController = cg.controller_of().unwrap(); diff --git a/tests/devices.rs b/tests/devices.rs index 83bb309..bfccc20 100644 --- a/tests/devices.rs +++ b/tests/devices.rs @@ -6,17 +6,17 @@ //! Integration tests about the devices subsystem -use cgroups_rs::devices::{DevicePermissions, DeviceType, DevicesController}; -use cgroups_rs::{Cgroup, DeviceResource}; +use cgroups_rs::fs::devices::{DevicePermissions, DeviceType, DevicesController}; +use cgroups_rs::fs::{Cgroup, DeviceResource}; #[test] fn test_devices_parsing() { // now only v2 - if cgroups_rs::hierarchies::is_cgroup2_unified_mode() { + if cgroups_rs::fs::hierarchies::is_cgroup2_unified_mode() { return; } - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cg = Cgroup::new(h, String::from("test_devices_parsing")).unwrap(); { let devices: &DevicesController = cg.controller_of().unwrap(); diff --git a/tests/hugetlb.rs b/tests/hugetlb.rs index 4319ee5..746237f 100644 --- a/tests/hugetlb.rs +++ b/tests/hugetlb.rs @@ -4,19 +4,19 @@ // //! Integration tests about the hugetlb subsystem -use cgroups_rs::error::*; -use cgroups_rs::hugetlb::{self, HugeTlbController}; -use cgroups_rs::Cgroup; +use cgroups_rs::fs::error::*; +use cgroups_rs::fs::hugetlb::{self, HugeTlbController}; +use cgroups_rs::fs::Cgroup; use std::fs; #[test] fn test_hugetlb_sizes() { // now only v2 - if cgroups_rs::hierarchies::is_cgroup2_unified_mode() { + if cgroups_rs::fs::hierarchies::is_cgroup2_unified_mode() { return; } - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cg = Cgroup::new(h, String::from("test_hugetlb_sizes")).unwrap(); { let hugetlb_controller: &HugeTlbController = cg.controller_of().unwrap(); diff --git a/tests/memory.rs b/tests/memory.rs index 14374cf..6a158e4 100644 --- a/tests/memory.rs +++ b/tests/memory.rs @@ -4,13 +4,12 @@ // //! Integration tests about the hugetlb subsystem -use cgroups_rs::memory::{MemController, SetMemory}; -use cgroups_rs::Controller; -use cgroups_rs::{Cgroup, MaxValue}; +use cgroups_rs::fs::memory::{MemController, SetMemory}; +use cgroups_rs::fs::{Cgroup, Controller, MaxValue}; #[test] fn test_disable_oom_killer() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cg = Cgroup::new(h, String::from("test_disable_oom_killer")).unwrap(); { let mem_controller: &MemController = cg.controller_of().unwrap(); @@ -35,7 +34,7 @@ fn test_disable_oom_killer() { #[test] fn set_kmem_limit_v1() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); if h.v2() { return; } @@ -50,7 +49,7 @@ fn set_kmem_limit_v1() { #[test] fn set_mem_v2() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); if !h.v2() { return; } diff --git a/tests/pids.rs b/tests/pids.rs index c555ade..01a64d5 100644 --- a/tests/pids.rs +++ b/tests/pids.rs @@ -5,18 +5,15 @@ // //! Integration tests about the pids subsystem -use cgroups_rs::pid::PidController; -use cgroups_rs::Controller; -use cgroups_rs::{Cgroup, MaxValue}; - +use cgroups_rs::fs::pid::PidController; +use cgroups_rs::fs::{Cgroup, Controller, MaxValue}; +use libc::pid_t; use nix::sys::wait::{waitpid, WaitStatus}; use nix::unistd::{fork, ForkResult}; -use libc::pid_t; - #[test] fn create_and_delete_cgroup() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cg = Cgroup::new(h, String::from("create_and_delete_cgroup")).unwrap(); { let pidcontroller: &PidController = cg.controller_of().unwrap(); @@ -30,7 +27,7 @@ fn create_and_delete_cgroup() { #[test] fn test_pids_current_is_zero() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cg = Cgroup::new(h, String::from("test_pids_current_is_zero")).unwrap(); { let pidcontroller: &PidController = cg.controller_of().unwrap(); @@ -42,7 +39,7 @@ fn test_pids_current_is_zero() { #[test] fn test_pids_events_is_zero() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cg = Cgroup::new(h, String::from("test_pids_events_is_zero")).unwrap(); { let pidcontroller: &PidController = cg.controller_of().unwrap(); @@ -55,7 +52,7 @@ fn test_pids_events_is_zero() { #[test] fn test_pid_events_is_not_zero() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cg = Cgroup::new(h, String::from("test_pid_events_is_not_zero")).unwrap(); { let pids: &PidController = cg.controller_of().unwrap(); diff --git a/tests/resources.rs b/tests/resources.rs index d983cce..76453e6 100644 --- a/tests/resources.rs +++ b/tests/resources.rs @@ -5,12 +5,12 @@ // //! Integration test about setting resources using `apply()` -use cgroups_rs::pid::PidController; -use cgroups_rs::{Cgroup, MaxValue, PidResources, Resources}; +use cgroups_rs::fs::pid::PidController; +use cgroups_rs::fs::{Cgroup, MaxValue, PidResources, Resources}; #[test] fn pid_resources() { - let h = cgroups_rs::hierarchies::auto(); + let h = cgroups_rs::fs::hierarchies::auto(); let cg = Cgroup::new(h, String::from("pid_resources")).unwrap(); { let res = Resources { From 1250cbe18237cd0ff88a4231ab8e128d7c8a775e Mon Sep 17 00:00:00 2001 From: Xuewei Niu Date: Fri, 11 Jul 2025 20:55:23 +0800 Subject: [PATCH 2/6] manager: Introduce FsManager `Manager` is a trait to unify the interface of cgroups. It is designed for OCI containers. Its `set()` takes Linux resources of the OCI spec to set cgroups. The `FsManager`, the concrete implementation of `Manager`, manipulates cgroups through cgroupfs, and supports both cgroups v1 and v2. Signed-off-by: Xuewei Niu --- Cargo.toml | 2 + src/fs/freezer.rs | 13 +- src/fs/mod.rs | 21 +- src/lib.rs | 42 ++ src/manager/conv.rs | 69 +++ src/manager/error.rs | 20 + src/manager/fs.rs | 1088 ++++++++++++++++++++++++++++++++++++++++++ src/manager/mod.rs | 77 +++ src/stats.rs | 156 ++++++ 9 files changed, 1456 insertions(+), 32 deletions(-) create mode 100644 src/manager/conv.rs create mode 100644 src/manager/error.rs create mode 100644 src/manager/fs.rs create mode 100644 src/manager/mod.rs create mode 100644 src/stats.rs diff --git a/Cargo.toml b/Cargo.toml index db0f7c5..4448783 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,11 @@ nix = { version = "0.25.0", default-features = false, features = ["event", "fs", libc = "0.2" serde = { version = "1.0", features = ["derive"], optional = true } thiserror = "1" +oci-spec = { version = "0.8.1", optional = true } [dev-dependencies] libc = "0.2.76" [features] default = [] +oci = ["oci-spec"] diff --git a/src/fs/freezer.rs b/src/fs/freezer.rs index 907dffb..036f6bb 100644 --- a/src/fs/freezer.rs +++ b/src/fs/freezer.rs @@ -13,8 +13,8 @@ use std::path::PathBuf; use crate::fs::error::ErrorKind::*; use crate::fs::error::*; - use crate::fs::{ControllIdentifier, ControllerInternal, Controllers, Resources, Subsystem}; +use crate::FreezerState; /// A controller that allows controlling the `freezer` subsystem of a Cgroup. /// @@ -31,17 +31,6 @@ pub struct FreezerController { v2: bool, } -/// The current state of the control group -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum FreezerState { - /// The processes in the control group are _not_ frozen. - Thawed, - /// The processes in the control group are in the processes of being frozen. - Freezing, - /// The processes in the control group are frozen. - Frozen, -} - impl ControllerInternal for FreezerController { fn control_type(&self) -> Controllers { Controllers::Freezer diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 81aa115..f5255a4 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -245,6 +245,7 @@ mod sealed { } pub(crate) use crate::fs::sealed::{ControllerInternal, CustomizedAttribute}; +use crate::CgroupPid; /// A Controller is a subsystem attached to the control group. /// @@ -771,26 +772,6 @@ pub struct Resources { pub blkio: BlkIoResources, } -/// A structure representing a `pid`. Currently implementations exist for `u64` and -/// `std::process::Child`. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct CgroupPid { - /// The process identifier - pub pid: u64, -} - -impl From for CgroupPid { - fn from(u: u64) -> CgroupPid { - CgroupPid { pid: u } - } -} - -impl From<&std::process::Child> for CgroupPid { - fn from(u: &std::process::Child) -> CgroupPid { - CgroupPid { pid: u.id() as u64 } - } -} - impl Subsystem { fn enter(self, path: &Path) -> Self { match self { diff --git a/src/lib.rs b/src/lib.rs index 2528ecf..4bb4a3b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,3 +5,45 @@ // pub mod fs; +#[cfg(feature = "oci")] +pub mod manager; +#[cfg(feature = "oci")] +pub use manager::{FsManager, Manager}; +pub mod stats; +pub use stats::CgroupStats; + +/// The maximum value for CPU shares in cgroups v1 +pub const CPU_SHARES_V1_MAX: u64 = 262144; +/// The maximum value for CPU weight in cgroups v2 +pub const CPU_WEIGHT_V2_MAX: u64 = 10000; + +/// The current state of the control group +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum FreezerState { + /// The processes in the control group are _not_ frozen. + Thawed, + /// The processes in the control group are in the processes of being frozen. + Freezing, + /// The processes in the control group are frozen. + Frozen, +} + +/// A structure representing a `pid`. Currently implementations exist for `u64` and +/// `std::process::Child`. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +pub struct CgroupPid { + /// The process identifier + pub pid: u64, +} + +impl From for CgroupPid { + fn from(u: u64) -> CgroupPid { + CgroupPid { pid: u } + } +} + +impl From<&std::process::Child> for CgroupPid { + fn from(u: &std::process::Child) -> CgroupPid { + CgroupPid { pid: u.id() as u64 } + } +} diff --git a/src/manager/conv.rs b/src/manager/conv.rs new file mode 100644 index 0000000..168c40c --- /dev/null +++ b/src/manager/conv.rs @@ -0,0 +1,69 @@ +// Copyright (c) 2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +use crate::manager::error::{Error, Result}; +use crate::{CPU_SHARES_V1_MAX, CPU_WEIGHT_V2_MAX}; + +// Converts CPU shares, used by cgroup v1, to CPU weight, used by cgroup +// v2. +// +// Cgroup v1 CPU shares has a range of [2^1...2^18], i.e. [2...262144], +// and the default value is 1024. +// +// Cgroup v2 CPU weight has a range of [10^0...10^4], i.e. [1...10000], +// and the default value is 100. +pub(crate) fn cpu_shares_to_cgroup_v2(shares: u64) -> u64 { + if shares == 0 { + return 0; + } + if shares <= 2 { + return 1; + } + if shares >= CPU_SHARES_V1_MAX { + return CPU_WEIGHT_V2_MAX; + } + + (((shares - 2) * 9999) / 262142) + 1 +} + +// ConvertMemorySwapToCgroupV2Value converts MemorySwap value from OCI spec +// for use by cgroup v2 drivers. A conversion is needed since +// Resources.MemorySwap is defined as memory+swap combined, while in cgroup +// v2 swap is a separate value. +pub(crate) fn memory_swap_to_cgroup_v2(memswap_limit: i64, mem_limit: i64) -> Result { + // For compatibility with cgroup1 controller, set swap to unlimited in + // case the memory is set to unlimited, and swap is not explicitly set, + // treating the request as "set both memory and swap to unlimited". + if mem_limit == -1 && memswap_limit == 0 { + return Ok(-1); + } + + // -1 is "max", 0 is "unset", so treat as is + if memswap_limit == -1 || memswap_limit == 0 { + return Ok(memswap_limit); + } + + // Unlimited memory, so treat swap as is. + if mem_limit == -1 { + return Ok(memswap_limit); + } + + // Unset or unknown memory, can't calculate swap. + if mem_limit == 0 { + return Err(Error::InvalidLinuxResource); + } + + // Does not make sense to subtract a negative value. + if mem_limit < 0 { + return Err(Error::InvalidLinuxResource); + } + + // Sanity check. + if memswap_limit < mem_limit { + return Err(Error::InvalidLinuxResource); + } + + Ok(memswap_limit - mem_limit) +} diff --git a/src/manager/error.rs b/src/manager/error.rs new file mode 100644 index 0000000..5b4b3cb --- /dev/null +++ b/src/manager/error.rs @@ -0,0 +1,20 @@ +// Copyright (c) 2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +use crate::fs::error::Error as CgroupfsError; + +pub type Result = std::result::Result; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("invalid argument")] + InvalidArgument, + + #[error("invalid linux resource")] + InvalidLinuxResource, + + #[error("cgroupfs error: {0}")] + Cgroupfs(#[from] CgroupfsError), +} diff --git a/src/manager/fs.rs b/src/manager/fs.rs new file mode 100644 index 0000000..b1316b2 --- /dev/null +++ b/src/manager/fs.rs @@ -0,0 +1,1088 @@ +// Copyright (c) 2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +use std::collections::HashMap; +use std::fs; +use std::path::Path; +use std::str::FromStr; + +use oci_spec::runtime::{ + LinuxBlockIo, LinuxCpu, LinuxDeviceCgroup, LinuxHugepageLimit, LinuxMemory, LinuxNetwork, + LinuxPids, LinuxResources, +}; + +use crate::fs::blkio::{BlkIoController, BlkIoData, IoService, IoStat}; +use crate::fs::cgroup::UNIFIED_MOUNTPOINT; +use crate::fs::cpu::CpuController; +use crate::fs::cpuacct::CpuAcctController; +use crate::fs::cpuset::CpuSetController; +use crate::fs::devices::{DevicePermissions, DeviceType, DevicesController}; +use crate::fs::error::{Error as FsError, ErrorKind as FsErrorKind, Result as FsResult}; +use crate::fs::freezer::FreezerController; +use crate::fs::hugetlb::HugeTlbController; +use crate::fs::memory::MemController; +use crate::fs::net_cls::NetClsController; +use crate::fs::net_prio::NetPrioController; +use crate::fs::pid::PidController; +use crate::fs::{hierarchies, Cgroup, ControllIdentifier, Controller, MaxValue, Subsystem}; +use crate::manager::error::Error; +use crate::manager::{conv, Manager, Result}; +use crate::stats::{ + BlkioCgroupStats, BlkioStat, CpuAcctStats, CpuCgroupStats, CpuThrottlingStats, + HugeTlbCgroupStats, HugeTlbStat, MemoryCgroupStats, MemoryStats, PidsCgroupStats, +}; +use crate::{CgroupPid, CgroupStats, FreezerState}; + +const CGROUP_PATH: &str = "/proc/self/cgroup"; +const MOUNTINFO_PATH: &str = "/proc/self/mountinfo"; + +/// FsManager manages cgroups using the cgroup filesystem (cgroupfs). +/// +/// This manager deals with `LinuxResources` conformed to the OCI runtime +/// specification, so that it allows users not to do type conversions. +#[derive(Debug, Clone)] +pub struct FsManager { + /// Cgroup subsystem paths read from `/proc/self/cgroup` + /// - cgroup v1: -> + /// - cgroup v2: "" -> + paths: HashMap, + /// Cgroup mountpoints read from `/proc/self/mountinfo`. + mounts: HashMap, + /// Base path of the cgroup filesystem, the complete path would be: + /// - cgroup v1: "/sys/fs/cgroup//" + /// - cgroup v2: "/sys/fs/cgroup/" + base: String, + /// Cgroup managed by this manager. + cgroup: Cgroup, +} + +impl FsManager { + /// Check if the cgroup exists or not. + pub fn exists(&self) -> bool { + self.cgroup.exists() + } + + /// Create an instance of FsManager. The cgroups won't be created until + /// `apply()` is called. + pub fn new(base: &str) -> Result { + let paths = parse_cgroup_subsystems()?; + let mounts = parse_cgroup_mountinfo(&paths)?; + let cgroup = Cgroup::load(hierarchies::auto(), base); + let base = base.to_string(); + + Ok(Self { + paths, + mounts, + base, + cgroup, + }) + } +} + +impl FsManager { + /// Create the cgroups if they are not created yet. + pub(crate) fn create_cgroups(&mut self) -> Result<()> { + if self.exists() { + return Ok(()); + } + self.cgroup.create()?; + Ok(()) + } + + /// Get the subcgroup path, which is useful for Docker-in-Docker (DinD) + /// with cgroup v2, see [1]. + /// + /// 1: https://github.com/kata-containers/kata-containers/issues/10733 + pub fn subcgroup(&self) -> &str { + // Check if we're in a Docker-in-Docker setup by verifying: + // 1. We're using cgroups v2 (which restricts direct process control) + // 2. An "init" subdirectory exists (used by DinD for process + // delegation) + let init_exists = hierarchies::auto() + .root() + .join(&self.base) + .join("init") + .exists(); + let is_dind = self.v2() && init_exists; + + if is_dind { + "/init/" + } else { + "/" + } + } + + fn controller<'a, T>(&'a self) -> FsResult<&'a T> + where + &'a T: From<&'a Subsystem>, + T: Controller + ControllIdentifier, + { + let controller: &T = self + .cgroup + .controller_of() + .ok_or(FsError::new(FsErrorKind::SubsystemsEmpty))?; + + Ok(controller) + } + + fn set_cpuset(&self, linux_cpu: &LinuxCpu) -> Result<()> { + let controller: &CpuSetController = self.controller()?; + + if let Some(cpus) = linux_cpu.cpus() { + controller.set_cpus(cpus)?; + } + + if let Some(mems) = linux_cpu.mems() { + controller.set_mems(mems)?; + } + + Ok(()) + } + + fn set_cpu(&self, linux_cpu: &LinuxCpu) -> Result<()> { + let controller: &CpuController = self.controller()?; + + if let Some(shares) = linux_cpu.shares() { + let shares = if self.v2() { + conv::cpu_shares_to_cgroup_v2(shares) + } else { + shares + }; + if shares != 0 { + controller.set_shares(shares)?; + } + } + + if let Some(quota) = linux_cpu.quota() { + controller.set_cfs_quota(quota)?; + } + + if let Some(period) = linux_cpu.period() { + controller.set_cfs_period(period)?; + } + + if let Some(rt_runtime) = linux_cpu.realtime_runtime() { + controller.set_rt_runtime(rt_runtime)?; + } + + if let Some(rt_period) = linux_cpu.realtime_period() { + controller.set_rt_period_us(rt_period)?; + } + + Ok(()) + } + + fn set_mem_and_memswap_v1(&self, limit: i64, mut swap_limit: i64) -> Result<()> { + let controller: &MemController = self.controller()?; + + // If the memory update is set to -1 and the swap is not set + // explicitly, we should also set swap to -1, it means + // unlimited memory. + if limit == -1 && swap_limit == 0 { + swap_limit = -1; + } + + if limit != 0 && swap_limit != 0 { + let memory = controller.memory_stat(); + let limit_actual = memory.limit_in_bytes; + + // When update memory limit, we should adapt the write sequence + // for memory and swap memory, so it won't fail because the new + // value and the old value don't fit kernel's validation. + if swap_limit == -1 || limit_actual < swap_limit { + controller.set_memswap_limit(swap_limit)?; + controller.set_limit(limit)?; + + return Ok(()); + } + } + + if limit != 0 { + controller.set_limit(limit)?; + } + if swap_limit != 0 { + controller.set_memswap_limit(swap_limit)?; + } + + Ok(()) + } + + fn set_memory_v1(&self, linux_memory: &LinuxMemory) -> Result<()> { + let controller: &MemController = self.controller()?; + + let mem_limit = linux_memory.limit().unwrap_or(0); + let memswap_limit = linux_memory.swap().unwrap_or(0); + + self.set_mem_and_memswap_v1(mem_limit, memswap_limit)?; + + if let Some(reservation) = linux_memory.reservation() { + controller.set_soft_limit(reservation)?; + } + + if linux_memory.disable_oom_killer().unwrap_or_default() { + controller.disable_oom_killer()?; + } + + if let Some(swappiness) = linux_memory.swappiness() { + if swappiness <= 100 { + controller.set_swappiness(swappiness)?; + } else { + return Err(Error::InvalidLinuxResource); + }; + } + + Ok(()) + } + + fn set_memory_v2(&self, linux_memory: &LinuxMemory) -> Result<()> { + let controller: &MemController = self.controller()?; + + if linux_memory.reservation().is_none() + && linux_memory.limit().is_none() + && linux_memory.swap().is_none() + { + return Ok(()); + } + + let mem_limit = linux_memory.limit().unwrap_or(0); + let memswap_limit = linux_memory.swap().unwrap_or(0); + + // Check memory usage + if mem_limit <= 0 && memswap_limit <= 0 { + return Ok(()); + } + + let memory_stat = controller.memory_stat(); + let usage_actual = memory_stat.usage_in_bytes; + + // Rejecting: memory+swap limit <= usage + if memswap_limit > 0 && memswap_limit as u64 <= usage_actual { + return Err(Error::InvalidLinuxResource); + } + + // Rejecting: memory limit <= usage + if mem_limit > 0 && mem_limit as u64 <= usage_actual { + return Err(Error::InvalidLinuxResource); + } + + let swap_limit = conv::memory_swap_to_cgroup_v2(memswap_limit, mem_limit)?; + controller.set_memswap_limit(swap_limit)?; + + if mem_limit != 0 { + controller.set_limit(mem_limit)?; + } + + if let Some(reservation) = linux_memory.reservation() { + controller.set_soft_limit(reservation)?; + } + + Ok(()) + } + + /// Set memory resources. + /// + /// Ignore kernel memory and kernel memory TCP, as runc does, see [1]. + /// + /// 1: https://github.com/opencontainers/cgroups/blob/d36d371fe756a30d2e21d83c6b42e86af77bf4a2/fs/memory.go#L36 + fn set_memory(&self, linux_memory: &LinuxMemory) -> Result<()> { + if self.v2() { + self.set_memory_v2(linux_memory)?; + } else { + self.set_memory_v1(linux_memory)?; + } + + Ok(()) + } + + fn set_pids(&self, pids: &LinuxPids) -> Result<()> { + let controller: &PidController = self.controller()?; + let value = if pids.limit() > 0 { + MaxValue::Value(pids.limit()) + } else { + MaxValue::Max + }; + controller.set_pid_max(value)?; + + Ok(()) + } + + fn set_blkio(&self, blkio: &LinuxBlockIo) -> Result<()> { + let controller: &BlkIoController = self.controller()?; + + if let Some(weight) = blkio.weight() { + controller.set_weight(weight as u64)?; + } + + if let Some(leaf_weight) = blkio.leaf_weight() { + controller.set_leaf_weight(leaf_weight as u64)?; + } + + if let Some(devices) = blkio.weight_device() { + for device in devices.iter() { + let major = device.major() as u64; + let minor = device.minor() as u64; + if let Some(weight) = device.weight() { + controller.set_weight_for_device(major, minor, weight as u64)?; + } + if let Some(leaf_weight) = device.leaf_weight() { + controller.set_leaf_weight_for_device(major, minor, leaf_weight as u64)?; + } + } + } + + if let Some(devices) = blkio.throttle_read_bps_device() { + for device in devices.iter() { + let major = device.major() as u64; + let minor = device.minor() as u64; + let rate = device.rate(); + controller.throttle_read_bps_for_device(major, minor, rate)?; + } + } + + if let Some(devices) = blkio.throttle_write_bps_device() { + for device in devices.iter() { + let major = device.major() as u64; + let minor = device.minor() as u64; + let rate = device.rate(); + controller.throttle_write_bps_for_device(major, minor, rate)?; + } + } + + if let Some(devices) = blkio.throttle_read_iops_device() { + for device in devices.iter() { + let major = device.major() as u64; + let minor = device.minor() as u64; + let rate = device.rate(); + controller.throttle_read_iops_for_device(major, minor, rate)?; + } + } + + if let Some(devices) = blkio.throttle_write_iops_device() { + for device in devices.iter() { + let major = device.major() as u64; + let minor = device.minor() as u64; + let rate = device.rate(); + controller.throttle_write_iops_for_device(major, minor, rate)?; + } + } + + Ok(()) + } + + fn set_hugepages(&self, hugepage_limits: &[LinuxHugepageLimit]) -> Result<()> { + let controller: &HugeTlbController = self.controller()?; + + for limit in hugepage_limits.iter() { + // ignore not supported page size + if !controller.size_supported(limit.page_size()) { + continue; + } + let page_size = limit.page_size(); + let limit = limit.limit() as u64; + controller.set_limit_in_bytes(page_size, limit)?; + } + + Ok(()) + } + + fn set_network(&self, network: &LinuxNetwork) -> Result<()> { + if let Some(class_id) = network.class_id() { + let controller: &NetClsController = self.controller()?; + controller.set_class(class_id as u64)?; + } + + if let Some(priorities) = network.priorities() { + let controller: &NetPrioController = self.controller()?; + for priority in priorities.iter() { + let eif = priority.name(); + let prio = priority.priority() as u64; + controller.set_if_prio(eif, prio)?; + } + } + + Ok(()) + } + + fn set_devices(&self, devices: &[LinuxDeviceCgroup]) -> Result<()> { + let controller: &DevicesController = self.controller()?; + + for device in devices.iter() { + let devtype = + DeviceType::from_char(device.typ().unwrap_or_default().as_str().chars().next()) + .ok_or(Error::InvalidLinuxResource)?; + + let perm = device + .access() + .as_ref() + .unwrap_or(&String::new()) + .chars() + .filter_map(|perm| match perm { + 'r' => Some(DevicePermissions::Read), + 'w' => Some(DevicePermissions::Write), + 'm' => Some(DevicePermissions::MkNod), + _ => None, + }) + .collect::>(); + + let major = device.major().unwrap_or(0); + let minor = device.minor().unwrap_or(0); + + if device.allow() { + controller.allow_device(devtype, major, minor, &perm)?; + } else { + controller.deny_device(devtype, major, minor, &perm)?; + } + } + + Ok(()) + } + + /// Set the controller topdown from root in cgroup hierarchy. The `f` + /// is going to be applied to: + /// -> root [not included] + /// -> root's child + /// -> ... + /// -> self.cgroup's parent + /// -> self.cgroup [not included] + /// + /// Please see `enable_cpus_topdown()` for more details. + /// + /// Please note that `self.cgroup` is not included. If you really want + /// that, you should do it manually. + fn set_controller_topdown(&self, f: F) -> Result<()> + where + for<'a> &'a T: From<&'a Subsystem>, + T: Controller + ControllIdentifier, + for<'a> F: Fn(&'a T) -> Result<()>, + { + let root = hierarchies::auto().root_control_group(); + let controller: &T = root + .controller_of() + .ok_or(FsError::new(FsErrorKind::SubsystemsEmpty))?; + let root_path = Path::new(controller.path()); + let root_path_str = root_path.to_string_lossy().to_string(); + + let controller: &T = self.controller()?; + let path = Path::new(controller.path()); + + // Push path's ancestors onto a stack, so the stack looks like: + // path's parent, path's grandparent, ..., root + let mut path_stack = vec![]; + for parent in path.ancestors() { + if parent == root_path { + break; + } + path_stack.push(parent); + } + + // Pop from the stack + while let Some(p) = path_stack.pop() { + let relative_path = p + .to_str() + .unwrap() + .trim_start_matches(&root_path_str) + // Makes sure the starting slash is removed + .trim_start_matches("/"); + let cgroup = Cgroup::new(hierarchies::auto(), relative_path)?; + let controller: &T = cgroup + .controller_of() + .ok_or(FsError::new(FsErrorKind::SubsystemsEmpty))?; + f(controller)?; + } + + Ok(()) + } + + fn cpu_acct_stats(&self) -> Result { + let controller: &CpuAcctController = self.controller()?; + let cpu_acct = controller.cpuacct(); + + let user_usage = parse_value_from_tuples(&cpu_acct.stat, "user").unwrap_or_default(); + + let system_usage = parse_value_from_tuples(&cpu_acct.stat, "system").unwrap_or_default(); + + let usage_percpu: Vec = cpu_acct + .usage_percpu + .lines() + .filter_map(|line| line.parse::().ok()) + .collect(); + + Ok(CpuAcctStats { + user_usage, + system_usage, + total_usage: cpu_acct.usage, + usage_percpu, + }) + } + + fn cpu_throttling_stats(&self) -> Result { + let controller: &CpuController = self.controller()?; + let stats = controller.cpu().stat; + + let periods = parse_value_from_tuples(&stats, "nr_periods").unwrap_or_default(); + let throttled_periods = parse_value_from_tuples(&stats, "nr_throttled").unwrap_or_default(); + let throttled_time = parse_value_from_tuples(&stats, "throttled_time").unwrap_or_default(); + + Ok(CpuThrottlingStats { + periods, + throttled_periods, + throttled_time, + }) + } + + fn cpu_cgroup_stats(&self) -> CpuCgroupStats { + CpuCgroupStats { + cpu_acct: self.cpu_acct_stats().ok(), + cpu_throttling: self.cpu_throttling_stats().ok(), + } + } + + fn memory_stats(&self) -> Result { + let controller: &MemController = self.controller()?; + let memory_stats = controller.memory_stat(); + + Ok(MemoryStats { + usage: memory_stats.usage_in_bytes, + max_usage: memory_stats.max_usage_in_bytes, + limit: memory_stats.limit_in_bytes, + fail_cnt: memory_stats.fail_cnt, + }) + } + + fn memory_swap_stats(&self) -> Result { + let controller: &MemController = self.controller()?; + let memory_swap_stats = controller.memswap(); + + Ok(MemoryStats { + usage: memory_swap_stats.usage_in_bytes, + max_usage: memory_swap_stats.max_usage_in_bytes, + limit: memory_swap_stats.limit_in_bytes, + fail_cnt: memory_swap_stats.fail_cnt, + }) + } + + fn kernel_memory_stats(&self) -> Result { + let controller: &MemController = self.controller()?; + let kmem_stats = controller.kmem_stat(); + + Ok(MemoryStats { + usage: kmem_stats.usage_in_bytes, + max_usage: kmem_stats.max_usage_in_bytes, + limit: kmem_stats.limit_in_bytes, + fail_cnt: kmem_stats.fail_cnt, + }) + } + + fn memory_cgroup_stats(&self) -> MemoryCgroupStats { + let memory = self.memory_stats().ok(); + let memory_swap = self.memory_swap_stats().ok(); + let kernel_memory = self.kernel_memory_stats().ok(); + + let mut memory = MemoryCgroupStats { + memory, + memory_swap, + kernel_memory, + ..Default::default() + }; + + let memory_stats = self + .controller::() + .map(|c| c.memory_stat()) + .ok(); + if let Some(memstats) = &memory_stats { + memory.use_hierarchy = memstats.use_hierarchy == 1; + // Copy items from memstats.stat + memory.cache = memstats.stat.cache; + memory.rss = memstats.stat.rss; + memory.rss_huge = memstats.stat.rss_huge; + memory.shmem = memstats.stat.shmem; + memory.mapped_file = memstats.stat.mapped_file; + memory.dirty = memstats.stat.dirty; + memory.writeback = memstats.stat.writeback; + memory.swap = memstats.stat.swap; + memory.pgpgin = memstats.stat.pgpgin; + memory.pgpgout = memstats.stat.pgpgout; + memory.pgfault = memstats.stat.pgfault; + memory.pgmajfault = memstats.stat.pgmajfault; + memory.inactive_anon = memstats.stat.inactive_anon; + memory.active_anon = memstats.stat.active_anon; + memory.inactive_file = memstats.stat.inactive_file; + memory.active_file = memstats.stat.active_file; + memory.unevictable = memstats.stat.unevictable; + memory.hierarchical_memory_limit = memstats.stat.hierarchical_memory_limit; + memory.hierarchical_memsw_limit = memstats.stat.hierarchical_memsw_limit; + memory.total_cache = memstats.stat.total_cache; + memory.total_rss = memstats.stat.total_rss; + memory.total_rss_huge = memstats.stat.total_rss_huge; + memory.total_shmem = memstats.stat.total_shmem; + memory.total_mapped_file = memstats.stat.total_mapped_file; + memory.total_dirty = memstats.stat.total_dirty; + memory.total_writeback = memstats.stat.total_writeback; + memory.total_swap = memstats.stat.total_swap; + memory.total_pgpgin = memstats.stat.total_pgpgin; + memory.total_pgpgout = memstats.stat.total_pgpgout; + memory.total_pgfault = memstats.stat.total_pgfault; + memory.total_pgmajfault = memstats.stat.total_pgmajfault; + memory.total_inactive_anon = memstats.stat.total_inactive_anon; + memory.total_active_anon = memstats.stat.total_active_anon; + memory.total_inactive_file = memstats.stat.total_inactive_file; + memory.total_active_file = memstats.stat.total_active_file; + memory.total_unevictable = memstats.stat.total_unevictable; + } + + memory + } + + fn pids_cgroup_stats(&self) -> PidsCgroupStats { + let controller: &PidController = match self.controller() { + Ok(controller) => controller, + Err(_) => return PidsCgroupStats::default(), + }; + let current = controller.get_pid_current().unwrap_or_default(); + let limit = controller + .get_pid_max() + .map(|mv| match mv { + MaxValue::Value(limit) => limit, + MaxValue::Max => 0, + }) + .unwrap_or_default(); + + PidsCgroupStats { current, limit } + } + + fn blkio_stats_v1(&self) -> Result { + let controller: &BlkIoController = self.controller()?; + let blkio = controller.blkio(); + + if blkio.io_serviced_recursive.is_empty() { + Ok(BlkioCgroupStats { + io_service_bytes_recursive: BlkioStat::from_io_services( + &blkio.throttle.io_service_bytes, + ), + io_serviced_recursive: BlkioStat::from_io_services(&blkio.throttle.io_serviced), + ..Default::default() + }) + } else { + Ok(BlkioCgroupStats { + io_service_bytes_recursive: BlkioStat::from_io_services( + &blkio.io_service_bytes_recursive, + ), + io_serviced_recursive: BlkioStat::from_io_services(&blkio.io_serviced_recursive), + io_queued_recursive: BlkioStat::from_io_services(&blkio.io_queued_recursive), + io_service_time_recursive: BlkioStat::from_io_services( + &blkio.io_service_time_recursive, + ), + io_wait_time_recursive: BlkioStat::from_io_services(&blkio.io_wait_time_recursive), + io_merged_recursive: BlkioStat::from_io_services(&blkio.io_merged_recursive), + io_time_recursive: BlkioStat::from_blk_io_data(&blkio.time_recursive), + sectors_recursive: BlkioStat::from_blk_io_data(&blkio.sectors_recursive), + }) + } + } + + fn blkio_stats_v2(&self) -> Result { + let controller: &BlkIoController = self.controller()?; + let blkio = controller.blkio(); + + Ok(BlkioCgroupStats { + io_service_bytes_recursive: BlkioStat::from_io_stats(&blkio.io_stat), + ..Default::default() + }) + } + + fn blkio_cgroup_stats(&self) -> BlkioCgroupStats { + if self.v2() { + self.blkio_stats_v2() + } else { + self.blkio_stats_v1() + } + .unwrap_or_default() + } + + fn huge_tlb_cgroup_stats(&self) -> HugeTlbCgroupStats { + let controller: &HugeTlbController = match self.controller() { + Ok(controller) => controller, + Err(_) => return HugeTlbCgroupStats::default(), + }; + + let sizes = controller.get_sizes(); + sizes + .iter() + .map(|s| { + let usage = controller.usage_in_bytes(s).unwrap_or_default(); + let max_usage = controller.max_usage_in_bytes(s).unwrap_or_default(); + let fail_cnt = controller.failcnt(s).unwrap_or_default(); + + let stat = HugeTlbStat { + usage, + max_usage, + fail_cnt, + }; + + (s.to_string(), stat) + }) + .collect() + } +} + +impl Manager for FsManager { + fn add_proc(&mut self, tgid: CgroupPid) -> Result<()> { + self.create_cgroups()?; + self.cgroup.add_task_by_tgid(tgid)?; + Ok(()) + } + + fn add_thread(&mut self, pid: CgroupPid) -> Result<()> { + self.create_cgroups()?; + + self.cgroup.add_task(pid).or_else(|err| { + // Try to add_proc with the pid when threaded cgroup is + // disabled in cgroup v2. + if err.kind() == &FsErrorKind::CgroupMode && self.v2() { + self.add_proc(pid) + } else { + Err(Error::Cgroupfs(err)) + } + }) + } + + fn pids(&self) -> Result> { + Ok(self + .controller::() + .map_err(Error::Cgroupfs)? + .tasks()) + } + + fn freeze(&self, state: FreezerState) -> Result<()> { + let controller: &FreezerController = self.controller()?; + + match state { + FreezerState::Thawed => controller.thaw()?, + FreezerState::Frozen => controller.freeze()?, + FreezerState::Freezing => return Err(Error::InvalidArgument), + } + + Ok(()) + } + + fn destroy(&mut self) -> Result<()> { + if !self.exists() { + return Ok(()); + } + + // Before deleting the cgroup, we should move processes in the + // cgroup to the root cgroup. Otherwise, we'll have a "Device or + // resource busy" error. + if self.v2() { + for tgid in self.cgroup.procs() { + // Ignore all errors as long as the cgroup is deleted. + let _ = self.cgroup.remove_task_by_tgid(tgid); + } + } else { + for pid in self.cgroup.tasks() { + // Ditto. + let _ = self.cgroup.remove_task(pid); + } + } + + self.cgroup.delete()?; + Ok(()) + } + + fn set(&mut self, resources: &LinuxResources) -> Result<()> { + if let Some(cpu) = resources.cpu() { + self.set_cpuset(cpu)?; + self.set_cpu(cpu)?; + } + + if let Some(memory) = resources.memory() { + self.set_memory(memory)?; + } + + if let Some(pid) = resources.pids() { + self.set_pids(pid)?; + } + + if let Some(blkio) = resources.block_io() { + self.set_blkio(blkio)?; + } + + if let Some(hugepage_limits) = resources.hugepage_limits() { + self.set_hugepages(hugepage_limits)?; + } + + if let Some(network) = resources.network() { + self.set_network(network)?; + } + + if let Some(devices) = resources.devices() { + self.set_devices(devices)?; + } + + Ok(()) + } + + fn cgroup_path(&self, subsystem: Option<&str>) -> Result { + if self.v2() { + return Ok(join_path(UNIFIED_MOUNTPOINT, &self.base)); + } + + let subsystem = subsystem + .ok_or_else(|| FsError::new(FsErrorKind::InvalidPath)) + .map_err(Error::Cgroupfs)?; + let path = self + .paths + .get(subsystem) + .ok_or(FsError::new(FsErrorKind::SubsystemsEmpty)) + .map_err(Error::Cgroupfs)?; + + Ok(path.clone()) + } + + fn enable_cpus_topdown(&self, cpus: &str) -> Result<()> { + if cpus.is_empty() { + return Ok(()); + } + + self.set_controller_topdown(|c: &CpuSetController| { + c.set_cpus(cpus).map_err(Error::Cgroupfs) + })?; + + Ok(()) + } + + fn stats(&self) -> CgroupStats { + CgroupStats { + cpu: self.cpu_cgroup_stats(), + memory: self.memory_cgroup_stats(), + pids: self.pids_cgroup_stats(), + blkio: self.blkio_cgroup_stats(), + hugetlb: self.huge_tlb_cgroup_stats(), + } + } + + fn paths(&self) -> &HashMap { + &self.paths + } + + fn mounts(&self) -> &HashMap { + &self.mounts + } + + fn systemd(&self) -> bool { + false + } + + fn v2(&self) -> bool { + self.cgroup.v2() + } +} + +/// Parse cgroup subsystem paths from `/proc/self/cgroup`. +fn parse_cgroup_subsystems() -> Result> { + let mut cgroup_paths = HashMap::new(); + let data = fs::read_to_string(CGROUP_PATH) + .map_err(|err| FsError::with_cause(FsErrorKind::FsError, err)) + .map_err(Error::Cgroupfs)?; + + // Expected line format: `10:memory:/user.slice` + for line in data.lines() { + let parts: Vec<&str> = line.split(':').collect(); + if parts.len() != 3 { + // Ignore corrupt lines + continue; + } + let subsystems = parts[1].split(','); + let path = parts[2]; + subsystems.for_each(|subsystem| { + cgroup_paths.insert(subsystem.to_string(), path.to_string()); + }); + } + + Ok(cgroup_paths) +} + +/// Parse cgroup mount information from `/proc/self/mountinfo`. +fn parse_cgroup_mountinfo(paths: &HashMap) -> Result> { + let mut mounts = HashMap::new(); + let data = fs::read_to_string(MOUNTINFO_PATH) + .map_err(|err| FsError::with_cause(FsErrorKind::FsError, err)) + .map_err(Error::Cgroupfs)?; + + for line in data.lines() { + let parts: Vec<&str> = line.splitn(2, " - ").collect(); + let part1: Vec<&str> = parts[0].split(' ').collect(); + let part2: Vec<&str> = parts[1].split(' ').collect(); + + if part2.len() != 3 { + continue; + } + + let fs_type = part2[0]; + if fs_type != "cgroup" && fs_type != "cgroup2" { + continue; + } + + let super_opts: Vec<&str> = part2[2].split(',').collect(); + for opt in super_opts.iter() { + // If opt matchs the one of cgroup subsystems + if paths.contains_key(*opt) { + let mountpoint = part1[4]; + mounts.insert(opt.to_string(), mountpoint.to_string()); + } + } + } + + Ok(mounts) +} + +pub(crate) fn join_path(base: &str, path: &str) -> String { + let base = Path::new(base); + base.join(path).to_string_lossy().to_string() +} + +/// Parse the value of an item from a tuple string split by whitespace. +/// +/// For example, we have a tuple string like: +/// +/// let tuple_str: &str = "system 100000\nuser 200000"; +/// +/// assert_eq!( +/// parse_value_from_tuples::(tuple_str, "user"), +/// Some(200000), +/// ); +/// assert_eq!( +/// parse_value_from_tuples::(tuple_str, "user1"), +/// None, +/// ); +fn parse_value_from_tuples(tuple_str: &str, item: &str) -> Option +where + T: FromStr, +{ + tuple_str.lines().find_map(|line| { + let mut parts = line.split_whitespace(); + let current_item = parts.next()?; + let value = parts.next()?; + if current_item != item { + return None; + } + value.parse::().ok() + }) +} + +impl BlkioStat { + fn from_io_services(io_services: &[IoService]) -> Vec { + let mut stats = Vec::new(); + + for service in io_services.iter() { + let major = service.major as u64; + let minor = service.minor as u64; + + stats.push(BlkioStat { + major, + minor, + op: "read".to_string(), + value: service.read, + }); + + stats.push(BlkioStat { + major, + minor, + op: "write".to_string(), + value: service.write, + }); + + stats.push(BlkioStat { + major, + minor, + op: "sync".to_string(), + value: service.sync, + }); + + stats.push(BlkioStat { + major, + minor, + op: "async".to_string(), + value: service.r#async, + }); + + stats.push(BlkioStat { + major, + minor, + op: "total".to_string(), + value: service.total, + }); + } + + stats + } + + fn from_io_stats(io_stats: &[IoStat]) -> Vec { + let mut stats = Vec::new(); + + for stat in io_stats.iter() { + let major = stat.major as u64; + let minor = stat.minor as u64; + + stats.push(BlkioStat { + major, + minor, + op: "read".to_string(), + value: stat.rbytes, + }); + + stats.push(BlkioStat { + major, + minor, + op: "write".to_string(), + value: stat.wbytes, + }); + + stats.push(BlkioStat { + major, + minor, + op: "rios".to_string(), + value: stat.rios, + }); + + stats.push(BlkioStat { + major, + minor, + op: "wios".to_string(), + value: stat.wios, + }); + + stats.push(BlkioStat { + major, + minor, + op: "dbytes".to_string(), + value: stat.dbytes, + }); + + stats.push(BlkioStat { + major, + minor, + op: "dios".to_string(), + value: stat.dios, + }); + } + + stats + } + + fn from_blk_io_data(blkiodata: &[BlkIoData]) -> Vec { + let op = String::new(); + + blkiodata + .iter() + .map(|item| BlkioStat { + major: item.major as u64, + minor: item.minor as u64, + op: op.clone(), + value: item.data, + }) + .collect() + } +} diff --git a/src/manager/mod.rs b/src/manager/mod.rs new file mode 100644 index 0000000..2f5005d --- /dev/null +++ b/src/manager/mod.rs @@ -0,0 +1,77 @@ +// Copyright (c) 2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +mod error; +use std::collections::HashMap; + +pub use error::{Error, Result}; +mod fs; +pub use fs::FsManager; +mod conv; + +use oci_spec::runtime::LinuxResources; + +use crate::{CgroupPid, CgroupStats, FreezerState}; + +/// Manage cgroups designed for OCI containers. +pub trait Manager: Send + Sync { + /// Add a process specified by its tgid. + fn add_proc(&mut self, tgid: CgroupPid) -> Result<()>; + + /// Add a thread specified by its pid. + fn add_thread(&mut self, pid: CgroupPid) -> Result<()>; + + /// Get the list of pids joint to the cgroups. + fn pids(&self) -> Result>; + + /// Set the freezer cgroup to the specified state. + fn freeze(&self, state: FreezerState) -> Result<()>; + + /// Remove the cgroups. + fn destroy(&mut self) -> Result<()>; + + /// Set the resources to the cgroups. + fn set(&mut self, resources: &LinuxResources) -> Result<()>; + + /// Get the cgroup path. + /// + /// # Arguments + /// + /// - `subsystem`: cgroup subsystem, for cgroup v1 the value should not + /// be empty, while for cgroup v2 the only valid value is `None`. + fn cgroup_path(&self, subsystem: Option<&str>) -> Result; + + /// Enable CPUs, topdown from root in cgroup hierarchy, this would be + /// useful for CPU hotplug in the guest. + /// + /// The caller should update cgroup resources manually, in particular + /// cpuset, after this, in order to use the new CPUs (or avoid using + /// offline CPUs). + /// + /// # Arguments + /// + /// - `cpus`: online CPUs in the same format with `cat + /// /sys/devices/system/cpu/online`, e.g. "0-3,6-7". + fn enable_cpus_topdown(&self, cpus: &str) -> Result<()>; + + /// Get cgroup stats. + fn stats(&self) -> CgroupStats; + + /// Get the mappings of subsystems to their relative path. The full + /// path would be something like "{mountpoint}/{relative_path}". The + /// mappings of mountpoints see "mounts()". + fn paths(&self) -> &HashMap; + + /// Get the mappings of subsystems to their mountpoints. The full + /// path would be something like "{mountpoint}/{relative_path}". The + /// mappings of relative paths see "paths()". + fn mounts(&self) -> &HashMap; + + /// Indicate whether the cgroup manager is using systemd. + fn systemd(&self) -> bool; + + /// Indicate whether the cgroup manager is using cgroup v2. + fn v2(&self) -> bool; +} diff --git a/src/stats.rs b/src/stats.rs new file mode 100644 index 0000000..856cc25 --- /dev/null +++ b/src/stats.rs @@ -0,0 +1,156 @@ +// Copyright (c) 2018 Levente Kurusa +// Copyright (c) 2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +use std::collections::HashMap; + +#[derive(Debug, Default)] +pub struct CgroupStats { + pub cpu: CpuCgroupStats, + pub memory: MemoryCgroupStats, + pub pids: PidsCgroupStats, + pub blkio: BlkioCgroupStats, + pub hugetlb: HugeTlbCgroupStats, +} + +#[derive(Debug, Default)] +pub struct CpuCgroupStats { + pub cpu_acct: Option, + pub cpu_throttling: Option, +} + +#[derive(Debug, Default)] +pub struct CpuAcctStats { + /// Usage in userspace, read from `cpuacct.stat` from the line starting + /// with `user`. Set 0 if no data. + pub user_usage: u64, + /// Usage in kernelspace, read from `cpuacct.stat` from the line + /// starting with `system`. Set 0 if no data. + pub system_usage: u64, + /// Total usage, read from `cpuacct.usage`. Set 0 if no data. + pub total_usage: u64, + /// Per-CPU usage, read from `cpuacct.usage_percpu`. + pub usage_percpu: Vec, +} + +#[derive(Debug, Default)] +pub struct CpuThrottlingStats { + /// Periods, read from `cpu.stat` from the line starting with + /// `nr_periods`. Set 0 if no data. + pub periods: u64, + /// Throttled periods, read from `cpu.stat` from the line starting with + /// `nr_throttled`. Set 0 if no data. + pub throttled_periods: u64, + /// Throttled time, read from `cpu.stat` from the line starting with + /// `throttled_time`. Set 0 if no data. + pub throttled_time: u64, +} + +#[derive(Debug, Default)] +pub struct MemoryCgroupStats { + pub memory: Option, + pub memory_swap: Option, + pub kernel_memory: Option, + + /// Use hierarchy, read from `memory.use_hierarchy` in cgroups v1. Only + /// available in cgroups v1. + pub use_hierarchy: bool, + + // The following data is read from `memory.stat`, see also + // `crate::fs::memory::MemoryStat::stat`. + pub cache: u64, + pub rss: u64, + pub rss_huge: u64, + pub shmem: u64, + pub mapped_file: u64, + pub dirty: u64, + pub writeback: u64, + pub swap: u64, + pub pgpgin: u64, + pub pgpgout: u64, + pub pgfault: u64, + pub pgmajfault: u64, + pub inactive_anon: u64, + pub active_anon: u64, + pub inactive_file: u64, + pub active_file: u64, + pub unevictable: u64, + pub hierarchical_memory_limit: i64, + pub hierarchical_memsw_limit: i64, + pub total_cache: u64, + pub total_rss: u64, + pub total_rss_huge: u64, + pub total_shmem: u64, + pub total_mapped_file: u64, + pub total_dirty: u64, + pub total_writeback: u64, + pub total_swap: u64, + pub total_pgpgin: u64, + pub total_pgpgout: u64, + pub total_pgfault: u64, + pub total_pgmajfault: u64, + pub total_inactive_anon: u64, + pub total_active_anon: u64, + pub total_inactive_file: u64, + pub total_active_file: u64, + pub total_unevictable: u64, +} + +#[derive(Debug, Default)] +pub struct MemoryStats { + /// Memory [swap] usage, read from `memory[.memsw].usage_in_bytes` in + /// cgroups v1 and `memory[.swap].current` in cgroups v2. + pub usage: u64, + /// Maximum memory [swap] usage observed by cgroups, read from + /// `memory[.memsw].max_usage_in_bytes` in cgroups v1 and + /// `memory[.swap].peak` in cgroups v2. + pub max_usage: u64, + /// Memory [swap] limit, read from `memory[.memsw].limit_in_bytes` in + /// cgroups v1 and `memory[.swap].max` in cgroups v2. + pub limit: i64, + /// Failure count, read from `memory[.memsw].failcnt`. Only available in + /// cgroups v1. + pub fail_cnt: u64, +} + +#[derive(Debug, Default)] +pub struct PidsCgroupStats { + /// Current number of processes in the cgroup, read from `pids.current`. + pub current: u64, + /// Maximum number of processes in the cgroup, read from `pids.limit`. + pub limit: i64, +} + +#[derive(Debug, Default)] +pub struct BlkioCgroupStats { + pub io_service_bytes_recursive: Vec, + pub io_serviced_recursive: Vec, + pub io_queued_recursive: Vec, + pub io_service_time_recursive: Vec, + pub io_wait_time_recursive: Vec, + pub io_merged_recursive: Vec, + pub io_time_recursive: Vec, + pub sectors_recursive: Vec, +} + +#[derive(Debug, Default)] +pub struct BlkioStat { + pub major: u64, + pub minor: u64, + pub op: String, + pub value: u64, +} + +/// A structure representing the statistics of the `hugetlb` subsystem of a +/// Cgroup. The key is the huge page size, and the value is the statistics +/// for that size. +pub type HugeTlbCgroupStats = HashMap; + +#[derive(Debug, Default)] +pub struct HugeTlbStat { + pub usage: u64, + pub max_usage: u64, + pub fail_cnt: u64, +} From 4a68c762e18b4c3c6693a1c58b69061012459d0c Mon Sep 17 00:00:00 2001 From: Xuewei Niu Date: Thu, 3 Jul 2025 20:02:03 +0800 Subject: [PATCH 3/6] systemd: Add support for managing cgroups through systemd `SystemdCgroup` takes a `parent`, which is the name of a slice, and a `unit`, which is the name of a slice or a scope unit, and provides methods to start, kill the unit, as well as set properties for the unit. The mods, `cpu`, `memory`, `cpuset`, and `pids`, are designed to generate properties quickly. It hides the difference between cgroups v1 and v2, and does simple checks for the systemd version and arguments. Signed-off-by: Xuewei Niu --- Cargo.toml | 2 + src/lib.rs | 1 + src/systemd/consts.rs | 68 ++ src/systemd/cpu.rs | 35 + src/systemd/cpuset.rs | 106 +++ src/systemd/dbus/README.md | 17 + src/systemd/dbus/client.rs | 192 ++++ src/systemd/dbus/error.rs | 18 + src/systemd/dbus/mod.rs | 18 + src/systemd/dbus/proxy.rs | 16 + src/systemd/dbus/systemd_manager_proxy.rs | 1011 +++++++++++++++++++++ src/systemd/error.rs | 18 + src/systemd/memory.rs | 32 + src/systemd/mod.rs | 25 + src/systemd/pids.rs | 11 + src/systemd/props.rs | 170 ++++ src/systemd/utils.rs | 64 ++ 17 files changed, 1804 insertions(+) create mode 100644 src/systemd/consts.rs create mode 100644 src/systemd/cpu.rs create mode 100644 src/systemd/cpuset.rs create mode 100644 src/systemd/dbus/README.md create mode 100644 src/systemd/dbus/client.rs create mode 100644 src/systemd/dbus/error.rs create mode 100644 src/systemd/dbus/mod.rs create mode 100644 src/systemd/dbus/proxy.rs create mode 100644 src/systemd/dbus/systemd_manager_proxy.rs create mode 100644 src/systemd/error.rs create mode 100644 src/systemd/memory.rs create mode 100644 src/systemd/mod.rs create mode 100644 src/systemd/pids.rs create mode 100644 src/systemd/props.rs create mode 100644 src/systemd/utils.rs diff --git a/Cargo.toml b/Cargo.toml index 4448783..f25b3bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ libc = "0.2" serde = { version = "1.0", features = ["derive"], optional = true } thiserror = "1" oci-spec = { version = "0.8.1", optional = true } +zbus = "5.8" +bit-vec = "0.6" [dev-dependencies] libc = "0.2.76" diff --git a/src/lib.rs b/src/lib.rs index 4bb4a3b..1f163a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ pub mod manager; pub use manager::{FsManager, Manager}; pub mod stats; pub use stats::CgroupStats; +pub mod systemd; /// The maximum value for CPU shares in cgroups v1 pub const CPU_SHARES_V1_MAX: u64 = 262144; diff --git a/src/systemd/consts.rs b/src/systemd/consts.rs new file mode 100644 index 0000000..9ce477a --- /dev/null +++ b/src/systemd/consts.rs @@ -0,0 +1,68 @@ +// Copyright (c) 2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +/// Who enum: all +pub const WHO_ENUM_ALL: &str = "all"; + +/// Unit mode: replace +pub const UNIT_MODE_REPLACE: &str = "replace"; + +/// No such unit error +pub const NO_SUCH_UNIT: &str = "org.freedesktop.systemd1.NoSuchUnit"; + +/// Default description for transient units. +pub const DEFAULT_DESCRIPTION: &str = "cgroups-rs transient unit"; + +/// Turn on CPU usage accounting for this unit. +pub const CPU_ACCOUNTING: &str = "CPUAccounting"; +/// This setting controls the memory controller in the unified hierarchy. +/// Added in version 208. +pub const MEMORY_ACCOUNTING: &str = "MemoryAccounting"; +/// This setting controls the pids controller in the unified hierarchy. +pub const TASKS_ACCOUNTING: &str = "TasksAccounting"; +/// This setting controls the io controller in the unified hierarchy. +/// Added in version 230. +pub const IO_ACCOUNTING: &str = "IOAccounting"; +/// This setting controls the block IO controller in the legacy hierarchy. +/// Deprecated in version 252. +pub const BLOCK_IO_ACCOUNTING: &str = "BlockIOAccounting"; +/// Description of the unit. +pub const DESCRIPTION: &str = "Description"; +/// PIDs +pub const PIDS: &str = "PIDs"; +/// Default dependencies for this unit. +pub const DEFAULT_DEPENDENCIES: &str = "DefaultDependencies"; +/// Wants, expressing a weak dependency on other units. +pub const WANTS: &str = "Wants"; +/// Slice, used to assign a unit to a specific slice. +pub const SLICE: &str = "Slice"; +/// Turns on delegation of further resource control partitioning to +/// processes of the unit. +pub const DELEGATE: &str = "Delegate"; +/// Timeout for stopping the unit in microseconds. +pub const TIMEOUT_STOP_USEC: &str = "TimeoutStopUSec"; + +/// CPU shares in the legacy hierarchy. +pub const CPU_SHARES: &str = "CPUShares"; +/// CPU shares in the unified hierarchy. +pub const CPU_WEIGHT: &str = "CPUWeight"; +/// CPU quota period us. +pub const CPU_QUOTA_PERIOD_US: &str = "CPUQuotaPeriodUSec"; +/// CPU quota us +pub const CPU_QUOTA_PER_SEC_US: &str = "CPUQuotaPerSecUSec"; +/// Allowed CPUs +pub const ALLOWED_CPUS: &str = "AllowedCPUs"; +/// Allowed memory nodes +pub const ALLOWED_MEMORY_NODES: &str = "AllowedMemoryNodes"; +/// Memory limit in the legacy hierarchy. +pub const MEMORY_LIMIT: &str = "MemoryLimit"; +/// Memory limit in the unified hierarchy. +pub const MEMORY_MAX: &str = "MemoryMax"; +/// Memory low +pub const MEMORY_LOW: &str = "MemoryLow"; +/// Memory swap max +pub const MEMORY_SWAP_MAX: &str = "MemorySwapMax"; +/// Tasks max +pub const TASKS_MAX: &str = "TasksMax"; diff --git a/src/systemd/cpu.rs b/src/systemd/cpu.rs new file mode 100644 index 0000000..2256866 --- /dev/null +++ b/src/systemd/cpu.rs @@ -0,0 +1,35 @@ +// Copyright (c) 2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +use crate::systemd::error::{Error, Result}; +use crate::systemd::{ + CPU_QUOTA_PERIOD_US, CPU_QUOTA_PER_SEC_US, CPU_SHARES, CPU_SYSTEMD_VERSION, CPU_WEIGHT, +}; + +/// Returns the property for CPU shares. +/// +/// Please note that if the shares is obtained from OCI runtime spec, it +/// MUST be converted, see [1] and `convert_shares_to_v2()`. +/// +/// 1: https://github.com/containers/crun/blob/main/crun.1.md#cgroup-v2 +pub fn shares(shares: u64, v2: bool) -> Result<(&'static str, u64)> { + let id = if v2 { CPU_WEIGHT } else { CPU_SHARES }; + + Ok((id, shares)) +} + +/// Returns the property for CPU period. +pub fn period(period: u64, systemd_version: usize) -> Result<(&'static str, u64)> { + if systemd_version < CPU_SYSTEMD_VERSION { + return Err(Error::ObsoleteSystemd); + } + + Ok((CPU_QUOTA_PERIOD_US, period)) +} + +/// Return the property for CPU quota. +pub fn quota(quota: u64) -> Result<(&'static str, u64)> { + Ok((CPU_QUOTA_PER_SEC_US, quota)) +} diff --git a/src/systemd/cpuset.rs b/src/systemd/cpuset.rs new file mode 100644 index 0000000..b11cbcc --- /dev/null +++ b/src/systemd/cpuset.rs @@ -0,0 +1,106 @@ +// Copyright (c) 2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +use bit_vec::BitVec; + +use crate::systemd::error::{Error, Result}; +use crate::systemd::{ALLOWED_CPUS, ALLOWED_MEMORY_NODES, CPUSET_SYSTEMD_VERSION}; + +const BYTE_IN_BITS: usize = 8; + +/// Returns the property for cpuset CPUs. +pub fn cpus(cpus: &str, systemd_version: usize) -> Result<(&'static str, Vec)> { + if systemd_version < CPUSET_SYSTEMD_VERSION { + return Err(Error::ObsoleteSystemd); + } + + let mask = convert_list_to_mask(cpus)?; + + Ok((ALLOWED_CPUS, mask)) +} + +/// Returns the property for cpuset memory nodes. +pub fn mems(mems: &str, systemd_version: usize) -> Result<(&'static str, Vec)> { + if systemd_version < CPUSET_SYSTEMD_VERSION { + return Err(Error::ObsoleteSystemd); + } + + let mask = convert_list_to_mask(mems)?; + + Ok((ALLOWED_MEMORY_NODES, mask)) +} + +/// Convert cpuset cpus/mems from the string in comma-separated list format +/// to bitmask restored in `Vec`, see [1]. +/// +/// 1: https://man7.org/linux/man-pages/man7/cpuset.7.html +/// +/// # Arguments +/// +/// * `list` - A string slice that holds the list of CPUs in the format +/// "0-3,5,7". +fn convert_list_to_mask(list: &str) -> Result> { + let mut bit_vec = BitVec::from_elem(8, false); + + let local_idx = + |index: usize| -> usize { index / BYTE_IN_BITS * BYTE_IN_BITS + 7 - index % BYTE_IN_BITS }; + + for part1 in list.split(',') { + let range: Vec<&str> = part1.split('-').collect(); + match range.len() { + // x- + 1 => { + let left: usize = range[0].parse().map_err(|_| Error::InvalidArgument)?; + + while left >= bit_vec.len() { + bit_vec.grow(BYTE_IN_BITS, false); + } + bit_vec.set(local_idx(left), true); + } + // x-y + 2 => { + let left: usize = range[0].parse().map_err(|_| Error::InvalidArgument)?; + let right: usize = range[1].parse().map_err(|_| Error::InvalidArgument)?; + + while right >= bit_vec.len() { + bit_vec.grow(BYTE_IN_BITS, false); + } + + for index in left..=right { + bit_vec.set(local_idx(index), true); + } + } + _ => { + return Err(Error::InvalidArgument); + } + } + } + + let mut mask = bit_vec.to_bytes(); + mask.reverse(); + + Ok(mask) +} + +#[cfg(test)] +mod tests { + use crate::systemd::cpuset::convert_list_to_mask; + + #[test] + fn test_convert_list_to_mask() { + let mask = convert_list_to_mask("2-4").unwrap(); + assert_eq!(vec![0b00011100_u8], mask); + + let mask = convert_list_to_mask("1,7").unwrap(); + assert_eq!(vec![0b10000010_u8], mask); + + let mask = convert_list_to_mask("0-4,9").unwrap(); + assert_eq!(vec![0b00000010_u8, 0b00011111_u8], mask); + + assert!(convert_list_to_mask("1-3-4").is_err()); + + assert!(convert_list_to_mask("1-3,,").is_err()); + } +} diff --git a/src/systemd/dbus/README.md b/src/systemd/dbus/README.md new file mode 100644 index 0000000..8461461 --- /dev/null +++ b/src/systemd/dbus/README.md @@ -0,0 +1,17 @@ +# Systemd Dbus + +How to generate `xxx_proxy.rs` files + +```shell +# install zbus-xmlgen if not +$ cargo install zbus-xmlgen +# generate interface in XML format +$ busctl introspect --xml-interface \ + org.freedesktop.systemd1 \ + /org/freedesktop/systemd1 \ + org.freedesktop.systemd1.Manager > /tmp/systemd1-manager.xml +# generate Rust code from XML +$ zbus-xmlgen file /tmp/systemd1-manager.xml \ + --output src/systemd/dbus/systemd_manager_proxy.rs +$ rm -rf /tmp/systemd1-manager.xml +``` diff --git a/src/systemd/dbus/client.rs b/src/systemd/dbus/client.rs new file mode 100644 index 0000000..27305f0 --- /dev/null +++ b/src/systemd/dbus/client.rs @@ -0,0 +1,192 @@ +// Copyright 2021-2023 Kata Contributors +// Copyright (c) 2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +use zbus::zvariant::Value; +use zbus::{Error as ZbusError, Result as ZbusResult}; + +use crate::systemd::dbus::error::{Error, Result}; +use crate::systemd::dbus::proxy::systemd_manager_proxy; +use crate::systemd::{Property, NO_SUCH_UNIT, PIDS, UNIT_MODE_REPLACE}; +use crate::CgroupPid; + +pub struct SystemdClient<'a> { + /// The name of the systemd unit (slice or scope) + unit: String, + props: Vec>, +} + +impl<'a> SystemdClient<'a> { + pub fn new(unit: &str, props: Vec>) -> Result { + Ok(Self { + unit: unit.to_string(), + props, + }) + } +} + +impl SystemdClient<'_> { + /// Set the pid to the PIDs property of the unit. + /// + /// Append a process ID to the PIDs property of the unit. If not + /// exists, one property will be created. + pub fn set_pid_prop(&mut self, pid: CgroupPid) -> Result<()> { + if self.exists() { + return Ok(()); + } + + for prop in self.props.iter_mut() { + if prop.0 == PIDS { + // If PIDS is already set, we append the new pid to the existing list. + if let Value::Array(arr) = &mut prop.1 { + arr.append(pid.pid.into()) + .map_err(|_| Error::InvalidProperties)?; + return Ok(()); + } + // Invalid type of PIDs + return Err(Error::InvalidProperties); + } + } + // If PIDS is not set, we create a new property. + self.props + .push((PIDS, Value::Array(vec![pid.pid as u32].into()))); + Ok(()) + } + + /// Start a slice or a scope unit controlled and supervised by systemd. + /// + /// For more information, see: + /// https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html + /// https://www.freedesktop.org/software/systemd/man/latest/systemd.slice.html + /// https://www.freedesktop.org/software/systemd/man/latest/systemd.scope.html + pub fn start(&self) -> Result<()> { + // PIDs property must be present + if !self.props.iter().any(|(k, _)| k == &PIDS) { + return Err(Error::InvalidProperties); + } + + let sys_proxy = systemd_manager_proxy()?; + + let props_borrowed: Vec<(&str, &zbus::zvariant::Value)> = + self.props.iter().map(|(k, v)| (*k, v)).collect(); + let props_borrowed: Vec<&(&str, &Value)> = props_borrowed.iter().collect(); + + sys_proxy.start_transient_unit(&self.unit, UNIT_MODE_REPLACE, &props_borrowed, &[])?; + + Ok(()) + } + + /// Stop the current transient unit, the processes will be killed on + /// unit stop, see [1]. + /// + /// 1. https://www.freedesktop.org/software/systemd/man/latest/systemd.kill.html#KillMode= + pub fn stop(&self) -> Result<()> { + let sys_proxy = systemd_manager_proxy()?; + + let ret = sys_proxy.stop_unit(&self.unit, UNIT_MODE_REPLACE); + ignore_no_such_unit(ret)?; + + // If we stop the unit and it still exists, it may be in a failed + // state, so we will try to reset it. + if self.exists() { + let ret = sys_proxy.reset_failed_unit(&self.unit); + ignore_no_such_unit(ret)?; + } + + Ok(()) + } + + /// Set properties for the unit through dbus `SetUnitProperties`. + pub fn set_properties(&mut self, properties: &[Property<'static>]) -> Result<()> { + for prop in properties { + let new = prop.1.try_clone().map_err(|_| Error::InvalidProperties)?; + // Try to update the value first, if fails, append it. + if let Some(existing) = self.props.iter_mut().find(|p| p.0 == prop.0) { + existing.1 = new; + } else { + self.props.push((prop.0, new)); + } + } + + // The unit must exist before setting properties. + if !self.exists() { + return Ok(()); + } + + let sys_proxy = systemd_manager_proxy()?; + + let props_borrowed: Vec<(&str, &Value)> = properties.iter().map(|(k, v)| (*k, v)).collect(); + let props_borrowed: Vec<&(&str, &Value)> = props_borrowed.iter().collect(); + + sys_proxy.set_unit_properties(&self.unit, true, &props_borrowed)?; + + Ok(()) + } + + /// Freeze the unit through dbus `FreezeUnit`. + pub fn freeze(&self) -> Result<()> { + let sys_proxy = systemd_manager_proxy()?; + + sys_proxy.freeze_unit(&self.unit)?; + + Ok(()) + } + + /// Thaw the frozen unit through dbus `ThawUnit`. + pub fn thaw(&self) -> Result<()> { + let sys_proxy = systemd_manager_proxy()?; + + sys_proxy.thaw_unit(&self.unit)?; + + Ok(()) + } + + /// Get the systemd version. + pub fn systemd_version(&self) -> Result { + let sys_proxy = systemd_manager_proxy()?; + + // Parse 249 from "249.11-0ubuntu3.16" + let version = sys_proxy.version()?; + let version = version + .split('.') + .next() + .and_then(|v| v.parse::().ok()) + .ok_or(Error::CorruptedSystemdVersion(version))?; + + Ok(version) + } + + /// Check if the unit exists. + pub fn exists(&self) -> bool { + let sys_proxy = match systemd_manager_proxy() { + Ok(proxy) => proxy, + _ => return false, + }; + + sys_proxy + .get_unit(&self.unit) + .map(|_| true) + .unwrap_or_default() + } + + /// Add a process (tgid) to the unit through dbus + /// `AttachProcessesToUnit`. + pub fn add_process(&self, pid: CgroupPid, subcgroup: &str) -> Result<()> { + let sys_proxy = systemd_manager_proxy()?; + + sys_proxy.attach_processes_to_unit(&self.unit, subcgroup, &[pid.pid as u32])?; + + Ok(()) + } +} + +fn ignore_no_such_unit(result: ZbusResult) -> ZbusResult { + if let Err(ZbusError::MethodError(err_name, _, _)) = &result { + if err_name.as_str() == NO_SUCH_UNIT { + return Ok(true); + } + } + result.map(|_| false) +} diff --git a/src/systemd/dbus/error.rs b/src/systemd/dbus/error.rs new file mode 100644 index 0000000..d8cae94 --- /dev/null +++ b/src/systemd/dbus/error.rs @@ -0,0 +1,18 @@ +// Copyright (c) 2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 +// + +pub type Result = std::result::Result; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("invalid properties")] + InvalidProperties, + + #[error("dbus error: {0}")] + Dbus(#[from] zbus::Error), + + #[error("corrupted systemd version: {0}")] + CorruptedSystemdVersion(String), +} diff --git a/src/systemd/dbus/mod.rs b/src/systemd/dbus/mod.rs new file mode 100644 index 0000000..8d39ef8 --- /dev/null +++ b/src/systemd/dbus/mod.rs @@ -0,0 +1,18 @@ +// Copyright (c) 2018 Levente Kurusa +// Copyright (c) 2020-2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +//! Systemd D-Bus interface for managing cgroups and units. +//! +//! References: +//! https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.systemd1.html +//! https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html +//! https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html + +mod client; +pub mod error; +mod systemd_manager_proxy; +pub use client::SystemdClient; +mod proxy; diff --git a/src/systemd/dbus/proxy.rs b/src/systemd/dbus/proxy.rs new file mode 100644 index 0000000..9ed2289 --- /dev/null +++ b/src/systemd/dbus/proxy.rs @@ -0,0 +1,16 @@ +// Copyright (c) 2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +use zbus::blocking::Connection; +use zbus::Result; + +use crate::systemd::dbus::systemd_manager_proxy::ManagerProxyBlocking as SystemManager; + +pub(crate) fn systemd_manager_proxy<'a>() -> Result> { + let connection = Connection::system()?; + let proxy = SystemManager::new(&connection)?; + + Ok(proxy) +} diff --git a/src/systemd/dbus/systemd_manager_proxy.rs b/src/systemd/dbus/systemd_manager_proxy.rs new file mode 100644 index 0000000..0f5dc2d --- /dev/null +++ b/src/systemd/dbus/systemd_manager_proxy.rs @@ -0,0 +1,1011 @@ +// Copyright 2021-2023 Kata Contributors +// Copyright (c) 2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +//! # D-Bus interface proxy for: `org.freedesktop.systemd1.Manager` +//! +//! This code was generated by `zbus-xmlgen` `5.1.0` from D-Bus introspection data. +//! Source: `systemd1-manager.xml`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::PeerProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, + +#![allow(clippy::type_complexity)] + +use zbus::proxy; + +#[proxy( + interface = "org.freedesktop.systemd1.Manager", + default_service = "org.freedesktop.systemd1", + default_path = "/org/freedesktop/systemd1" +)] +pub trait Manager { + /// AbandonScope method + fn abandon_scope(&self, name: &str) -> zbus::Result<()>; + + /// AddDependencyUnitFiles method + fn add_dependency_unit_files( + &self, + files: &[&str], + target: &str, + type_: &str, + runtime: bool, + force: bool, + ) -> zbus::Result>; + + /// AttachProcessesToUnit method + fn attach_processes_to_unit( + &self, + unit_name: &str, + subcgroup: &str, + pids: &[u32], + ) -> zbus::Result<()>; + + /// BindMountUnit method + fn bind_mount_unit( + &self, + name: &str, + source: &str, + destination: &str, + read_only: bool, + mkdir: bool, + ) -> zbus::Result<()>; + + /// CancelJob method + fn cancel_job(&self, id: u32) -> zbus::Result<()>; + + /// CleanUnit method + fn clean_unit(&self, name: &str, mask: &[&str]) -> zbus::Result<()>; + + /// ClearJobs method + fn clear_jobs(&self) -> zbus::Result<()>; + + /// DisableUnitFiles method + fn disable_unit_files( + &self, + files: &[&str], + runtime: bool, + ) -> zbus::Result>; + + /// DisableUnitFilesWithFlags method + fn disable_unit_files_with_flags( + &self, + files: &[&str], + flags: u64, + ) -> zbus::Result>; + + /// Dump method + fn dump(&self) -> zbus::Result; + + /// DumpByFileDescriptor method + fn dump_by_file_descriptor(&self) -> zbus::Result; + + /// EnableUnitFiles method + fn enable_unit_files( + &self, + files: &[&str], + runtime: bool, + force: bool, + ) -> zbus::Result<(bool, Vec<(String, String, String)>)>; + + /// EnableUnitFilesWithFlags method + fn enable_unit_files_with_flags( + &self, + files: &[&str], + flags: u64, + ) -> zbus::Result<(bool, Vec<(String, String, String)>)>; + + /// EnqueueMarkedJobs method + fn enqueue_marked_jobs(&self) -> zbus::Result>; + + /// EnqueueUnitJob method + #[allow(clippy::too_many_arguments)] + fn enqueue_unit_job( + &self, + name: &str, + job_type: &str, + job_mode: &str, + ) -> zbus::Result<( + u32, + zbus::zvariant::OwnedObjectPath, + String, + zbus::zvariant::OwnedObjectPath, + String, + Vec<( + u32, + zbus::zvariant::OwnedObjectPath, + String, + zbus::zvariant::OwnedObjectPath, + String, + )>, + )>; + + /// Exit method + fn exit(&self) -> zbus::Result<()>; + + /// FreezeUnit method + fn freeze_unit(&self, name: &str) -> zbus::Result<()>; + + /// GetDefaultTarget method + fn get_default_target(&self) -> zbus::Result; + + /// GetDynamicUsers method + fn get_dynamic_users(&self) -> zbus::Result>; + + /// GetJob method + fn get_job(&self, id: u32) -> zbus::Result; + + /// GetJobAfter method + fn get_job_after( + &self, + id: u32, + ) -> zbus::Result< + Vec<( + u32, + String, + String, + String, + zbus::zvariant::OwnedObjectPath, + zbus::zvariant::OwnedObjectPath, + )>, + >; + + /// GetJobBefore method + fn get_job_before( + &self, + id: u32, + ) -> zbus::Result< + Vec<( + u32, + String, + String, + String, + zbus::zvariant::OwnedObjectPath, + zbus::zvariant::OwnedObjectPath, + )>, + >; + + /// GetUnit method + fn get_unit(&self, name: &str) -> zbus::Result; + + /// GetUnitByControlGroup method + fn get_unit_by_control_group( + &self, + cgroup: &str, + ) -> zbus::Result; + + /// GetUnitByInvocationID method + #[zbus(name = "GetUnitByInvocationID")] + fn get_unit_by_invocation_id( + &self, + invocation_id: &[u8], + ) -> zbus::Result; + + /// GetUnitByPID method + #[zbus(name = "GetUnitByPID")] + fn get_unit_by_pid(&self, pid: u32) -> zbus::Result; + + /// GetUnitFileLinks method + fn get_unit_file_links(&self, name: &str, runtime: bool) -> zbus::Result>; + + /// GetUnitFileState method + fn get_unit_file_state(&self, file: &str) -> zbus::Result; + + /// GetUnitProcesses method + fn get_unit_processes(&self, name: &str) -> zbus::Result>; + + /// Halt method + fn halt(&self) -> zbus::Result<()>; + + /// KExec method + #[zbus(name = "KExec")] + fn kexec(&self) -> zbus::Result<()>; + + /// KillUnit method + fn kill_unit(&self, name: &str, whom: &str, signal: i32) -> zbus::Result<()>; + + /// LinkUnitFiles method + fn link_unit_files( + &self, + files: &[&str], + runtime: bool, + force: bool, + ) -> zbus::Result>; + + /// ListJobs method + fn list_jobs( + &self, + ) -> zbus::Result< + Vec<( + u32, + String, + String, + String, + zbus::zvariant::OwnedObjectPath, + zbus::zvariant::OwnedObjectPath, + )>, + >; + + /// ListUnitFiles method + fn list_unit_files(&self) -> zbus::Result>; + + /// ListUnitFilesByPatterns method + fn list_unit_files_by_patterns( + &self, + states: &[&str], + patterns: &[&str], + ) -> zbus::Result>; + + /// ListUnits method + fn list_units( + &self, + ) -> zbus::Result< + Vec<( + String, + String, + String, + String, + String, + String, + zbus::zvariant::OwnedObjectPath, + u32, + String, + zbus::zvariant::OwnedObjectPath, + )>, + >; + + /// ListUnitsByNames method + fn list_units_by_names( + &self, + names: &[&str], + ) -> zbus::Result< + Vec<( + String, + String, + String, + String, + String, + String, + zbus::zvariant::OwnedObjectPath, + u32, + String, + zbus::zvariant::OwnedObjectPath, + )>, + >; + + /// ListUnitsByPatterns method + fn list_units_by_patterns( + &self, + states: &[&str], + patterns: &[&str], + ) -> zbus::Result< + Vec<( + String, + String, + String, + String, + String, + String, + zbus::zvariant::OwnedObjectPath, + u32, + String, + zbus::zvariant::OwnedObjectPath, + )>, + >; + + /// ListUnitsFiltered method + fn list_units_filtered( + &self, + states: &[&str], + ) -> zbus::Result< + Vec<( + String, + String, + String, + String, + String, + String, + zbus::zvariant::OwnedObjectPath, + u32, + String, + zbus::zvariant::OwnedObjectPath, + )>, + >; + + /// LoadUnit method + fn load_unit(&self, name: &str) -> zbus::Result; + + /// LookupDynamicUserByName method + fn lookup_dynamic_user_by_name(&self, name: &str) -> zbus::Result; + + /// LookupDynamicUserByUID method + #[zbus(name = "LookupDynamicUserByUID")] + fn lookup_dynamic_user_by_uid(&self, uid: u32) -> zbus::Result; + + /// MaskUnitFiles method + fn mask_unit_files( + &self, + files: &[&str], + runtime: bool, + force: bool, + ) -> zbus::Result>; + + /// MountImageUnit method + fn mount_image_unit( + &self, + name: &str, + source: &str, + destination: &str, + read_only: bool, + mkdir: bool, + options: &[&(&str, &str)], + ) -> zbus::Result<()>; + + /// PowerOff method + fn power_off(&self) -> zbus::Result<()>; + + /// PresetAllUnitFiles method + fn preset_all_unit_files( + &self, + mode: &str, + runtime: bool, + force: bool, + ) -> zbus::Result>; + + /// PresetUnitFiles method + fn preset_unit_files( + &self, + files: &[&str], + runtime: bool, + force: bool, + ) -> zbus::Result<(bool, Vec<(String, String, String)>)>; + + /// PresetUnitFilesWithMode method + fn preset_unit_files_with_mode( + &self, + files: &[&str], + mode: &str, + runtime: bool, + force: bool, + ) -> zbus::Result<(bool, Vec<(String, String, String)>)>; + + /// Reboot method + fn reboot(&self) -> zbus::Result<()>; + + /// ReenableUnitFiles method + fn reenable_unit_files( + &self, + files: &[&str], + runtime: bool, + force: bool, + ) -> zbus::Result<(bool, Vec<(String, String, String)>)>; + + /// Reexecute method + fn reexecute(&self) -> zbus::Result<()>; + + /// RefUnit method + fn ref_unit(&self, name: &str) -> zbus::Result<()>; + + /// Reload method + fn reload(&self) -> zbus::Result<()>; + + /// ReloadOrRestartUnit method + fn reload_or_restart_unit( + &self, + name: &str, + mode: &str, + ) -> zbus::Result; + + /// ReloadOrTryRestartUnit method + fn reload_or_try_restart_unit( + &self, + name: &str, + mode: &str, + ) -> zbus::Result; + + /// ReloadUnit method + fn reload_unit(&self, name: &str, mode: &str) -> zbus::Result; + + /// ResetFailed method + fn reset_failed(&self) -> zbus::Result<()>; + + /// ResetFailedUnit method + fn reset_failed_unit(&self, name: &str) -> zbus::Result<()>; + + /// RestartUnit method + fn restart_unit(&self, name: &str, mode: &str) + -> zbus::Result; + + /// RevertUnitFiles method + fn revert_unit_files(&self, files: &[&str]) -> zbus::Result>; + + /// SetDefaultTarget method + fn set_default_target( + &self, + name: &str, + force: bool, + ) -> zbus::Result>; + + /// SetEnvironment method + fn set_environment(&self, assignments: &[&str]) -> zbus::Result<()>; + + /// SetExitCode method + fn set_exit_code(&self, number: u8) -> zbus::Result<()>; + + /// SetShowStatus method + fn set_show_status(&self, mode: &str) -> zbus::Result<()>; + + /// SetUnitProperties method + fn set_unit_properties( + &self, + name: &str, + runtime: bool, + properties: &[&(&str, &zbus::zvariant::Value<'_>)], + ) -> zbus::Result<()>; + + /// StartTransientUnit method + #[allow(clippy::type_complexity)] + fn start_transient_unit( + &self, + name: &str, + mode: &str, + properties: &[&(&str, &zbus::zvariant::Value<'_>)], + aux: &[&(&str, &[&(&str, &zbus::zvariant::Value<'_>)])], + ) -> zbus::Result; + + /// StartUnit method + fn start_unit(&self, name: &str, mode: &str) -> zbus::Result; + + /// StartUnitReplace method + fn start_unit_replace( + &self, + old_unit: &str, + new_unit: &str, + mode: &str, + ) -> zbus::Result; + + /// StopUnit method + fn stop_unit(&self, name: &str, mode: &str) -> zbus::Result; + + /// Subscribe method + fn subscribe(&self) -> zbus::Result<()>; + + /// SwitchRoot method + fn switch_root(&self, new_root: &str, init: &str) -> zbus::Result<()>; + + /// ThawUnit method + fn thaw_unit(&self, name: &str) -> zbus::Result<()>; + + /// TryRestartUnit method + fn try_restart_unit( + &self, + name: &str, + mode: &str, + ) -> zbus::Result; + + /// UnmaskUnitFiles method + fn unmask_unit_files( + &self, + files: &[&str], + runtime: bool, + ) -> zbus::Result>; + + /// UnrefUnit method + fn unref_unit(&self, name: &str) -> zbus::Result<()>; + + /// UnsetAndSetEnvironment method + fn unset_and_set_environment(&self, names: &[&str], assignments: &[&str]) -> zbus::Result<()>; + + /// UnsetEnvironment method + fn unset_environment(&self, names: &[&str]) -> zbus::Result<()>; + + /// Unsubscribe method + fn unsubscribe(&self) -> zbus::Result<()>; + + /// JobNew signal + #[zbus(signal)] + fn job_new(&self, id: u32, job: zbus::zvariant::ObjectPath<'_>, unit: &str) + -> zbus::Result<()>; + + /// JobRemoved signal + #[zbus(signal)] + fn job_removed( + &self, + id: u32, + job: zbus::zvariant::ObjectPath<'_>, + unit: &str, + result: &str, + ) -> zbus::Result<()>; + + /// Reloading signal + #[zbus(signal)] + fn reloading(&self, active: bool) -> zbus::Result<()>; + + /// StartupFinished signal + #[zbus(signal)] + fn startup_finished( + &self, + firmware: u64, + loader: u64, + kernel: u64, + initrd: u64, + userspace: u64, + total: u64, + ) -> zbus::Result<()>; + + /// UnitFilesChanged signal + #[zbus(signal)] + fn unit_files_changed(&self) -> zbus::Result<()>; + + /// UnitNew signal + #[zbus(signal)] + fn unit_new(&self, id: &str, unit: zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>; + + /// UnitRemoved signal + #[zbus(signal)] + fn unit_removed(&self, id: &str, unit: zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>; + + /// Architecture property + #[zbus(property)] + fn architecture(&self) -> zbus::Result; + + /// ConfirmSpawn property + #[zbus(property)] + fn confirm_spawn(&self) -> zbus::Result; + + /// ControlGroup property + #[zbus(property)] + fn control_group(&self) -> zbus::Result; + + /// CtrlAltDelBurstAction property + #[zbus(property)] + fn ctrl_alt_del_burst_action(&self) -> zbus::Result; + + /// DefaultBlockIOAccounting property + #[zbus(property, name = "DefaultBlockIOAccounting")] + fn default_block_ioaccounting(&self) -> zbus::Result; + + /// DefaultCPUAccounting property + #[zbus(property, name = "DefaultCPUAccounting")] + fn default_cpuaccounting(&self) -> zbus::Result; + + /// DefaultLimitAS property + #[zbus(property, name = "DefaultLimitAS")] + fn default_limit_as(&self) -> zbus::Result; + + /// DefaultLimitASSoft property + #[zbus(property, name = "DefaultLimitASSoft")] + fn default_limit_assoft(&self) -> zbus::Result; + + /// DefaultLimitCORE property + #[zbus(property, name = "DefaultLimitCORE")] + fn default_limit_core(&self) -> zbus::Result; + + /// DefaultLimitCORESoft property + #[zbus(property, name = "DefaultLimitCORESoft")] + fn default_limit_coresoft(&self) -> zbus::Result; + + /// DefaultLimitCPU property + #[zbus(property, name = "DefaultLimitCPU")] + fn default_limit_cpu(&self) -> zbus::Result; + + /// DefaultLimitCPUSoft property + #[zbus(property, name = "DefaultLimitCPUSoft")] + fn default_limit_cpusoft(&self) -> zbus::Result; + + /// DefaultLimitDATA property + #[zbus(property, name = "DefaultLimitDATA")] + fn default_limit_data(&self) -> zbus::Result; + + /// DefaultLimitDATASoft property + #[zbus(property, name = "DefaultLimitDATASoft")] + fn default_limit_datasoft(&self) -> zbus::Result; + + /// DefaultLimitFSIZE property + #[zbus(property, name = "DefaultLimitFSIZE")] + fn default_limit_fsize(&self) -> zbus::Result; + + /// DefaultLimitFSIZESoft property + #[zbus(property, name = "DefaultLimitFSIZESoft")] + fn default_limit_fsizesoft(&self) -> zbus::Result; + + /// DefaultLimitLOCKS property + #[zbus(property, name = "DefaultLimitLOCKS")] + fn default_limit_locks(&self) -> zbus::Result; + + /// DefaultLimitLOCKSSoft property + #[zbus(property, name = "DefaultLimitLOCKSSoft")] + fn default_limit_lockssoft(&self) -> zbus::Result; + + /// DefaultLimitMEMLOCK property + #[zbus(property, name = "DefaultLimitMEMLOCK")] + fn default_limit_memlock(&self) -> zbus::Result; + + /// DefaultLimitMEMLOCKSoft property + #[zbus(property, name = "DefaultLimitMEMLOCKSoft")] + fn default_limit_memlocksoft(&self) -> zbus::Result; + + /// DefaultLimitMSGQUEUE property + #[zbus(property, name = "DefaultLimitMSGQUEUE")] + fn default_limit_msgqueue(&self) -> zbus::Result; + + /// DefaultLimitMSGQUEUESoft property + #[zbus(property, name = "DefaultLimitMSGQUEUESoft")] + fn default_limit_msgqueuesoft(&self) -> zbus::Result; + + /// DefaultLimitNICE property + #[zbus(property, name = "DefaultLimitNICE")] + fn default_limit_nice(&self) -> zbus::Result; + + /// DefaultLimitNICESoft property + #[zbus(property, name = "DefaultLimitNICESoft")] + fn default_limit_nicesoft(&self) -> zbus::Result; + + /// DefaultLimitNOFILE property + #[zbus(property, name = "DefaultLimitNOFILE")] + fn default_limit_nofile(&self) -> zbus::Result; + + /// DefaultLimitNOFILESoft property + #[zbus(property, name = "DefaultLimitNOFILESoft")] + fn default_limit_nofilesoft(&self) -> zbus::Result; + + /// DefaultLimitNPROC property + #[zbus(property, name = "DefaultLimitNPROC")] + fn default_limit_nproc(&self) -> zbus::Result; + + /// DefaultLimitNPROCSoft property + #[zbus(property, name = "DefaultLimitNPROCSoft")] + fn default_limit_nprocsoft(&self) -> zbus::Result; + + /// DefaultLimitRSS property + #[zbus(property, name = "DefaultLimitRSS")] + fn default_limit_rss(&self) -> zbus::Result; + + /// DefaultLimitRSSSoft property + #[zbus(property, name = "DefaultLimitRSSSoft")] + fn default_limit_rsssoft(&self) -> zbus::Result; + + /// DefaultLimitRTPRIO property + #[zbus(property, name = "DefaultLimitRTPRIO")] + fn default_limit_rtprio(&self) -> zbus::Result; + + /// DefaultLimitRTPRIOSoft property + #[zbus(property, name = "DefaultLimitRTPRIOSoft")] + fn default_limit_rtpriosoft(&self) -> zbus::Result; + + /// DefaultLimitRTTIME property + #[zbus(property, name = "DefaultLimitRTTIME")] + fn default_limit_rttime(&self) -> zbus::Result; + + /// DefaultLimitRTTIMESoft property + #[zbus(property, name = "DefaultLimitRTTIMESoft")] + fn default_limit_rttimesoft(&self) -> zbus::Result; + + /// DefaultLimitSIGPENDING property + #[zbus(property, name = "DefaultLimitSIGPENDING")] + fn default_limit_sigpending(&self) -> zbus::Result; + + /// DefaultLimitSIGPENDINGSoft property + #[zbus(property, name = "DefaultLimitSIGPENDINGSoft")] + fn default_limit_sigpendingsoft(&self) -> zbus::Result; + + /// DefaultLimitSTACK property + #[zbus(property, name = "DefaultLimitSTACK")] + fn default_limit_stack(&self) -> zbus::Result; + + /// DefaultLimitSTACKSoft property + #[zbus(property, name = "DefaultLimitSTACKSoft")] + fn default_limit_stacksoft(&self) -> zbus::Result; + + /// DefaultMemoryAccounting property + #[zbus(property)] + fn default_memory_accounting(&self) -> zbus::Result; + + /// DefaultOOMPolicy property + #[zbus(property, name = "DefaultOOMPolicy")] + fn default_oompolicy(&self) -> zbus::Result; + + /// DefaultRestartUSec property + #[zbus(property, name = "DefaultRestartUSec")] + fn default_restart_usec(&self) -> zbus::Result; + + /// DefaultStandardError property + #[zbus(property)] + fn default_standard_error(&self) -> zbus::Result; + + /// DefaultStandardOutput property + #[zbus(property)] + fn default_standard_output(&self) -> zbus::Result; + + /// DefaultStartLimitBurst property + #[zbus(property)] + fn default_start_limit_burst(&self) -> zbus::Result; + + /// DefaultStartLimitIntervalUSec property + #[zbus(property, name = "DefaultStartLimitIntervalUSec")] + fn default_start_limit_interval_usec(&self) -> zbus::Result; + + /// DefaultTasksAccounting property + #[zbus(property)] + fn default_tasks_accounting(&self) -> zbus::Result; + + /// DefaultTasksMax property + #[zbus(property)] + fn default_tasks_max(&self) -> zbus::Result; + + /// DefaultTimeoutAbortUSec property + #[zbus(property, name = "DefaultTimeoutAbortUSec")] + fn default_timeout_abort_usec(&self) -> zbus::Result; + + /// DefaultTimeoutStartUSec property + #[zbus(property, name = "DefaultTimeoutStartUSec")] + fn default_timeout_start_usec(&self) -> zbus::Result; + + /// DefaultTimeoutStopUSec property + #[zbus(property, name = "DefaultTimeoutStopUSec")] + fn default_timeout_stop_usec(&self) -> zbus::Result; + + /// DefaultTimerAccuracyUSec property + #[zbus(property, name = "DefaultTimerAccuracyUSec")] + fn default_timer_accuracy_usec(&self) -> zbus::Result; + + /// Environment property + #[zbus(property)] + fn environment(&self) -> zbus::Result>; + + /// ExitCode property + #[zbus(property)] + fn exit_code(&self) -> zbus::Result; + + /// Features property + #[zbus(property)] + fn features(&self) -> zbus::Result; + + /// FinishTimestamp property + #[zbus(property)] + fn finish_timestamp(&self) -> zbus::Result; + + /// FinishTimestampMonotonic property + #[zbus(property)] + fn finish_timestamp_monotonic(&self) -> zbus::Result; + + /// FirmwareTimestamp property + #[zbus(property)] + fn firmware_timestamp(&self) -> zbus::Result; + + /// FirmwareTimestampMonotonic property + #[zbus(property)] + fn firmware_timestamp_monotonic(&self) -> zbus::Result; + + /// GeneratorsFinishTimestamp property + #[zbus(property)] + fn generators_finish_timestamp(&self) -> zbus::Result; + + /// GeneratorsFinishTimestampMonotonic property + #[zbus(property)] + fn generators_finish_timestamp_monotonic(&self) -> zbus::Result; + + /// GeneratorsStartTimestamp property + #[zbus(property)] + fn generators_start_timestamp(&self) -> zbus::Result; + + /// GeneratorsStartTimestampMonotonic property + #[zbus(property)] + fn generators_start_timestamp_monotonic(&self) -> zbus::Result; + + /// InitRDGeneratorsFinishTimestamp property + #[zbus(property, name = "InitRDGeneratorsFinishTimestamp")] + fn init_rdgenerators_finish_timestamp(&self) -> zbus::Result; + + /// InitRDGeneratorsFinishTimestampMonotonic property + #[zbus(property, name = "InitRDGeneratorsFinishTimestampMonotonic")] + fn init_rdgenerators_finish_timestamp_monotonic(&self) -> zbus::Result; + + /// InitRDGeneratorsStartTimestamp property + #[zbus(property, name = "InitRDGeneratorsStartTimestamp")] + fn init_rdgenerators_start_timestamp(&self) -> zbus::Result; + + /// InitRDGeneratorsStartTimestampMonotonic property + #[zbus(property, name = "InitRDGeneratorsStartTimestampMonotonic")] + fn init_rdgenerators_start_timestamp_monotonic(&self) -> zbus::Result; + + /// InitRDSecurityFinishTimestamp property + #[zbus(property, name = "InitRDSecurityFinishTimestamp")] + fn init_rdsecurity_finish_timestamp(&self) -> zbus::Result; + + /// InitRDSecurityFinishTimestampMonotonic property + #[zbus(property, name = "InitRDSecurityFinishTimestampMonotonic")] + fn init_rdsecurity_finish_timestamp_monotonic(&self) -> zbus::Result; + + /// InitRDSecurityStartTimestamp property + #[zbus(property, name = "InitRDSecurityStartTimestamp")] + fn init_rdsecurity_start_timestamp(&self) -> zbus::Result; + + /// InitRDSecurityStartTimestampMonotonic property + #[zbus(property, name = "InitRDSecurityStartTimestampMonotonic")] + fn init_rdsecurity_start_timestamp_monotonic(&self) -> zbus::Result; + + /// InitRDTimestamp property + #[zbus(property, name = "InitRDTimestamp")] + fn init_rdtimestamp(&self) -> zbus::Result; + + /// InitRDTimestampMonotonic property + #[zbus(property, name = "InitRDTimestampMonotonic")] + fn init_rdtimestamp_monotonic(&self) -> zbus::Result; + + /// InitRDUnitsLoadFinishTimestamp property + #[zbus(property, name = "InitRDUnitsLoadFinishTimestamp")] + fn init_rdunits_load_finish_timestamp(&self) -> zbus::Result; + + /// InitRDUnitsLoadFinishTimestampMonotonic property + #[zbus(property, name = "InitRDUnitsLoadFinishTimestampMonotonic")] + fn init_rdunits_load_finish_timestamp_monotonic(&self) -> zbus::Result; + + /// InitRDUnitsLoadStartTimestamp property + #[zbus(property, name = "InitRDUnitsLoadStartTimestamp")] + fn init_rdunits_load_start_timestamp(&self) -> zbus::Result; + + /// InitRDUnitsLoadStartTimestampMonotonic property + #[zbus(property, name = "InitRDUnitsLoadStartTimestampMonotonic")] + fn init_rdunits_load_start_timestamp_monotonic(&self) -> zbus::Result; + + /// KExecWatchdogUSec property + #[zbus(property, name = "KExecWatchdogUSec")] + fn kexec_watchdog_usec(&self) -> zbus::Result; + #[zbus(property, name = "KExecWatchdogUSec")] + fn set_kexec_watchdog_usec(&self, value: u64) -> zbus::Result<()>; + + /// KernelTimestamp property + #[zbus(property)] + fn kernel_timestamp(&self) -> zbus::Result; + + /// KernelTimestampMonotonic property + #[zbus(property)] + fn kernel_timestamp_monotonic(&self) -> zbus::Result; + + /// LoaderTimestamp property + #[zbus(property)] + fn loader_timestamp(&self) -> zbus::Result; + + /// LoaderTimestampMonotonic property + #[zbus(property)] + fn loader_timestamp_monotonic(&self) -> zbus::Result; + + /// LogLevel property + #[zbus(property)] + fn log_level(&self) -> zbus::Result; + #[zbus(property)] + fn set_log_level(&self, value: &str) -> zbus::Result<()>; + + /// LogTarget property + #[zbus(property)] + fn log_target(&self) -> zbus::Result; + #[zbus(property)] + fn set_log_target(&self, value: &str) -> zbus::Result<()>; + + /// NFailedJobs property + #[zbus(property, name = "NFailedJobs")] + fn nfailed_jobs(&self) -> zbus::Result; + + /// NFailedUnits property + #[zbus(property, name = "NFailedUnits")] + fn nfailed_units(&self) -> zbus::Result; + + /// NInstalledJobs property + #[zbus(property, name = "NInstalledJobs")] + fn ninstalled_jobs(&self) -> zbus::Result; + + /// NJobs property + #[zbus(property, name = "NJobs")] + fn njobs(&self) -> zbus::Result; + + /// NNames property + #[zbus(property, name = "NNames")] + fn nnames(&self) -> zbus::Result; + + /// Progress property + #[zbus(property)] + fn progress(&self) -> zbus::Result; + + /// RebootWatchdogUSec property + #[zbus(property, name = "RebootWatchdogUSec")] + fn reboot_watchdog_usec(&self) -> zbus::Result; + #[zbus(property, name = "RebootWatchdogUSec")] + fn set_reboot_watchdog_usec(&self, value: u64) -> zbus::Result<()>; + + /// RuntimeWatchdogUSec property + #[zbus(property, name = "RuntimeWatchdogUSec")] + fn runtime_watchdog_usec(&self) -> zbus::Result; + #[zbus(property, name = "RuntimeWatchdogUSec")] + fn set_runtime_watchdog_usec(&self, value: u64) -> zbus::Result<()>; + + /// SecurityFinishTimestamp property + #[zbus(property)] + fn security_finish_timestamp(&self) -> zbus::Result; + + /// SecurityFinishTimestampMonotonic property + #[zbus(property)] + fn security_finish_timestamp_monotonic(&self) -> zbus::Result; + + /// SecurityStartTimestamp property + #[zbus(property)] + fn security_start_timestamp(&self) -> zbus::Result; + + /// SecurityStartTimestampMonotonic property + #[zbus(property)] + fn security_start_timestamp_monotonic(&self) -> zbus::Result; + + /// ServiceWatchdogs property + #[zbus(property)] + fn service_watchdogs(&self) -> zbus::Result; + #[zbus(property)] + fn set_service_watchdogs(&self, value: bool) -> zbus::Result<()>; + + /// ShowStatus property + #[zbus(property)] + fn show_status(&self) -> zbus::Result; + + /// SystemState property + #[zbus(property)] + fn system_state(&self) -> zbus::Result; + + /// Tainted property + #[zbus(property)] + fn tainted(&self) -> zbus::Result; + + /// TimerSlackNSec property + #[zbus(property, name = "TimerSlackNSec")] + fn timer_slack_nsec(&self) -> zbus::Result; + + /// UnitPath property + #[zbus(property)] + fn unit_path(&self) -> zbus::Result>; + + /// UnitsLoadFinishTimestamp property + #[zbus(property)] + fn units_load_finish_timestamp(&self) -> zbus::Result; + + /// UnitsLoadFinishTimestampMonotonic property + #[zbus(property)] + fn units_load_finish_timestamp_monotonic(&self) -> zbus::Result; + + /// UnitsLoadStartTimestamp property + #[zbus(property)] + fn units_load_start_timestamp(&self) -> zbus::Result; + + /// UnitsLoadStartTimestampMonotonic property + #[zbus(property)] + fn units_load_start_timestamp_monotonic(&self) -> zbus::Result; + + /// UserspaceTimestamp property + #[zbus(property)] + fn userspace_timestamp(&self) -> zbus::Result; + + /// UserspaceTimestampMonotonic property + #[zbus(property)] + fn userspace_timestamp_monotonic(&self) -> zbus::Result; + + /// Version property + #[zbus(property)] + fn version(&self) -> zbus::Result; + + /// Virtualization property + #[zbus(property)] + fn virtualization(&self) -> zbus::Result; +} diff --git a/src/systemd/error.rs b/src/systemd/error.rs new file mode 100644 index 0000000..9189174 --- /dev/null +++ b/src/systemd/error.rs @@ -0,0 +1,18 @@ +// Copyright (c) 2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +pub type Result = std::result::Result; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("invalid argument")] + InvalidArgument, + + #[error("obsolete systemd, please upgrade your systemd")] + ObsoleteSystemd, + + #[error("resource not supported by cgroups v1")] + CgroupsV1NotSupported, +} diff --git a/src/systemd/memory.rs b/src/systemd/memory.rs new file mode 100644 index 0000000..66c6cd2 --- /dev/null +++ b/src/systemd/memory.rs @@ -0,0 +1,32 @@ +// Copyright (c) 2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +use crate::systemd::error::{Error, Result}; +use crate::systemd::{MEMORY_LIMIT, MEMORY_LOW, MEMORY_MAX, MEMORY_SWAP_MAX}; + +/// Returns the property for memory limit. +pub fn limit(limit: i64, v2: bool) -> Result<(&'static str, u64)> { + let id = if v2 { MEMORY_MAX } else { MEMORY_LIMIT }; + + Ok((id, limit as u64)) +} + +/// Returns the property for memory limit. +pub fn low(low: i64, v2: bool) -> Result<(&'static str, u64)> { + if !v2 { + return Err(Error::CgroupsV1NotSupported); + } + + Ok((MEMORY_LOW, low as u64)) +} + +/// Returns the property for memory swap. +pub fn swap(swap: i64, v2: bool) -> Result<(&'static str, u64)> { + if !v2 { + return Err(Error::CgroupsV1NotSupported); + } + + Ok((MEMORY_SWAP_MAX, swap as u64)) +} diff --git a/src/systemd/mod.rs b/src/systemd/mod.rs new file mode 100644 index 0000000..66e35eb --- /dev/null +++ b/src/systemd/mod.rs @@ -0,0 +1,25 @@ +// Copyright (c) 2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +pub mod cpu; +pub mod cpuset; +pub mod dbus; +pub use dbus::SystemdClient; +mod consts; +pub use consts::*; +pub mod error; +pub mod memory; +pub mod pids; +pub mod props; +pub use props::Property; +pub mod utils; + +pub const DEFAULT_SLICE: &str = "system.slice"; + +pub const SLICE_SUFFIX: &str = ".slice"; +pub const SCOPE_SUFFIX: &str = ".scope"; + +pub const CPU_SYSTEMD_VERSION: usize = 242; +pub const CPUSET_SYSTEMD_VERSION: usize = 244; diff --git a/src/systemd/pids.rs b/src/systemd/pids.rs new file mode 100644 index 0000000..5dab66c --- /dev/null +++ b/src/systemd/pids.rs @@ -0,0 +1,11 @@ +// Copyright (c) 2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +use crate::systemd::error::Result; +use crate::systemd::TASKS_MAX; + +pub fn max(max: i64) -> Result<(&'static str, u64)> { + Ok((TASKS_MAX, max as u64)) +} diff --git a/src/systemd/props.rs b/src/systemd/props.rs new file mode 100644 index 0000000..8b6f8e2 --- /dev/null +++ b/src/systemd/props.rs @@ -0,0 +1,170 @@ +// Copyright (c) 2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +use zbus::zvariant::Value as ZbusValue; + +use crate::fs::hierarchies; +use crate::systemd::utils::is_slice_unit; +use crate::systemd::{ + BLOCK_IO_ACCOUNTING, CPU_ACCOUNTING, DEFAULT_DEPENDENCIES, DEFAULT_DESCRIPTION, DELEGATE, + DESCRIPTION, IO_ACCOUNTING, MEMORY_ACCOUNTING, PIDS, SLICE, TASKS_ACCOUNTING, + TIMEOUT_STOP_USEC, WANTS, +}; + +pub type Property<'a> = (&'a str, ZbusValue<'a>); + +#[derive(Debug, Clone, Default)] +pub struct PropertiesBuilder { + cpu_accounting: Option, + // MemoryAccount is for cgroup v2 as documented in dbus. However, + // "github.com/opencontainer/runc" uses it for all. Shall we follow the + // same way? + memory_accounting: Option, + task_accounting: Option, + // Use IO_ACCOUNTING for cgroup v2 and BLOCK_IO_ACCOUNTING for cgroup v1. + io_accounting: Option, + default_dependencies: Option, + description: Option, + wants: Option, + slice: Option, + delegate: Option, + pids: Option>, + timeout_stop_usec: Option, +} + +impl PropertiesBuilder { + pub fn default_cgroup(slice: &str, unit: &str) -> Self { + let mut builder = Self::default() + .cpu_accounting(true) + .memory_accounting(true) + .task_accounting(true) + .io_accounting(true) + .default_dependencies(false) + .description(format!("{} {}:{}", DEFAULT_DESCRIPTION, slice, unit)); + + if is_slice_unit(unit) { + // If we create a slice, the parent is defined via a Wants=. + builder = builder.wants(slice.to_string()); + } else { + // Otherwise it's a scope, which we put into a Slice=. + builder = builder.slice(slice.to_string()); + // Assume scopes always support delegation (supported since systemd v218). + builder = builder.delegate(true); + } + + builder + } + + pub fn cpu_accounting(mut self, enabled: bool) -> Self { + self.cpu_accounting = Some(enabled); + self + } + + pub fn memory_accounting(mut self, enabled: bool) -> Self { + self.memory_accounting = Some(enabled); + self + } + + pub fn task_accounting(mut self, enabled: bool) -> Self { + self.task_accounting = Some(enabled); + self + } + + pub fn io_accounting(mut self, enabled: bool) -> Self { + self.io_accounting = Some(enabled); + self + } + + pub fn default_dependencies(mut self, enabled: bool) -> Self { + self.default_dependencies = Some(enabled); + self + } + + pub fn description(mut self, desc: String) -> Self { + self.description = Some(desc); + self + } + + pub fn wants(mut self, wants: String) -> Self { + self.wants = Some(wants); + self + } + + pub fn slice(mut self, slice: String) -> Self { + self.slice = Some(slice); + self + } + + pub fn delegate(mut self, enabled: bool) -> Self { + self.delegate = Some(enabled); + self + } + + pub fn pids(mut self, pids: Vec) -> Self { + self.pids = Some(pids); + self + } + + pub fn timeout_stop_usec(mut self, timeout: u64) -> Self { + self.timeout_stop_usec = Some(timeout); + self + } + + pub fn build(self) -> Vec> { + let mut props = vec![]; + + if let Some(cpu_accounting) = self.cpu_accounting { + props.push((CPU_ACCOUNTING, ZbusValue::Bool(cpu_accounting))); + } + + if let Some(memory_accounting) = self.memory_accounting { + props.push((MEMORY_ACCOUNTING, ZbusValue::Bool(memory_accounting))); + } + + if let Some(task_accounting) = self.task_accounting { + props.push((TASKS_ACCOUNTING, ZbusValue::Bool(task_accounting))); + } + + if let Some(io_accounting) = self.io_accounting { + if hierarchies::is_cgroup2_unified_mode() { + props.push((IO_ACCOUNTING, ZbusValue::Bool(io_accounting))); + } else { + props.push((BLOCK_IO_ACCOUNTING, ZbusValue::Bool(io_accounting))); + } + } + + if let Some(default_dependencies) = self.default_dependencies { + props.push((DEFAULT_DEPENDENCIES, ZbusValue::Bool(default_dependencies))); + } + + if let Some(description) = self.description { + props.push((DESCRIPTION, ZbusValue::Str(description.into()))); + } else { + props.push((DESCRIPTION, ZbusValue::Str(DEFAULT_DESCRIPTION.into()))); + } + + if let Some(wants) = self.wants { + props.push((WANTS, ZbusValue::Str(wants.into()))); + } + + if let Some(slice) = self.slice { + props.push((SLICE, ZbusValue::Str(slice.into()))); + } + + if let Some(delegate) = self.delegate { + props.push((DELEGATE, ZbusValue::Bool(delegate))); + } + + if let Some(pids) = self.pids { + props.push((PIDS, ZbusValue::Array(pids.into()))); + } + + if let Some(timeout) = self.timeout_stop_usec { + props.push((TIMEOUT_STOP_USEC, ZbusValue::U64(timeout))); + } + + props + } +} diff --git a/src/systemd/utils.rs b/src/systemd/utils.rs new file mode 100644 index 0000000..c654c59 --- /dev/null +++ b/src/systemd/utils.rs @@ -0,0 +1,64 @@ +// Copyright (c) 2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +use crate::systemd::error::{Error, Result}; +use crate::systemd::{SCOPE_SUFFIX, SLICE_SUFFIX}; + +/// Check if a systemd unit name is a slice unit. +pub fn is_slice_unit(name: &str) -> bool { + name.ends_with(SLICE_SUFFIX) +} + +/// Check if a systemd unit name is a scope unit. +pub fn is_scope_unit(name: &str) -> bool { + name.ends_with(SCOPE_SUFFIX) +} + +/// Expand a slice name to a full path in the filesystem. +/// +/// # Arguments +/// +/// * `slice` - A string slice that holds the slice name in the format +/// "xxx-yyy-zzz.slice". +/// +/// # Returns +/// +/// A string that represents the full path of the slice in the filesystem. +/// In the above case, the value would be +/// "xxx.slice/xxx-yyy.slice/xxx-yyy-zzz.slice". +pub fn expand_slice(slice: &str) -> Result { + // Name has to end with ".slice", but can't be just ".slice". + if !slice.ends_with(SLICE_SUFFIX) || slice.len() < SLICE_SUFFIX.len() { + return Err(Error::InvalidArgument); + } + + // Path-separators are not allowed. + if slice.contains('/') { + return Err(Error::InvalidArgument); + } + + let name = slice.trim_end_matches(SLICE_SUFFIX); + + // If input was -.slice, we should just return root now + if name == "-" { + return Ok("".to_string()); + } + + let mut slice_path = String::new(); + let mut prefix = String::new(); + for sub_slice in name.split('-') { + if sub_slice.is_empty() { + return Err(Error::InvalidArgument); + } + + slice_path = format!("{}/{}{}{}", slice_path, prefix, sub_slice, SLICE_SUFFIX); + prefix = format!("{}{}-", prefix, sub_slice); + } + + // We need a relative path, so remove the first slash. + slice_path.remove(0); + + Ok(slice_path) +} From 0c26caecfc4d8f5949ab1276d60960a35856a13a Mon Sep 17 00:00:00 2001 From: Xuewei Niu Date: Mon, 7 Jul 2025 19:38:48 +0800 Subject: [PATCH 4/6] manager: Introduce SystemdManager Systemd manager takes cgroups path in the format of "parent:scope_prefix:name" to create and manipulate cgroups through systemd. It does value conversions for resources defined in the Linux resources from the OCI spec, such as CPU quota, period, etc. Signed-off-by: Xuewei Niu --- src/lib.rs | 2 +- src/manager/error.rs | 8 + src/manager/mod.rs | 9 ++ src/manager/systemd.rs | 341 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 src/manager/systemd.rs diff --git a/src/lib.rs b/src/lib.rs index 1f163a4..86762b2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ pub mod fs; #[cfg(feature = "oci")] pub mod manager; #[cfg(feature = "oci")] -pub use manager::{FsManager, Manager}; +pub use manager::{FsManager, Manager, SystemdManager}; pub mod stats; pub use stats::CgroupStats; pub mod systemd; diff --git a/src/manager/error.rs b/src/manager/error.rs index 5b4b3cb..132dfd0 100644 --- a/src/manager/error.rs +++ b/src/manager/error.rs @@ -4,6 +4,8 @@ // use crate::fs::error::Error as CgroupfsError; +use crate::systemd::dbus::error::Error as SystemdDbusError; +use crate::systemd::error::Error as SystemdCgroupError; pub type Result = std::result::Result; @@ -17,4 +19,10 @@ pub enum Error { #[error("cgroupfs error: {0}")] Cgroupfs(#[from] CgroupfsError), + + #[error("systemd cgroup error: {0}")] + SystemdCgroup(#[from] SystemdCgroupError), + + #[error("systemd dbus error: {0}")] + SystemdDbus(#[from] SystemdDbusError), } diff --git a/src/manager/mod.rs b/src/manager/mod.rs index 2f5005d..9dd54be 100644 --- a/src/manager/mod.rs +++ b/src/manager/mod.rs @@ -9,12 +9,21 @@ use std::collections::HashMap; pub use error::{Error, Result}; mod fs; pub use fs::FsManager; +mod systemd; +pub use systemd::SystemdManager; mod conv; use oci_spec::runtime::LinuxResources; +use crate::systemd::SLICE_SUFFIX; use crate::{CgroupPid, CgroupStats, FreezerState}; +/// Check if the cgroups path is a systemd cgroup. +pub fn is_systemd_cgroup(cgroups_path: &str) -> bool { + let parts: Vec<&str> = cgroups_path.split(':').collect(); + parts.len() == 3 && parts[0].ends_with(SLICE_SUFFIX) +} + /// Manage cgroups designed for OCI containers. pub trait Manager: Send + Sync { /// Add a process specified by its tgid. diff --git a/src/manager/systemd.rs b/src/manager/systemd.rs new file mode 100644 index 0000000..55557fd --- /dev/null +++ b/src/manager/systemd.rs @@ -0,0 +1,341 @@ +// Copyright (c) 2025 Ant Group +// +// SPDX-License-Identifier: Apache-2.0 or MIT +// + +use std::collections::HashMap; + +use oci_spec::runtime::{LinuxCpu, LinuxMemory, LinuxPids, LinuxResources}; +use zbus::zvariant::Value as ZbusValue; + +use crate::manager::conv; +use crate::manager::error::{Error, Result}; +use crate::manager::fs::{join_path, FsManager}; +use crate::systemd::props::PropertiesBuilder; +use crate::systemd::utils::expand_slice; +use crate::systemd::{ + cpu, cpuset, memory, pids, Property, SystemdClient, DEFAULT_SLICE, SCOPE_SUFFIX, SLICE_SUFFIX, + TIMEOUT_STOP_USEC, +}; +use crate::{CgroupPid, CgroupStats, FreezerState, Manager}; + +/// Default kernel value for cpu quota period is 100000 us (100 ms), same +/// for v1 [1] and v2 [2]. +/// +/// 1: https://www.kernel.org/doc/html/latest/scheduler/sched-bwc.html +/// 2: https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html +const DEFAULT_CPU_QUOTA_PERIOD: u64 = 100_000; // 100ms + +pub struct SystemdManager<'a> { + /// The name of slice + slice: String, + /// The name of unit + unit: String, + /// Systemd client + systemd_client: SystemdClient<'a>, + /// Cgroupfs manager + fs_manager: FsManager, +} + +impl SystemdManager<'_> { + fn parse_slice_and_unit(path: &str) -> Result<(String, String)> { + let parts: Vec<&str> = path.split(':').collect(); + if parts.len() != 3 { + return Err(Error::InvalidArgument); + } + + let slice = if parts[0].is_empty() { + DEFAULT_SLICE.to_string() + } else { + parts[0].to_string() + }; + + let unit = new_unit_name(parts[1], parts[2]); + + Ok((slice, unit)) + } + + /// Create a new `SystemdManager` from a cgroup path. + /// + /// # Arguments + /// + /// * `path` - A string slice that holds the cgroup path in the format + /// "parent:scope_prefix:name". + pub fn new(path: &str) -> Result { + let (slice, unit) = Self::parse_slice_and_unit(path)?; + let props = PropertiesBuilder::default_cgroup(&slice, &unit).build(); + let slice_base = expand_slice(&slice)?; + + let fs_base = join_path(&slice_base, &unit); + let fs_manager = FsManager::new(&fs_base)?; + + let cgroup = SystemdClient::new(&unit, props)?; + + Ok(Self { + slice, + unit, + fs_manager, + systemd_client: cgroup, + }) + } +} + +impl SystemdManager<'_> { + /// Get the slice name. + pub fn slice(&self) -> &str { + &self.slice + } + + /// Get the unit name. + pub fn unit(&self) -> &str { + &self.unit + } + + fn set_cpuset( + &self, + props: &mut Vec, + linux_cpu: &LinuxCpu, + systemd_version: usize, + ) -> Result<()> { + if let Some(cpus) = linux_cpu.cpus().as_ref() { + let (id, value) = cpuset::cpus(cpus, systemd_version)?; + props.push((id, value.into())); + } + + if let Some(mems) = linux_cpu.mems().as_ref() { + let (id, value) = cpuset::mems(mems, systemd_version)?; + props.push((id, value.into())); + } + + Ok(()) + } + + fn set_cpu( + &self, + props: &mut Vec, + linux_cpu: &LinuxCpu, + systemd_version: usize, + ) -> Result<()> { + if let Some(shares) = linux_cpu.shares() { + let shares = if self.v2() { + conv::cpu_shares_to_cgroup_v2(shares) + } else { + shares + }; + let (id, value) = cpu::shares(shares, self.v2())?; + props.push((id, value.into())); + } + + let period = linux_cpu.period().unwrap_or(0); + let quota = linux_cpu.quota().unwrap_or(0); + + if period != 0 { + let (id, value) = cpu::period(period, systemd_version)?; + props.push((id, value.into())); + } + + if period != 0 || quota != 0 { + // Corresponds to USEC_INFINITY in systemd + let mut quota_systemd = u64::MAX; + let mut period = period; + if quota > 0 { + if period == 0 { + period = DEFAULT_CPU_QUOTA_PERIOD; + } + // systemd converts CPUQuotaPerSecUSec (microseconds per + // CPU second) to CPUQuota (integer percentage of CPU) + // internally. This means that if a fractional percent of + // CPU is indicated by Resources.CpuQuota, we need to round + // up to the nearest 10ms (1% of a second) such that child + // cgroups can set the cpu.cfs_quota_us they expect. + quota_systemd = ((quota as u64) * s_to_us(1)) / period; + if quota_systemd % ms_to_us(10) != 0 { + quota_systemd = (quota_systemd / ms_to_us(10) + 1) * ms_to_us(10); + } + } + let (id, value) = cpu::quota(quota_systemd)?; + props.push((id, value.into())); + } + + Ok(()) + } + + fn set_memory(&self, props: &mut Vec, linux_memory: &LinuxMemory) -> Result<()> { + let v2 = self.v2(); + + let mem_limit = linux_memory.limit().unwrap_or(0); + if mem_limit != 0 { + let (id, value) = memory::limit(mem_limit, v2)?; + props.push((id, value.into())); + } + + let reservation = linux_memory.reservation().unwrap_or(0); + if reservation != 0 && v2 { + let (id, value) = memory::low(reservation, v2)?; + props.push((id, value.into())); + } + + let memswap_limit = linux_memory.swap().unwrap_or(0); + if memswap_limit != 0 && v2 { + let memswap_limit = conv::memory_swap_to_cgroup_v2(memswap_limit, mem_limit)?; + let (id, value) = memory::swap(memswap_limit, v2)?; + props.push((id, value.into())); + } + + Ok(()) + } + + fn set_pids(&self, props: &mut Vec, linux_pids: &LinuxPids) -> Result<()> { + let limit = linux_pids.limit(); + if limit == -1 || limit > 0 { + let (id, value) = pids::max(limit)?; + props.push((id, value.into())); + } + + Ok(()) + } + + /// The systemd sends SIGTERM to processes in the unit on stop. Once a + /// timeout occurs, SIGKILL will be sent to the processes. + /// + /// The item could be retrieved by: + /// + /// ```bash + /// $ systemctl show -p TimeoutStopUSec + /// ``` + pub fn set_term_timeout(&mut self, timeout_in_sec: u64) -> Result<()> { + let timeout_in_usec = timeout_in_sec * 1_000_000; + let prop = (TIMEOUT_STOP_USEC, ZbusValue::U64(timeout_in_usec)); + self.systemd_client.set_properties(&[prop])?; + Ok(()) + } +} + +impl Manager for SystemdManager<'_> { + fn add_proc(&mut self, pid: CgroupPid) -> Result<()> { + if !self.systemd_client.exists() { + self.systemd_client.set_pid_prop(pid)?; + self.systemd_client.start()?; + // The fs_manager was created in load mode, which doesn't create + // the cgroups. So we create them here. + self.fs_manager.create_cgroups()?; + return Ok(()); + } + + let subcgroup = self.fs_manager.subcgroup(); + self.systemd_client.add_process(pid, subcgroup)?; + + Ok(()) + } + + /// `add_thread()` is the same as `add_proc()`, as systemd doesn't + /// expose an API to add a thread directly. As a result, the whole + /// threads belonging to one process will be added to this cgroup. + fn add_thread(&mut self, pid: CgroupPid) -> Result<()> { + self.add_proc(pid) + } + + fn cgroup_path(&self, subsystem: Option<&str>) -> Result { + self.fs_manager.cgroup_path(subsystem) + } + + /// Destroy the cgroup and stop the transient unit. + /// + /// Please note that if the current manager is in the cgroup, the + /// manager will be killed with SIGTERM signal. If you do not intend + /// that, please ignore the signal and do cleanup things immediately. + /// Systemd will forcibly terminate the process with SIGKILL after a + /// while. + fn destroy(&mut self) -> Result<()> { + self.systemd_client.stop()?; + Ok(()) + } + + fn enable_cpus_topdown(&self, cpus: &str) -> Result<()> { + self.fs_manager.enable_cpus_topdown(cpus) + } + + fn freeze(&self, state: FreezerState) -> Result<()> { + match state { + FreezerState::Thawed => self.systemd_client.thaw()?, + FreezerState::Frozen => self.systemd_client.freeze()?, + FreezerState::Freezing => return Err(Error::InvalidArgument), + } + + Ok(()) + } + + fn pids(&self) -> Result> { + self.fs_manager.pids() + } + + fn set(&mut self, resources: &LinuxResources) -> Result<()> { + let mut props = vec![]; + + let systemd_version = self.systemd_client.systemd_version()?; + + if let Some(linux_cpu) = resources.cpu() { + self.set_cpuset(&mut props, linux_cpu, systemd_version)?; + self.set_cpu(&mut props, linux_cpu, systemd_version)?; + } + + if let Some(linux_memory) = resources.memory() { + self.set_memory(&mut props, linux_memory)?; + } + + if let Some(linux_pids) = resources.pids() { + self.set_pids(&mut props, linux_pids)?; + } + + self.systemd_client.set_properties(&props)?; + + Ok(()) + } + + fn stats(&self) -> CgroupStats { + self.fs_manager.stats() + } + + fn paths(&self) -> &HashMap { + self.fs_manager.paths() + } + + fn mounts(&self) -> &HashMap { + self.fs_manager.mounts() + } + + fn systemd(&self) -> bool { + true + } + + fn v2(&self) -> bool { + self.fs_manager.v2() + } +} + +fn new_unit_name(scope_prefix: &str, name: &str) -> String { + // By default, we create a scope unless the user explicitly asks + // for a slice. + if !name.ends_with(SLICE_SUFFIX) { + if scope_prefix.is_empty() { + // {name}.scope + return format!("{}{}", name, SCOPE_SUFFIX); + } + // {scope_prefix}-{name}.scope + return format!("{}-{}{}", scope_prefix, name, SCOPE_SUFFIX); + } + + name.to_string() +} + +#[inline] +/// Convert milliseconds to microseconds. +fn ms_to_us(ms: u64) -> u64 { + ms * 1_000 +} + +#[inline] +/// Convert seconds to microseconds. +fn s_to_us(s: u64) -> u64 { + s * 1_000_000 +} From d69517847bf603948bd3cc8fa372342acd3e3a69 Mon Sep 17 00:00:00 2001 From: Xuewei Niu Date: Thu, 10 Jul 2025 16:01:04 +0800 Subject: [PATCH 5/6] systemd: Add unit test cases The tests cover `SystemdClient` and methods from `utils` mods. Please note that the `SystemdClient` tests do not run well in parallel, but work well in sequence. Please run them with `--test-threads=1`. Signed-off-by: Xuewei Niu --- Cargo.toml | 1 + Makefile | 14 +- src/lib.rs | 43 +++++ src/systemd/dbus/client.rs | 380 +++++++++++++++++++++++++++++++++++++ src/systemd/utils.rs | 37 ++++ 5 files changed, 473 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f25b3bf..ffba0e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ bit-vec = "0.6" [dev-dependencies] libc = "0.2.76" +rand = "0.8" [features] default = [] diff --git a/Makefile b/Makefile index 89fe2a5..91a69d9 100644 --- a/Makefile +++ b/Makefile @@ -20,8 +20,18 @@ build: debug # .PHONY: test -test: - cargo test -- --color always --nocapture +test: test-systemd + cargo test -- --color always --nocapture \ + --skip systemd::dbus::client::tests + +.PHONY: test-systemd +# Tests that manipulate cgroups should run in sequence, so that +# `--test-threads=1` is used. +test-systemd: + cargo test --package cgroups-rs --lib \ + -- systemd::dbus::client::tests \ + --color always --nocapture \ + --test-threads=1 .PHONY: check check: fmt clippy diff --git a/src/lib.rs b/src/lib.rs index 86762b2..362f4b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,3 +48,46 @@ impl From<&std::process::Child> for CgroupPid { CgroupPid { pid: u.id() as u64 } } } + +#[cfg(test)] +pub mod tests { + use std::fs; + use std::process::{Child, Command, Stdio}; + + /// Start a mock subprocess that will sleep forever + pub fn spawn_sleep_inf() -> Child { + let child = Command::new("sleep") + .arg("infinity") + .spawn() + .expect("Failed to start mock subprocess"); + child + } + + pub fn spawn_yes() -> Child { + let devnull = fs::File::create("/dev/null").expect("cannot open /dev/null"); + let child = Command::new("yes") + .stdout(Stdio::from(devnull)) + .spawn() + .expect("Failed to start mock subprocess"); + child + } + + pub fn systemd_version() -> Option { + let output = Command::new("systemd").arg("--version").output().ok()?; // Return None if command execution fails + + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8_lossy(&output.stdout); + + // The first line is typically like "systemd 254 (254.5-1-arch)" + let first_line = stdout.lines().next()?; + let mut words = first_line.split_whitespace(); + + words.next()?; // Skip the "systemd" word + let version_str = words.next()?; // The version number as string + + version_str.parse::().ok() + } +} diff --git a/src/systemd/dbus/client.rs b/src/systemd/dbus/client.rs index 27305f0..8bbe245 100644 --- a/src/systemd/dbus/client.rs +++ b/src/systemd/dbus/client.rs @@ -190,3 +190,383 @@ fn ignore_no_such_unit(result: ZbusResult) -> ZbusResult { } result.map(|_| false) } + +#[cfg(test)] +pub mod tests { + //! Unit tests for the SystemdClient + //! + //! Not sure why the tests are going to fail if we run them in + //! parallel. Everything goes smoothly in serial. + //! + //! $ cargo test --package cgroups-rs --lib \ + //! -- systemd::dbus::client::tests \ + //! --show-output --test-threads=1 + + use std::fs; + use std::path::Path; + use std::process::Command; + use std::thread::sleep; + use std::time::Duration; + + use rand::distributions::Alphanumeric; + use rand::Rng; + + use crate::fs::hierarchies; + use crate::systemd::dbus::client::*; + use crate::systemd::props::PropertiesBuilder; + use crate::systemd::utils::expand_slice; + use crate::systemd::{DEFAULT_DESCRIPTION, DESCRIPTION, PIDS}; + use crate::tests::{spawn_sleep_inf, spawn_yes, systemd_version}; + + const TEST_SLICE: &str = "cgroupsrs-test.slice"; + + fn test_unit() -> String { + let rand_string: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(5) + .map(char::from) + .collect(); + format!("cri-pod{}.scope", rand_string) + } + + #[macro_export] + macro_rules! skip_if_no_systemd { + () => { + if $crate::tests::systemd_version().is_none() { + eprintln!("Test skipped, no systemd?"); + return; + } + }; + } + + fn systemd_show(unit: &str) -> String { + let output = Command::new("systemctl") + .arg("show") + .arg(unit) + .output() + .expect("Failed to execute systemctl show command"); + String::from_utf8_lossy(&output.stdout).to_string() + } + + fn start_default_cgroup(pid: CgroupPid, unit: &str) -> SystemdClient { + let mut props = PropertiesBuilder::default_cgroup(TEST_SLICE, unit).build(); + props.push((PIDS, Value::Array(vec![pid.pid as u32].into()))); + let cgroup = SystemdClient::new(unit, props).unwrap(); + // Stop the unit if it exists. + cgroup.stop().unwrap(); + + // Write the current process to the cgroup. + cgroup.start().unwrap(); + cgroup.add_process(pid, "/").unwrap(); + cgroup + } + + fn stop_cgroup(cgroup: &SystemdClient) { + cgroup.stop().unwrap(); + } + + #[test] + fn test_start() { + skip_if_no_systemd!(); + + let v2 = hierarchies::is_cgroup2_unified_mode(); + let unit = test_unit(); + let mut child = spawn_sleep_inf(); + let cgroup = start_default_cgroup(CgroupPid::from(child.id() as u64), &unit); + + let base = expand_slice(TEST_SLICE).unwrap(); + + // Check if the cgroup exists in the filesystem + let full_base = if v2 { + format!("/sys/fs/cgroup/{}", base) + } else { + format!("/sys/fs/cgroup/memory/{}", base) + }; + assert!( + Path::new(&full_base).exists(), + "Cgroup base path does not exist: {}", + full_base + ); + + // PIDs + let cgroup_procs_path = format!("{}/{}/cgroup.procs", full_base, &unit); + for i in 0..5 { + let content = fs::read_to_string(&cgroup_procs_path); + if let Ok(content) = &content { + if content.contains(&child.id().to_string()) { + break; + } + } + // Retry attempts exhausted, resulting in failure + if i == 4 { + let content = content.as_ref().unwrap(); + assert!( + content.contains(&child.id().to_string()), + "Cgroup procs does not contain the child process ID" + ); + } + // Wait 500ms before next retrying + sleep(Duration::from_millis(500)); + } + + // Check the unit from "systemctl show " + let output = systemd_show(&cgroup.unit); + + // Slice + assert!( + output + .lines() + .any(|line| line == format!("Slice={}", TEST_SLICE)), + "Slice not found" + ); + // Delegate + assert!( + output.lines().any(|line| line == "Delegate=yes"), + "Delegate not set" + ); + // DelegateControllers + // controllers: cpu cpuacct cpuset io blkio memory devices pids + let controllers = output + .lines() + .find(|line| line.starts_with("DelegateControllers=")) + .map(|line| line.trim_start_matches("DelegateControllers=")) + .unwrap(); + let controllers = controllers.split(' ').collect::>(); + assert!( + controllers.contains(&"cpu"), + "DelegateControllers cpu not set" + ); + assert!( + controllers.contains(&"cpuset"), + "DelegateControllers cpuset not set" + ); + if v2 { + assert!( + controllers.contains(&"io"), + "DelegateControllers io not set" + ); + } else { + assert!( + controllers.contains(&"blkio"), + "DelegateControllers blkio not set" + ); + } + assert!( + controllers.contains(&"memory"), + "DelegateControllers memory not set" + ); + assert!( + controllers.contains(&"pids"), + "DelegateControllers pids not set" + ); + + // CPUAccounting + assert!( + output.lines().any(|line| line == "CPUAccounting=yes"), + "CPUAccounting not set" + ); + // IOAccounting for v2, and BlockIOAccounting for v1 + if v2 { + assert!( + output.lines().any(|line| line == "IOAccounting=yes"), + "IOAccounting not set" + ); + } else { + assert!( + output.lines().any(|line| line == "BlockIOAccounting=yes"), + "BlockIOAccounting not set" + ); + } + // MemoryAccounting + assert!( + output.lines().any(|line| line == "MemoryAccounting=yes"), + "MemoryAccounting not set" + ); + // TasksAccounting + assert!( + output.lines().any(|line| line == "TasksAccounting=yes"), + "TasksAccounting not set" + ); + // ActiveState + assert!( + output.lines().any(|line| line == "ActiveState=active"), + "Unit is not active" + ); + + stop_cgroup(&cgroup); + child.wait().unwrap(); + } + + #[test] + fn test_stop() { + skip_if_no_systemd!(); + + let unit = test_unit(); + let mut child = spawn_sleep_inf(); + let cgroup = start_default_cgroup(CgroupPid::from(child.id() as u64), &unit); + + // Check ActiveState: expected to be "active" + let output = systemd_show(&cgroup.unit); + assert!( + output.lines().any(|line| line == "ActiveState=active"), + "Unit is not active" + ); + + stop_cgroup(&cgroup); + + // Check ActiveState: expected to be "inactive" + let output = systemd_show(&cgroup.unit); + assert!( + output.lines().any(|line| line == "ActiveState=inactive"), + "Unit is not inactive" + ); + + child.wait().unwrap(); + } + + #[test] + fn test_set_properties() { + skip_if_no_systemd!(); + + let unit = test_unit(); + let mut child = spawn_sleep_inf(); + let mut cgroup = start_default_cgroup(CgroupPid::from(child.id() as u64), &unit); + + let output = systemd_show(&cgroup.unit); + assert!( + output.lines().any(|line| line + == format!( + "Description={} {}:{}", + DEFAULT_DESCRIPTION, TEST_SLICE, unit + )), + "Initial description not set correctly" + ); + + let properties = [( + DESCRIPTION, + Value::Str("kata-container1 description".into()), + )]; + cgroup.set_properties(&properties).unwrap(); + assert!(cgroup.props.iter().any(|(k, v)| { + k == &DESCRIPTION && v == &Value::Str("kata-container1 description".into()) + })); + + let output = systemd_show(&cgroup.unit); + assert!( + output + .lines() + .any(|line| line == "Description=kata-container1 description"), + "Updated description not set correctly" + ); + + stop_cgroup(&cgroup); + child.wait().unwrap(); + } + + #[test] + fn test_freeze_and_thaw() { + skip_if_no_systemd!(); + + let unit = test_unit(); + let mut child = spawn_yes(); + let cgroup = start_default_cgroup(CgroupPid::from(child.id() as u64), &unit); + + // Freeze the unit + cgroup.freeze().unwrap(); + + let pid = child.id() as u64; + + let stat_path = format!("/proc/{}/stat", pid); + let content = fs::read_to_string(&stat_path).unwrap(); + // The process state is the third field, e.g.: + // 1234 (bash) S 1233 ... + // ^ + let mut content_iter = content.split_whitespace(); + assert_eq!( + content_iter.nth(2).unwrap(), + "S", + "Process should be in 'S' (sleeping) state after freezing" + ); + + // Thaw the unit + cgroup.thaw().unwrap(); + + // No more S now + let content = fs::read_to_string(&stat_path).unwrap(); + let mut content_iter = content.split_whitespace(); + assert_ne!( + content_iter.nth(2).unwrap(), + "S", + "Process should not be in 'S' (sleeping) state after thawing" + ); + + stop_cgroup(&cgroup); + child.wait().unwrap(); + } + + #[test] + fn test_systemd_version() { + skip_if_no_systemd!(); + + let unit = test_unit(); + let props = PropertiesBuilder::default_cgroup(TEST_SLICE, &unit).build(); + let cgroup = SystemdClient::new(&unit, props).unwrap(); + let version = cgroup.systemd_version().unwrap(); + + let expected_version = systemd_version().unwrap(); + assert_eq!(version, expected_version, "Systemd version mismatch"); + } + + #[test] + fn test_exists() { + skip_if_no_systemd!(); + + let unit = test_unit(); + let mut child = spawn_sleep_inf(); + let cgroup = start_default_cgroup(CgroupPid::from(child.id() as u64), &unit); + + assert!(cgroup.exists(), "Cgroup should exist after starting"); + + stop_cgroup(&cgroup); + child.wait().unwrap(); + } + + #[test] + fn test_add_process() { + skip_if_no_systemd!(); + + let unit = test_unit(); + let mut child = spawn_sleep_inf(); + let cgroup = start_default_cgroup(CgroupPid::from(child.id() as u64), &unit); + + let mut child1 = spawn_sleep_inf(); + let pid1 = CgroupPid::from(child1.id() as u64); + cgroup.add_process(pid1, "/").unwrap(); + + let cgroup_procs_path = format!( + "/sys/fs/cgroup/{}/{}/cgroup.procs", + expand_slice(TEST_SLICE).unwrap(), + unit + ); + for i in 0..5 { + let content = fs::read_to_string(&cgroup_procs_path); + if let Ok(content) = content { + assert!( + content.contains(&child1.id().to_string()), + "Cgroup procs does not contain the child1 process ID" + ); + break; + } + // Retry attempts exhausted, resulting in failure + if i == 4 { + content.unwrap(); + } + // Wait 500ms before next retrying + sleep(Duration::from_millis(500)); + } + + stop_cgroup(&cgroup); + child.wait().unwrap(); + child1.wait().unwrap(); + } +} diff --git a/src/systemd/utils.rs b/src/systemd/utils.rs index c654c59..0eec806 100644 --- a/src/systemd/utils.rs +++ b/src/systemd/utils.rs @@ -62,3 +62,40 @@ pub fn expand_slice(slice: &str) -> Result { Ok(slice_path) } + +#[cfg(test)] +mod tests { + use crate::systemd::utils::*; + + #[test] + fn test_is_slice_unit() { + assert!(is_slice_unit("test.slice")); + assert!(!is_slice_unit("test.scope")); + } + + #[test] + fn test_is_scope_unit() { + assert!(is_scope_unit("test.scope")); + assert!(!is_scope_unit("test.slice")); + } + + #[test] + fn test_expand_slice() { + assert_eq!(expand_slice("test.slice").unwrap(), "test.slice"); + assert_eq!( + expand_slice("test-1.slice").unwrap(), + "test.slice/test-1.slice" + ); + assert_eq!( + expand_slice("test-1-test-2.slice").unwrap(), + "test.slice/test-1.slice/test-1-test.slice/test-1-test-2.slice" + ); + assert_eq!( + expand_slice("slice-slice.slice").unwrap(), + "slice.slice/slice-slice.slice" + ); + assert_eq!(expand_slice("-.slice").unwrap(), ""); + assert!(expand_slice("invalid/slice").is_err()); + assert!(expand_slice("invalid-slice").is_err()); + } +} From c774e9e12305feb8c45bfa87b9a062e4fd276de4 Mon Sep 17 00:00:00 2001 From: Xuewei Niu Date: Fri, 11 Jul 2025 20:36:37 +0800 Subject: [PATCH 6/6] manager: Add unit test cases The tests cover the methods of `conv` mod, `FsManager`, and `SystemdManager`. Since we have to manipulate the cgroups during testing, the tests related to this part are set to be run in sequence. Signed-off-by: Xuewei Niu --- Cargo.toml | 1 + Makefile | 26 +++- src/manager/conv.rs | 44 ++++++ src/manager/fs.rs | 332 +++++++++++++++++++++++++++++++++++++++++ src/manager/mod.rs | 27 ++++ src/manager/systemd.rs | 241 ++++++++++++++++++++++++++++++ 6 files changed, 667 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ffba0e5..969f32a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ bit-vec = "0.6" [dev-dependencies] libc = "0.2.76" rand = "0.8" +nix = "0.25" [features] default = [] diff --git a/Makefile b/Makefile index 91a69d9..619b5f3 100644 --- a/Makefile +++ b/Makefile @@ -19,10 +19,14 @@ build: debug # Tests and linters # -.PHONY: test -test: test-systemd - cargo test -- --color always --nocapture \ - --skip systemd::dbus::client::tests +# Tests that manipulate cgroups should run in sequence, so that +# `--test-threads=1` is used. +test: test-systemd test-fs-manager test-systemd-manager + cargo test --all-features -- --color always \ + --nocapture \ + --skip systemd::dbus::client::tests \ + --skip manager::fs::tests \ + --skip manager::systemd::tests .PHONY: test-systemd # Tests that manipulate cgroups should run in sequence, so that @@ -33,6 +37,20 @@ test-systemd: --color always --nocapture \ --test-threads=1 +.PHONY: test-fs-manager +# See test-systemd +test-fs-manager: + cargo test --all-features --package cgroups-rs \ + --lib -- manager::fs::tests \ + --color always --nocapture --test-threads=1 + +.PHONY: test-systemd-manager +# See test-systemd +test-systemd-manager: + cargo test --all-features --package cgroups-rs \ + --lib -- manager::systemd::tests \ + --color always --nocapture --test-threads=1 + .PHONY: check check: fmt clippy diff --git a/src/manager/conv.rs b/src/manager/conv.rs index 168c40c..e2f21ea 100644 --- a/src/manager/conv.rs +++ b/src/manager/conv.rs @@ -67,3 +67,47 @@ pub(crate) fn memory_swap_to_cgroup_v2(memswap_limit: i64, mem_limit: i64) -> Re Ok(memswap_limit - mem_limit) } + +#[cfg(test)] +mod tests { + use crate::manager::conv::*; + + #[test] + fn test_cpu_shares_to_cgroup_v2() { + assert_eq!(cpu_shares_to_cgroup_v2(0), 0); + assert_eq!(cpu_shares_to_cgroup_v2(1), 1); + assert_eq!(cpu_shares_to_cgroup_v2(2), 1); + assert_eq!(cpu_shares_to_cgroup_v2(100), 4); + assert_eq!( + cpu_shares_to_cgroup_v2(CPU_SHARES_V1_MAX), + CPU_WEIGHT_V2_MAX + ); + assert_eq!( + cpu_shares_to_cgroup_v2(CPU_SHARES_V1_MAX - 1), + CPU_WEIGHT_V2_MAX - 1 + ); + assert_eq!(cpu_shares_to_cgroup_v2(u64::MAX), CPU_WEIGHT_V2_MAX); + } + + #[test] + fn test_memory_swap_to_cgroup_v2() { + // memory no limit and swap is 0, treat it as no limit + assert_eq!(memory_swap_to_cgroup_v2(0, -1).unwrap(), -1); + // -1 is "max", 0 is "unset", so treat as is + assert_eq!(memory_swap_to_cgroup_v2(-1, 0).unwrap(), -1); + assert_eq!(memory_swap_to_cgroup_v2(0, 0).unwrap(), 0); + + // Now swap cannot be 0 or -1 + + // Unlimited memory, so treat swap as is. + assert_eq!(memory_swap_to_cgroup_v2(100, -1).unwrap(), 100); + // Unset or unknown memory, can't calculate swap. + assert!(memory_swap_to_cgroup_v2(100, 0).is_err()); + // Does not make sense to subtract a negative value. + assert!(memory_swap_to_cgroup_v2(100, -2).is_err()); + // Swap + mem < mem + assert!(memory_swap_to_cgroup_v2(50, 100).is_err()); + // Real swap + assert_eq!(memory_swap_to_cgroup_v2(200, 100).unwrap(), 100); + } +} diff --git a/src/manager/fs.rs b/src/manager/fs.rs index b1316b2..f865594 100644 --- a/src/manager/fs.rs +++ b/src/manager/fs.rs @@ -1086,3 +1086,335 @@ impl BlkioStat { .collect() } } + +#[cfg(test)] +mod tests { + //! Tests for the `FsManager` implementation of the `Manager` trait. + //! + //! Don't run tests in parallel, use `--test-threads=1`! + //! + + use nix::sys::signal::{kill, Signal}; + use nix::unistd::Pid; + use oci_spec::runtime::{LinuxCpuBuilder, LinuxMemoryBuilder, LinuxResourcesBuilder}; + + use crate::manager::fs::*; + use crate::manager::tests::{MEMORY_1G, MEMORY_2G, MEMORY_512M}; + use crate::tests::spawn_sleep_inf; + use crate::{skip_if_cgroups_v1, skip_if_cgroups_v2}; + + const TEST_BASE: &str = "cgroupsrs/pod"; + + impl FsManager { + pub fn cgroup(&self) -> &Cgroup { + &self.cgroup + } + } + + fn clean_cgroups(path: &str) { + let dirs = path.split("/").fold(vec![], |mut acc, dir| { + if let Some(last) = acc.last() { + acc.push(format!("{}/{}", last, dir)); + } else { + acc.push(dir.to_string()); + } + acc + }); + + for dir in dirs.iter().rev() { + let paths = parse_cgroup_subsystems().unwrap(); + let mounts = parse_cgroup_mountinfo(&paths).unwrap(); + + if hierarchies::is_cgroup2_unified_mode() { + let full = join_path(UNIFIED_MOUNTPOINT, dir); + let path = Path::new(&full); + if path.exists() { + // kill processes in cgroup.procs + let processes = + fs::read_to_string(path.join("cgroup.procs")).unwrap_or_default(); + for pid in processes.lines() { + if let Ok(pid) = pid.parse() { + // kill the process + let _ = kill(Pid::from_raw(pid), Signal::SIGKILL); + } + } + fs::remove_dir(path).unwrap(); + } + } else { + for (subsystem, mountpoint) in mounts.iter() { + let full = join_path(mountpoint, paths.get(subsystem).unwrap()); + let full = join_path(&full, dir); + let path = Path::new(&full); + if path.exists() { + // kill processes in the cgroup, by going through + // `tasks` + let tasks = fs::read_to_string(path.join("tasks")).unwrap_or_default(); + for pid in tasks.lines() { + if let Ok(pid) = pid.parse() { + // kill the process + let _ = kill(Pid::from_raw(pid), Signal::SIGKILL); + } + } + fs::remove_dir(path).unwrap(); + } + } + } + } + } + + fn new_manager() -> FsManager { + clean_cgroups(TEST_BASE); + FsManager::new(TEST_BASE).unwrap() + } + + fn run_set_resources_failed(resources: LinuxResources) { + let mut child = spawn_sleep_inf(); + let mut manager = new_manager(); + manager + .add_proc(CgroupPid { + pid: child.id() as u64, + }) + .unwrap(); + assert!(manager.set(&resources).is_err()); + manager.destroy().unwrap(); + child.kill().unwrap(); + child.wait().unwrap(); + } + + fn run_set_resources(linux_resources: LinuxResources, test_fn: F) + where + F: FnOnce(&mut FsManager), + { + let mut child = spawn_sleep_inf(); + let mut manager = new_manager(); + manager + .add_proc(CgroupPid { + pid: child.id() as u64, + }) + .unwrap(); + manager.set(&linux_resources).unwrap(); + test_fn(&mut manager); + manager.destroy().unwrap(); + child.kill().unwrap(); + child.wait().unwrap(); + } + + #[test] + fn test_parse_value_from_tuples() { + let tuple_str = "system 100000\nuser 200000"; + assert_eq!( + parse_value_from_tuples::(tuple_str, "user"), + Some(200000) + ); + assert_eq!( + parse_value_from_tuples::(tuple_str, "system"), + Some(100000) + ); + assert_eq!(parse_value_from_tuples::(tuple_str, "user1"), None); + } + + #[test] + fn test_paths_and_mounts() { + let mut manager = new_manager(); + + for (subsystem, mountpoint) in manager.mounts() { + let subsys = if subsystem.is_empty() { + assert!(manager.v2()); + None + } else { + Some(subsystem.as_str()) + }; + let path = manager.cgroup_path(subsys).unwrap(); + let path = join_path(mountpoint, &path); + assert!(Path::new(&path).exists(), "Cgroup {} does not exist", path); + } + + manager.destroy().unwrap(); + } + + #[test] + fn test_destroy() { + let mut manager = new_manager(); + manager.create_cgroups().unwrap(); + + let cgroup_path = if manager.v2() { + manager.cgroup_path(None).unwrap() + } else { + manager.cgroup_path(Some("memory")).unwrap() + }; + assert!( + Path::new(&cgroup_path).exists(), + "Cgroup should exist before destroy" + ); + + manager.destroy().unwrap(); + assert!( + !Path::new(&cgroup_path).exists(), + "Cgroup should not exist after destroy" + ); + } + + #[test] + fn test_set_cpu() { + // 1024 shares, every 100ms allows to use 1 CPU + let linux_cpu = LinuxCpuBuilder::default() + .shares(1024u64) + .quota(100000i64) + .period(100000u64) + .quota(100000i64) + .build() + .unwrap(); + + let linux_resources = LinuxResourcesBuilder::default() + .cpu(linux_cpu) + .build() + .unwrap(); + + run_set_resources(linux_resources, |manager| { + let controller: &CpuController = manager.controller().unwrap(); + let shares = controller.shares().unwrap(); + let period = controller.cfs_period().unwrap(); + let quota = controller.cfs_quota().unwrap(); + + if manager.v2() { + assert_eq!(shares, conv::cpu_shares_to_cgroup_v2(1024)); + } else { + assert_eq!(shares, 1024); + } + + assert_eq!(period, 100000); + assert_eq!(quota, 100000); + }) + } + + #[test] + fn test_set_memory_v2() { + skip_if_cgroups_v1!(); + + // expected failure: swap < limit + let linux_memory = LinuxMemoryBuilder::default() + .limit(MEMORY_1G) + .swap(MEMORY_512M) + .build() + .unwrap(); + let linux_resources = LinuxResourcesBuilder::default() + .memory(linux_memory) + .build() + .unwrap(); + run_set_resources_failed(linux_resources); + + let linux_memory = LinuxMemoryBuilder::default() + .limit(MEMORY_512M) + .swap(MEMORY_1G) + .reservation(MEMORY_2G) + .build() + .unwrap(); + let linux_resources = LinuxResourcesBuilder::default() + .memory(linux_memory) + .build() + .unwrap(); + run_set_resources(linux_resources, |manager| { + let controller: &MemController = manager.controller().unwrap(); + let memory_stat = controller.memory_stat(); + let memory_swap_stat = controller.memswap(); + + assert_eq!(memory_stat.limit_in_bytes, MEMORY_512M); + assert_eq!(memory_swap_stat.limit_in_bytes, MEMORY_512M); + assert_eq!(memory_stat.soft_limit_in_bytes, MEMORY_2G); + }); + } + + #[test] + fn test_set_memory_v1() { + skip_if_cgroups_v2!(); + + let linux_memory = LinuxMemoryBuilder::default() + .limit(MEMORY_512M) + .swap(MEMORY_512M) + .reservation(MEMORY_512M) + .disable_oom_killer(true) + .swappiness(50u64) + .build() + .unwrap(); + let linux_resources = LinuxResourcesBuilder::default() + .memory(linux_memory) + .build() + .unwrap(); + run_set_resources(linux_resources, |manager| { + let controller: &MemController = manager.controller().unwrap(); + let memory_stat = controller.memory_stat(); + let memory_swap_stat = controller.memswap(); + + assert_eq!(memory_stat.limit_in_bytes, MEMORY_512M); + assert_eq!(memory_swap_stat.limit_in_bytes, MEMORY_512M); + assert_eq!(memory_stat.soft_limit_in_bytes, MEMORY_512M); + assert_eq!(memory_stat.swappiness, 50); + assert!(memory_stat.oom_control.oom_kill_disable); + }); + + // expected failure: swapiness too high + let linux_memory = LinuxMemoryBuilder::default() + .swappiness(101u64) + .build() + .unwrap(); + let linux_resources = LinuxResourcesBuilder::default() + .memory(linux_memory) + .build() + .unwrap(); + run_set_resources_failed(linux_resources); + } + + fn parse_cpu_list(online_str: &str) -> Vec { + let mut cpus = Vec::new(); + for part in online_str.trim().split(',') { + if let Some((start, end)) = part.split_once('-') { + let start: u32 = start.parse().unwrap(); + let end: u32 = end.parse().unwrap(); + cpus.extend(start..=end); + } else { + cpus.push(part.parse().unwrap()); + } + } + cpus + } + + #[test] + fn test_enable_cpus_topdown() { + let cpuset_cpus_path = format!("/sys/fs/cgroup/{}/cpuset.cpus", TEST_BASE); + let online_cpus = fs::read_to_string("/sys/devices/system/cpu/online").unwrap(); + let cpus = parse_cpu_list(&online_cpus); + + // Skip this test if there are less than 2 CPUs online + if cpus.len() < 2 { + return; + } + + let linux_cpu = LinuxCpuBuilder::default() + .cpus(format!("{}", cpus[0])) + .build() + .unwrap(); + let linux_resources = LinuxResourcesBuilder::default() + .cpu(linux_cpu) + .build() + .unwrap(); + run_set_resources(linux_resources, |manager| { + let cpus1 = fs::read_to_string(&cpuset_cpus_path).unwrap(); + let cpus1 = parse_cpu_list(&cpus1); + assert_eq!(cpus[..1], cpus1); + + manager + .enable_cpus_topdown(&format!("{},{}", cpus[0], cpus[1])) + .unwrap(); + let cpuset_cpus = fs::read_to_string(&cpuset_cpus_path).unwrap(); + let cpus2 = parse_cpu_list(&cpuset_cpus); + assert_eq!(cpus[..2], cpus2); + }); + } + + #[test] + fn test_systemd() { + let mut manager = new_manager(); + assert!(!manager.systemd(), "FsManager should not be systemd"); + manager.destroy().unwrap(); + } +} diff --git a/src/manager/mod.rs b/src/manager/mod.rs index 9dd54be..d7a0f9e 100644 --- a/src/manager/mod.rs +++ b/src/manager/mod.rs @@ -84,3 +84,30 @@ pub trait Manager: Send + Sync { /// Indicate whether the cgroup manager is using cgroup v2. fn v2(&self) -> bool; } + +#[cfg(test)] +mod tests { + pub const MEMORY_512M: i64 = 512 * 1024 * 1024; // 512 MiB + pub const MEMORY_1G: i64 = 1024 * 1024 * 1024; // 1 GiB + pub const MEMORY_2G: i64 = 2 * 1024 * 1024 * 1024; // 2 GiB + + #[macro_export] + macro_rules! skip_if_cgroups_v1 { + () => { + if !$crate::fs::hierarchies::is_cgroup2_unified_mode() { + eprintln!("Skipping test in cgroups v1 mode"); + return; + } + }; + } + + #[macro_export] + macro_rules! skip_if_cgroups_v2 { + () => { + if $crate::fs::hierarchies::is_cgroup2_unified_mode() { + eprintln!("Skipping test in cgroups v2 mode"); + return; + } + }; + } +} diff --git a/src/manager/systemd.rs b/src/manager/systemd.rs index 55557fd..ffb0140 100644 --- a/src/manager/systemd.rs +++ b/src/manager/systemd.rs @@ -339,3 +339,244 @@ fn ms_to_us(ms: u64) -> u64 { fn s_to_us(s: u64) -> u64 { s * 1_000_000 } + +#[cfg(test)] +mod tests { + //! Tests for the `SystemdManager` implementation of the `Manager` + //! trait. + //! + //! Don't run tests in parallel, use `--test-threads=1`! + //! + + use std::path::Path; + use std::thread::sleep; + use std::time::Duration; + + use oci_spec::runtime::{LinuxCpuBuilder, LinuxMemoryBuilder, LinuxResourcesBuilder}; + use rand::distributions::Alphanumeric; + use rand::Rng; + + use crate::fs::cpu::CpuController; + use crate::fs::memory::MemController; + use crate::fs::{ControllIdentifier, Controller, Subsystem}; + use crate::manager::systemd::*; + use crate::manager::tests::{MEMORY_1G, MEMORY_2G, MEMORY_512M}; + use crate::tests::spawn_sleep_inf; + use crate::{skip_if_cgroups_v1, skip_if_cgroups_v2, skip_if_no_systemd}; + + fn new_cgroups_path() -> (String, String, String) { + let rand_string: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(5) + .map(char::from) + .collect(); + ( + "cgroupsrs-test.slice".to_string(), + "cri".to_string(), + format!("pod{}", rand_string), + ) + } + + fn new_systemd_manager<'a>() -> SystemdManager<'a> { + let (slice, scope_prefix, name) = new_cgroups_path(); + SystemdManager::new(&format!("{}:{}:{}", slice, scope_prefix, name)).unwrap() + } + + fn run_set_resources_failed(resources: LinuxResources) { + let mut child = spawn_sleep_inf(); + let mut manager = new_systemd_manager(); + manager + .add_proc(CgroupPid { + pid: child.id() as u64, + }) + .unwrap(); + assert!(manager.set(&resources).is_err()); + manager.destroy().unwrap(); + child.wait().unwrap(); + } + + fn run_set_resources(linux_resources: LinuxResources, test_fn: F) + where + F: FnOnce(&mut SystemdManager), + { + let mut manager = new_systemd_manager(); + let mut child = spawn_sleep_inf(); + + manager + .add_proc(CgroupPid { + pid: child.id() as u64, + }) + .unwrap(); + manager.set(&linux_resources).unwrap(); + + test_fn(&mut manager); + + manager.destroy().unwrap(); + child.wait().unwrap(); + } + + #[test] + fn test_new_unit_name() { + assert_eq!(new_unit_name("test", "unit"), "test-unit.scope"); + assert_eq!(new_unit_name("test", "unit.slice"), "unit.slice"); + assert_eq!(new_unit_name("", "unit"), "unit.scope"); + assert_eq!(new_unit_name("", "unit.slice"), "unit.slice"); + assert_eq!(new_unit_name("prefix", "unit"), "prefix-unit.scope"); + } + + #[test] + fn test_slice_and_unit() { + skip_if_no_systemd!(); + + let (slice, scope_prefix, name) = new_cgroups_path(); + let manager = SystemdManager::new(&format!("{}:{}:{}", slice, scope_prefix, name)).unwrap(); + + assert_eq!(manager.slice(), "cgroupsrs-test.slice"); + assert_eq!(manager.unit(), format!("{scope_prefix}-{name}.scope")); + } + + #[test] + fn test_destory() { + skip_if_no_systemd!(); + + let (slice, scope_prefix, name) = new_cgroups_path(); + let mut manager = + SystemdManager::new(&format!("{}:{}:{}", slice, scope_prefix, name)).unwrap(); + + let cgroup_path = manager.cgroup_path(Some("memory")).unwrap(); + // Before starting the unit, no cgroup should exist. + assert!(!Path::new(&cgroup_path).exists()); + + let mut child = spawn_sleep_inf(); + manager + .add_proc(CgroupPid { + pid: child.id() as u64, + }) + .unwrap(); + + // Now cgroup should exist. + assert!(Path::new(&cgroup_path).exists()); + + manager.destroy().unwrap(); + + // This process should be killed. + child.wait().unwrap(); + + // No cgroup should exist after destroy, retry 5 times at 1-second + // intervals. + for _ in 0..5 { + if !Path::new(&cgroup_path).exists() { + break; + } + sleep(Duration::from_secs(1)); + } + assert!(!Path::new(&cgroup_path).exists()); + // Unit should be stopped. + assert!(!manager.systemd_client.exists()); + } + + fn controller<'a, T>(fs_manager: &'a FsManager) -> &'a T + where + &'a T: From<&'a Subsystem>, + T: Controller + ControllIdentifier, + { + let controller: &T = fs_manager.cgroup().controller_of().unwrap(); + + controller + } + + #[test] + fn test_set_cpu() { + skip_if_no_systemd!(); + + // 1024 shares, every 100ms allows to use 1 CPU + let linux_cpu = LinuxCpuBuilder::default() + .shares(1024u64) + .quota(100000i64) + .period(100000u64) + .quota(100000i64) + .build() + .unwrap(); + + let linux_resources = LinuxResourcesBuilder::default() + .cpu(linux_cpu) + .build() + .unwrap(); + + run_set_resources(linux_resources, |manager| { + let controller: &CpuController = controller(&manager.fs_manager); + let shares = controller.shares().unwrap(); + let period = controller.cfs_period().unwrap(); + let quota = controller.cfs_quota().unwrap(); + + if manager.v2() { + assert_eq!(shares, conv::cpu_shares_to_cgroup_v2(1024)); + } else { + assert_eq!(shares, 1024); + } + + assert_eq!(period, 100000); + assert_eq!(quota, 100000); + }) + } + + #[test] + fn test_set_memory_v2() { + skip_if_no_systemd!(); + skip_if_cgroups_v1!(); + + // Expected failure: swap < limit + let linux_memory = LinuxMemoryBuilder::default() + .limit(MEMORY_1G) + .swap(MEMORY_512M) + .build() + .unwrap(); + let linux_resources = LinuxResourcesBuilder::default() + .memory(linux_memory) + .build() + .unwrap(); + run_set_resources_failed(linux_resources); + + // Expected success + let linux_memory = LinuxMemoryBuilder::default() + .limit(MEMORY_512M) + .swap(MEMORY_1G) + .reservation(MEMORY_2G) + .build() + .unwrap(); + let linux_resources = LinuxResourcesBuilder::default() + .memory(linux_memory) + .build() + .unwrap(); + run_set_resources(linux_resources, |manager| { + let controller: &MemController = controller(&manager.fs_manager); + let memory_stat = controller.memory_stat(); + let memory_swap_stat = controller.memswap(); + + assert_eq!(memory_stat.limit_in_bytes, MEMORY_512M); + assert_eq!(memory_swap_stat.limit_in_bytes, MEMORY_512M); + assert_eq!(memory_stat.soft_limit_in_bytes, MEMORY_2G); + }); + } + + #[test] + fn test_set_memory_v1() { + skip_if_no_systemd!(); + skip_if_cgroups_v2!(); + + // Expected success + let linux_memory = LinuxMemoryBuilder::default() + .limit(MEMORY_512M) + .build() + .unwrap(); + let linux_resources = LinuxResourcesBuilder::default() + .memory(linux_memory) + .build() + .unwrap(); + run_set_resources(linux_resources, |manager| { + let controller: &MemController = controller(&manager.fs_manager); + let memory_stat = controller.memory_stat(); + assert_eq!(memory_stat.limit_in_bytes, MEMORY_512M); + }); + } +}