nixos/beszel.agent: expose GPU_COLLECTOR and refactor around it

Describe the supported GPU collectors in a single table and derive the
service path, capabilities, syscall filter and device access from it.

PrivateDevices is only relaxed when a selected collector actually needs
device nodes, and the nodes are listed in DeviceAllow so /dev stays an
allow-list, following the same pattern as ollama and the smartctl
exporter. PrivateUsers only has to go when a collector needs
capabilities, which are void on the host inside a user namespace; plain
device access (nvidia-smi) keeps it. When smartmon relies on full /dev
access no DeviceAllow is emitted at all, since any entry would turn
DevicePolicy=auto into an allow-list and lock the disks out.

amdgpu now defaults to amd_sysfs and intel to intel_sysfs (new in
beszel 0.20.0). Both read sysfs directly and need neither a package nor
/dev access; rocm-smi is deprecated upstream and intel_gpu_top is not
used on the xe driver. An
empty GPU_COLLECTOR is not exported, keeping upstream auto-detection.

GPU_COLLECTOR still accepts upstream's comma-separated string, which is
how it had to be set through the freeform environment until now.
This commit is contained in:
Mirza Arnaut
2026-04-09 20:06:51 +02:00
parent cefee7ffe5
commit 6c9cfec111

View File

@@ -6,6 +6,76 @@
}:
let
cfg = config.services.beszel.agent;
hasVideoDriver = driver: builtins.elem driver config.services.xserver.videoDrivers;
# Collector names must match `isValidCollectorSource` in upstream's agent/gpu.go.
# macmon and powermetrics are macOS-only and omitted here.
gpuCollectors = {
# read sysfs directly, need no package or device access
"amd_sysfs" = { };
"intel_sysfs" = { };
"intel_gpu_top" = {
package = lib.getBin pkgs.intel-gpu-tools;
deviceAllow = [ "char-drm rw" ];
capabilities = [ "CAP_PERFMON" ];
# perf_event_open is in @debug, not @system-service
systemCalls = [ "perf_event_open" ];
};
"nvidia-smi" = {
package = lib.getBin config.hardware.nvidia.package;
deviceAllow = [ "char-nvidia* rw" ];
};
"nvml" = {
deviceAllow = [ "char-nvidia* rw" ];
};
"nvtop" = {
package = lib.getBin pkgs.nvtopPackages.full;
deviceAllow = [
"char-nvidia* rw"
"char-drm rw"
];
};
"rocm-smi" = {
package = lib.getBin pkgs.rocmPackages.rocm-smi;
deviceAllow = [
"char-drm rw"
"char-kfd rw"
];
};
};
activeCollectors = lib.optionals (!cfg.environment.SKIP_GPU) cfg.environment.GPU_COLLECTOR;
collectorAttrs =
attr: lib.unique (lib.concatMap (name: gpuCollectors.${name}.${attr} or [ ]) activeCollectors);
gpuPackages = map (name: gpuCollectors.${name}.package) (
lib.filter (name: gpuCollectors.${name} ? package) activeCollectors
);
gpuNeedsDevices = collectorAttrs "deviceAllow" != [ ];
# capabilities granted under PrivateUsers are void on the host, see
# systemd.exec(5), so these collectors also need the user namespace disabled
gpuNeedsCapabilities = collectorAttrs "capabilities" != [ ];
# Any explicit DeviceAllow turns DevicePolicy=auto into an allow-list, so the GPU
# devices are omitted when smartmon relies on full /dev access.
deviceAllowList =
lib.optionals (cfg.smartmon.enable && cfg.smartmon.deviceAllow != [ ]) (
map (device: "${device} r") cfg.smartmon.deviceAllow
)
++ lib.optionals (!cfg.smartmon.enable || cfg.smartmon.deviceAllow != [ ]) (
collectorAttrs "deviceAllow"
);
serviceCapabilities =
lib.optionals cfg.smartmon.enable [
"CAP_SYS_RAWIO"
"CAP_SYS_ADMIN"
]
++ collectorAttrs "capabilities";
in
{
meta.maintainers = with lib.maintainers; [
@@ -68,6 +138,37 @@ in
Enabling this option will skip GPU tracking.
'';
};
GPU_COLLECTOR = lib.mkOption {
# upstream takes a comma-separated string, which used to be passed through as is
type =
with lib.types;
coercedTo str (value: map lib.trim (lib.splitString "," value)) (
listOf (enum (lib.attrNames gpuCollectors))
);
default =
lib.optionals (hasVideoDriver "nvidia") [ "nvidia-smi" ]
++ lib.optionals (hasVideoDriver "amdgpu") [ "amd_sysfs" ]
++ lib.optionals (hasVideoDriver "intel") [ "intel_sysfs" ];
defaultText = lib.literalMD ''
derived from {option}`services.xserver.videoDrivers`
'';
example = [
"nvidia-smi"
"intel_gpu_top"
];
description = ''
GPU collectors to use, in priority order. Overrides the agent's
auto-detection; the packages needed by the selected collectors are added
to the service path. If empty, the agent auto-detects available
collectors. `rocm-smi` is deprecated upstream in favour of `amd_sysfs`,
and `intel_gpu_top` is not used on the xe driver, where `intel_sysfs` is
preferred.
Access to GPU device nodes is only granted for the collectors listed
here, so a collector provided through
{option}`services.beszel.agent.extraPath` has to be listed as well.
'';
};
};
};
default = { };
@@ -137,25 +238,22 @@ in
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
# drop empty lists so an unset GPU_COLLECTOR keeps upstream auto-detection
environment = lib.mapAttrs (
_: value: if lib.isBool value then (lib.boolToString value) else value
) (cfg.environment // { DATA_DIR = cfg.dataDir; });
_: value:
if lib.isBool value then
(lib.boolToString value)
else if lib.isList value then
lib.concatStringsSep "," value
else
value
) (lib.filterAttrs (_: value: value != [ ]) (cfg.environment // { DATA_DIR = cfg.dataDir; }));
path =
cfg.extraPath
++ lib.optionals cfg.smartmon.enable [ cfg.smartmon.package ]
++ lib.optionals config.boot.zfs.enabled [ config.boot.zfs.package ]
++ lib.optionals (!cfg.environment.SKIP_GPU) (
lib.optionals (builtins.elem "nvidia" config.services.xserver.videoDrivers) [
(lib.getBin config.hardware.nvidia.package)
]
++ lib.optionals (builtins.elem "amdgpu" config.services.xserver.videoDrivers) [
(lib.getBin pkgs.rocmPackages.rocm-smi)
]
++ lib.optionals (builtins.elem "intel" config.services.xserver.videoDrivers) [
(lib.getBin pkgs.intel-gpu-tools)
]
);
++ gpuPackages;
serviceConfig = {
ExecStart = ''
@@ -176,26 +274,21 @@ in
DynamicUser = true;
User = "beszel-agent";
# Capabilities needed for SMART monitoring
AmbientCapabilities = lib.mkIf cfg.smartmon.enable [
"CAP_SYS_RAWIO"
"CAP_SYS_ADMIN"
];
CapabilityBoundingSet = lib.mkIf cfg.smartmon.enable [
"CAP_SYS_RAWIO"
"CAP_SYS_ADMIN"
];
# Capabilities needed for SMART monitoring and GPU performance counters
AmbientCapabilities = serviceCapabilities;
CapabilityBoundingSet = serviceCapabilities;
# Device access for SMART monitoring
DeviceAllow = lib.mkIf (cfg.smartmon.enable && cfg.smartmon.deviceAllow != [ ]) (
map (device: "${device} r") cfg.smartmon.deviceAllow
);
DeviceAllow = lib.mkIf (deviceAllowList != [ ]) deviceAllowList;
LockPersonality = true;
NoNewPrivileges = !cfg.smartmon.enable;
PrivateDevices = !cfg.smartmon.enable && !config.boot.zfs.enabled && cfg.environment.SKIP_GPU;
PrivateDevices = !cfg.smartmon.enable && !config.boot.zfs.enabled && !gpuNeedsDevices;
PrivateTmp = true;
PrivateUsers = !cfg.smartmon.enable && !config.boot.zfs.enabled && !cfg.environment.SKIP_SYSTEMD;
PrivateUsers =
!cfg.smartmon.enable
&& !config.boot.zfs.enabled
&& !cfg.environment.SKIP_SYSTEMD
&& !gpuNeedsCapabilities;
ProtectClock = true;
ProtectControlGroups = "strict";
ProtectHome = "read-only";
@@ -210,7 +303,7 @@ in
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
SystemCallFilter = [ "@system-service" ];
SystemCallFilter = [ "@system-service" ] ++ collectorAttrs "systemCalls";
Type = "simple";
UMask = 27;
};