diff --git a/hive-c0re/src/container_stats.rs b/hive-c0re/src/container_stats.rs index bd39097e..0e95bd9d 100644 --- a/hive-c0re/src/container_stats.rs +++ b/hive-c0re/src/container_stats.rs @@ -3,7 +3,11 @@ //! machine directly — same privsep-clean posture as the turn-stats //! sqlite reads (`cpu.stat` / `memory.*` are world-readable; no //! `hive-priv` needed). Runs host-side in hive-c0re, where -//! `/sys/fs/cgroup/machine.slice/` holds the nspawn machine scopes. +//! `/sys/fs/cgroup/machine.slice/` holds the per-container cgroups. Because +//! nixos-container runs `systemd-nspawn --keep-unit` (with +//! `Slice = "machine.slice"`), each container's cgroup is its launching +//! service unit `container@.service` — not a machined +//! `machine-.scope`. See [`scope_dir`]. //! //! No network: agents share the host network namespace //! (`privateNetwork = false`), so there is no per-container net @@ -47,16 +51,22 @@ pub struct ContainerResource { pub mem_max_bytes: Option, } -/// systemd escapes the machine name in the cgroup scope dir -/// (`machine-.scope`). For our machine names — `h-`, -/// agent ∈ `[a-z0-9_-]` — the only character systemd escapes is `-`, -/// which becomes `\x2d` (verified against `systemd-escape`). -fn escape_machine(machine: &str) -> String { - machine.replace('-', "\\x2d") -} - +/// Cgroup directory for a container's machine. +/// +/// nixos-container runs `systemd-nspawn --keep-unit` with +/// `Slice = "machine.slice"` (see nixpkgs +/// `virtualisation/nixos-containers.nix`). `--keep-unit` means nspawn does +/// **not** create a separate machined `machine-.scope` — the +/// container's cgroup *is* the launching service unit, +/// `container@.service`, placed under `machine.slice`. +/// systemd-machined still logs "New machine " (registration), but the +/// cgroup stays on the service unit. So the path is +/// `machine.slice/container@.service`, and the service unit name is +/// used verbatim — no `\x2d` escaping (that only applies when a string is +/// converted *into* a scope/slice unit name, not to an already-formed +/// instance unit; the journal shows the literal `container@h-.service`). fn scope_dir(machine: &str) -> PathBuf { - PathBuf::from(MACHINE_SLICE).join(format!("machine-{}.scope", escape_machine(machine))) + PathBuf::from(MACHINE_SLICE).join(format!("container@{machine}.service")) } /// Read a single unsigned integer from a one-line cgroup file. @@ -175,3 +185,24 @@ pub async fn gather() -> Vec { } out } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn scope_dir_is_the_keep_unit_service_under_machine_slice() { + // nixos-container `--keep-unit` keeps the cgroup on the service + // unit `container@.service` (literal name, no `\x2d`), + // under machine.slice — NOT a machined `machine-.scope`. + assert_eq!( + scope_dir("h-atlas"), + PathBuf::from("/sys/fs/cgroup/machine.slice/container@h-atlas.service") + ); + // Underscores in agent names are likewise verbatim. + assert_eq!( + scope_dir("h-foo_bar"), + PathBuf::from("/sys/fs/cgroup/machine.slice/container@h-foo_bar.service") + ); + } +}