mirror of
https://github.com/kata-containers/cgroups-rs.git
synced 2026-08-05 02:13:23 +00:00
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 <niuxuewei.nxw@antgroup.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
68
src/systemd/consts.rs
Normal file
68
src/systemd/consts.rs
Normal file
@@ -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";
|
||||
35
src/systemd/cpu.rs
Normal file
35
src/systemd/cpu.rs
Normal file
@@ -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))
|
||||
}
|
||||
106
src/systemd/cpuset.rs
Normal file
106
src/systemd/cpuset.rs
Normal file
@@ -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<u8>)> {
|
||||
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<u8>)> {
|
||||
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<u8>`, 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<Vec<u8>> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
17
src/systemd/dbus/README.md
Normal file
17
src/systemd/dbus/README.md
Normal file
@@ -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
|
||||
```
|
||||
192
src/systemd/dbus/client.rs
Normal file
192
src/systemd/dbus/client.rs
Normal file
@@ -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<Property<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> SystemdClient<'a> {
|
||||
pub fn new(unit: &str, props: Vec<Property<'a>>) -> Result<Self> {
|
||||
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<usize> {
|
||||
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::<usize>().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<T>(result: ZbusResult<T>) -> ZbusResult<bool> {
|
||||
if let Err(ZbusError::MethodError(err_name, _, _)) = &result {
|
||||
if err_name.as_str() == NO_SUCH_UNIT {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
result.map(|_| false)
|
||||
}
|
||||
18
src/systemd/dbus/error.rs
Normal file
18
src/systemd/dbus/error.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2025 Ant Group
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[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),
|
||||
}
|
||||
18
src/systemd/dbus/mod.rs
Normal file
18
src/systemd/dbus/mod.rs
Normal file
@@ -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;
|
||||
16
src/systemd/dbus/proxy.rs
Normal file
16
src/systemd/dbus/proxy.rs
Normal file
@@ -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<SystemManager<'a>> {
|
||||
let connection = Connection::system()?;
|
||||
let proxy = SystemManager::new(&connection)?;
|
||||
|
||||
Ok(proxy)
|
||||
}
|
||||
1011
src/systemd/dbus/systemd_manager_proxy.rs
Normal file
1011
src/systemd/dbus/systemd_manager_proxy.rs
Normal file
File diff suppressed because it is too large
Load Diff
18
src/systemd/error.rs
Normal file
18
src/systemd/error.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2025 Ant Group
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0 or MIT
|
||||
//
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[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,
|
||||
}
|
||||
32
src/systemd/memory.rs
Normal file
32
src/systemd/memory.rs
Normal file
@@ -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))
|
||||
}
|
||||
25
src/systemd/mod.rs
Normal file
25
src/systemd/mod.rs
Normal file
@@ -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;
|
||||
11
src/systemd/pids.rs
Normal file
11
src/systemd/pids.rs
Normal file
@@ -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))
|
||||
}
|
||||
170
src/systemd/props.rs
Normal file
170
src/systemd/props.rs
Normal file
@@ -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<bool>,
|
||||
// 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<bool>,
|
||||
task_accounting: Option<bool>,
|
||||
// Use IO_ACCOUNTING for cgroup v2 and BLOCK_IO_ACCOUNTING for cgroup v1.
|
||||
io_accounting: Option<bool>,
|
||||
default_dependencies: Option<bool>,
|
||||
description: Option<String>,
|
||||
wants: Option<String>,
|
||||
slice: Option<String>,
|
||||
delegate: Option<bool>,
|
||||
pids: Option<Vec<u32>>,
|
||||
timeout_stop_usec: Option<u64>,
|
||||
}
|
||||
|
||||
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<u32>) -> 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<Property<'static>> {
|
||||
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
|
||||
}
|
||||
}
|
||||
64
src/systemd/utils.rs
Normal file
64
src/systemd/utils.rs
Normal file
@@ -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<String> {
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user