fix(dashboard): correct container-load cgroup path (always-empty LOAD tab)

The dashboard LOAD tab always showed "no running agent containers":
container_stats::gather() looked for each agent's cgroup at
machine.slice/machine-h\x2d<name>.scope, but that path never exists.

nixos-container runs `systemd-nspawn --keep-unit` with
`Slice = "machine.slice"` (nixpkgs virtualisation/nixos-containers.nix),
so --keep-unit means no separate machined `machine-<name>.scope` is
created — the container's cgroup IS the launching service unit,
`container@<machine>.service`, under machine.slice. systemd-machined
still logs "New machine <name>" (registration), which is what made the
scope-path assumption look plausible, but the cgroup stays on the unit.

Fix scope_dir to machine.slice/container@<machine>.service and drop the
\x2d escaping (the service unit name is used verbatim in the cgroup dir;
the journal shows the literal `container@h-<agent>.service`). Adds a unit
test pinning the path.
This commit is contained in:
atlas 2026-06-10 22:22:59 +02:00 committed by mara
commit 6c3a83ffa1

View file

@ -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@<machine>.service` — not a machined
//! `machine-<name>.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<u64>,
}
/// systemd escapes the machine name in the cgroup scope dir
/// (`machine-<escaped>.scope`). For our machine names — `h-<agent>`,
/// 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-<name>.scope` — the
/// container's cgroup *is* the launching service unit,
/// `container@<machine>.service`, placed under `machine.slice`.
/// systemd-machined still logs "New machine <name>" (registration), but the
/// cgroup stays on the service unit. So the path is
/// `machine.slice/container@<machine>.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-<agent>.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<ContainerResource> {
}
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@<machine>.service` (literal name, no `\x2d`),
// under machine.slice — NOT a machined `machine-<name>.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")
);
}
}