use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SliceOwner {
Pact,
Workload,
}
pub mod slices {
pub const PACT_ROOT: &str = "pact.slice";
pub const PACT_INFRA: &str = "pact.slice/infra.slice";
pub const PACT_NETWORK: &str = "pact.slice/network.slice";
pub const PACT_GPU: &str = "pact.slice/gpu.slice";
pub const PACT_AUDIT: &str = "pact.slice/audit.slice";
pub const WORKLOAD_ROOT: &str = "workload.slice";
}
#[must_use]
pub fn slice_owner(path: &str) -> Option<SliceOwner> {
if path.starts_with(slices::PACT_ROOT) {
Some(SliceOwner::Pact)
} else if path.starts_with(slices::WORKLOAD_ROOT) {
Some(SliceOwner::Workload)
} else {
None
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ResourceLimits {
pub memory_max: Option<u64>,
pub cpu_weight: Option<u16>,
pub io_max: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct CgroupHandle {
pub path: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CgroupMetrics {
pub memory_current: u64,
pub memory_max: Option<u64>,
pub cpu_usage_usec: u64,
pub nr_processes: u32,
}
pub trait CgroupManager: Send + Sync {
fn create_hierarchy(&self) -> Result<(), CgroupError>;
fn create_scope(
&self,
parent_slice: &str,
name: &str,
limits: &ResourceLimits,
) -> Result<CgroupHandle, CgroupError>;
fn destroy_scope(&self, handle: &CgroupHandle) -> Result<(), CgroupError>;
fn read_metrics(&self, path: &str) -> Result<CgroupMetrics, CgroupError>;
fn is_scope_empty(&self, handle: &CgroupHandle) -> Result<bool, CgroupError>;
}
#[derive(Debug, thiserror::Error)]
pub enum CgroupError {
#[error("cgroup creation failed: {reason}")]
CreationFailed { reason: String },
#[error("cgroup.kill failed for {path}: {reason}")]
KillFailed { path: String, reason: String },
#[error("cgroup path not found: {path}")]
NotFound { path: String },
#[error("permission denied: {path} owned by {owner:?}")]
PermissionDenied { path: String, owner: SliceOwner },
#[error("cgroup I/O error: {0}")]
Io(#[from] std::io::Error),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slice_owner_pact() {
assert_eq!(slice_owner(slices::PACT_ROOT), Some(SliceOwner::Pact));
assert_eq!(slice_owner(slices::PACT_INFRA), Some(SliceOwner::Pact));
assert_eq!(slice_owner(slices::PACT_GPU), Some(SliceOwner::Pact));
assert_eq!(slice_owner(slices::PACT_NETWORK), Some(SliceOwner::Pact));
assert_eq!(slice_owner(slices::PACT_AUDIT), Some(SliceOwner::Pact));
}
#[test]
fn slice_owner_workload() {
assert_eq!(
slice_owner(slices::WORKLOAD_ROOT),
Some(SliceOwner::Workload)
);
assert_eq!(
slice_owner("workload.slice/alloc-42"),
Some(SliceOwner::Workload)
);
}
#[test]
fn slice_owner_unknown() {
assert_eq!(slice_owner("system.slice"), None);
assert_eq!(slice_owner(""), None);
assert_eq!(slice_owner("/sys/fs/cgroup"), None);
}
#[test]
fn resource_limits_default() {
let limits = ResourceLimits::default();
assert!(limits.memory_max.is_none());
assert!(limits.cpu_weight.is_none());
assert!(limits.io_max.is_none());
}
#[test]
fn slice_owner_nested_paths() {
assert_eq!(
slice_owner("pact.slice/infra.slice/chronyd.scope"),
Some(SliceOwner::Pact)
);
assert_eq!(
slice_owner("workload.slice/alloc-42/task-1.scope"),
Some(SliceOwner::Workload)
);
}
#[test]
fn slice_owner_substring_not_matched() {
assert_eq!(slice_owner("not-pact.slice/foo"), None);
assert_eq!(
slice_owner("workload.slice-extra"),
Some(SliceOwner::Workload)
);
}
#[test]
fn slice_owner_serialization() {
let owner = SliceOwner::Pact;
let json = serde_json::to_string(&owner).unwrap();
let deser: SliceOwner = serde_json::from_str(&json).unwrap();
assert_eq!(deser, SliceOwner::Pact);
let owner = SliceOwner::Workload;
let json = serde_json::to_string(&owner).unwrap();
let deser: SliceOwner = serde_json::from_str(&json).unwrap();
assert_eq!(deser, SliceOwner::Workload);
}
#[test]
fn resource_limits_with_values() {
let limits = ResourceLimits {
memory_max: Some(512 * 1024 * 1024), cpu_weight: Some(200),
io_max: Some(100_000_000),
};
assert_eq!(limits.memory_max, Some(536_870_912));
assert_eq!(limits.cpu_weight, Some(200));
assert_eq!(limits.io_max, Some(100_000_000));
}
#[test]
fn resource_limits_serialization_roundtrip() {
let limits = ResourceLimits {
memory_max: Some(1024),
cpu_weight: Some(500),
io_max: None,
};
let json = serde_json::to_string(&limits).unwrap();
let deser: ResourceLimits = serde_json::from_str(&json).unwrap();
assert_eq!(deser.memory_max, Some(1024));
assert_eq!(deser.cpu_weight, Some(500));
assert!(deser.io_max.is_none());
}
#[test]
fn cgroup_handle_path() {
let handle = CgroupHandle {
path: "/sys/fs/cgroup/pact.slice/gpu.slice/nvidia-persistenced".to_string(),
};
assert!(handle.path.contains("pact.slice"));
}
#[test]
fn cgroup_metrics_default() {
let metrics = CgroupMetrics::default();
assert_eq!(metrics.memory_current, 0);
assert!(metrics.memory_max.is_none());
assert_eq!(metrics.cpu_usage_usec, 0);
assert_eq!(metrics.nr_processes, 0);
}
#[test]
fn cgroup_error_display() {
let err = CgroupError::CreationFailed {
reason: "no space".to_string(),
};
assert_eq!(err.to_string(), "cgroup creation failed: no space");
let err = CgroupError::KillFailed {
path: "/sys/fs/cgroup/test".to_string(),
reason: "D-state".to_string(),
};
assert!(err.to_string().contains("D-state"));
let err = CgroupError::PermissionDenied {
path: "workload.slice".to_string(),
owner: SliceOwner::Workload,
};
assert!(err.to_string().contains("Workload"));
}
struct MockCgroupManager;
impl CgroupManager for MockCgroupManager {
fn create_hierarchy(&self) -> Result<(), CgroupError> {
Ok(())
}
fn create_scope(
&self,
parent_slice: &str,
name: &str,
_limits: &ResourceLimits,
) -> Result<CgroupHandle, CgroupError> {
Ok(CgroupHandle {
path: format!("{parent_slice}/{name}.scope"),
})
}
fn destroy_scope(&self, _handle: &CgroupHandle) -> Result<(), CgroupError> {
Ok(())
}
fn read_metrics(&self, _path: &str) -> Result<CgroupMetrics, CgroupError> {
Ok(CgroupMetrics::default())
}
fn is_scope_empty(&self, _handle: &CgroupHandle) -> Result<bool, CgroupError> {
Ok(true)
}
}
#[test]
fn mock_cgroup_manager_lifecycle() {
let mgr = MockCgroupManager;
mgr.create_hierarchy().unwrap();
let handle = mgr
.create_scope(
slices::PACT_GPU,
"nvidia-persistenced",
&ResourceLimits::default(),
)
.unwrap();
assert_eq!(
handle.path,
"pact.slice/gpu.slice/nvidia-persistenced.scope"
);
assert!(mgr.is_scope_empty(&handle).unwrap());
let metrics = mgr.read_metrics(&handle.path).unwrap();
assert_eq!(metrics.nr_processes, 0);
mgr.destroy_scope(&handle).unwrap();
}
#[test]
fn mock_cgroup_manager_permission_denied() {
struct StrictMockCgroupManager;
impl CgroupManager for StrictMockCgroupManager {
fn create_hierarchy(&self) -> Result<(), CgroupError> {
Ok(())
}
fn create_scope(
&self,
parent_slice: &str,
_name: &str,
_limits: &ResourceLimits,
) -> Result<CgroupHandle, CgroupError> {
if let Some(owner) = slice_owner(parent_slice) {
if owner != SliceOwner::Pact {
return Err(CgroupError::PermissionDenied {
path: parent_slice.to_string(),
owner,
});
}
}
Ok(CgroupHandle {
path: format!("{parent_slice}/test.scope"),
})
}
fn destroy_scope(&self, _handle: &CgroupHandle) -> Result<(), CgroupError> {
Ok(())
}
fn read_metrics(&self, _path: &str) -> Result<CgroupMetrics, CgroupError> {
Ok(CgroupMetrics::default())
}
fn is_scope_empty(&self, _handle: &CgroupHandle) -> Result<bool, CgroupError> {
Ok(true)
}
}
let mgr = StrictMockCgroupManager;
assert!(mgr
.create_scope(slices::PACT_INFRA, "test", &ResourceLimits::default())
.is_ok());
let err = mgr
.create_scope(slices::WORKLOAD_ROOT, "test", &ResourceLimits::default())
.unwrap_err();
assert!(matches!(err, CgroupError::PermissionDenied { .. }));
}
}