diff --git a/nixos/doc/manual/release-notes/rl-2511.section.md b/nixos/doc/manual/release-notes/rl-2511.section.md index 8dc6829d7182..4fc6e12d04d6 100644 --- a/nixos/doc/manual/release-notes/rl-2511.section.md +++ b/nixos/doc/manual/release-notes/rl-2511.section.md @@ -146,6 +146,8 @@ - [KMinion](https://github.com/redpanda-data/kminion), feature-rich Prometheus exporter for Apache Kafka. Available as [services.prometheus.exporters.kafka](options.html#opt-services.prometheus.exporters.kafka). +- [Beszel](https://beszel.dev), a lightweight server monitoring hub with historical data, docker stats, and alerts. Available as [`services.beszel.agent`](options.html#opt-services.beszel.agent.enable) and [`services.beszel.hub`](options.html#opt-services.beszel.hub.enable). + - [Spoolman](https://github.com/Donkie/Spoolman), a inventory management system for Filament spools. Available as [services.spoolman](#opt-services.spoolman.enable). - [Temporal](https://temporal.io/), a durable execution platform that enables diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 3b925039ebc7..520e7ad094dd 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -972,6 +972,8 @@ ./services/monitoring/apcupsd.nix ./services/monitoring/arbtt.nix ./services/monitoring/below.nix + ./services/monitoring/beszel-agent.nix + ./services/monitoring/beszel-hub.nix ./services/monitoring/bosun.nix ./services/monitoring/cadvisor.nix ./services/monitoring/certspotter.nix diff --git a/nixos/modules/services/monitoring/beszel-agent.nix b/nixos/modules/services/monitoring/beszel-agent.nix new file mode 100644 index 000000000000..9cb4e1e57c7c --- /dev/null +++ b/nixos/modules/services/monitoring/beszel-agent.nix @@ -0,0 +1,119 @@ +{ + lib, + config, + pkgs, + ... +}: +let + cfg = config.services.beszel.agent; +in +{ + meta.maintainers = with lib.maintainers; [ + BonusPlay + arunoruto + ]; + + options.services.beszel.agent = { + enable = lib.mkEnableOption "beszel agent"; + package = lib.mkPackageOption pkgs "beszel" { }; + openFirewall = (lib.mkEnableOption "") // { + description = "Whether to open the firewall port (default 45876)."; + }; + + environment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = '' + Environment variables for configuring the beszel-agent service. + This field will end up public in /nix/store, for secret values (such as `KEY`) use `environmentFile`. + + See for available options. + ''; + }; + environmentFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + File path containing environment variables for configuring the beszel-agent service in the format of an EnvironmentFile. See {manpage}`systemd.exec(5)`. + ''; + }; + extraPath = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + description = '' + Extra packages to add to beszel path (such as nvidia-smi or rocm-smi). + ''; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.beszel-agent = { + description = "Beszel Server Monitoring Agent"; + + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + + environment = cfg.environment; + path = + cfg.extraPath + ++ 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) + ]; + + serviceConfig = { + ExecStart = '' + ${cfg.package}/bin/beszel-agent + ''; + + EnvironmentFile = cfg.environmentFile; + + # adds ability to monitor docker/podman containers + SupplementaryGroups = + lib.optionals config.virtualisation.docker.enable [ "docker" ] + ++ lib.optionals ( + config.virtualisation.podman.enable && config.virtualisation.podman.dockerSocket.enable + ) [ "podman" ]; + + DynamicUser = true; + User = "beszel-agent"; + LockPersonality = true; + NoNewPrivileges = true; + PrivateTmp = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = "strict"; + ProtectHome = "read-only"; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectSystem = "strict"; + Restart = "on-failure"; + RestartSec = "30s"; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallErrorNumber = "EPERM"; + SystemCallFilter = [ "@system-service" ]; + Type = "simple"; + UMask = 27; + }; + }; + + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ + ( + if (builtins.hasAttr "PORT" cfg.environment) then + (lib.strings.toInt cfg.environment.PORT) + else + 45876 + ) + ]; + }; +} diff --git a/nixos/modules/services/monitoring/beszel-hub.nix b/nixos/modules/services/monitoring/beszel-hub.nix new file mode 100644 index 000000000000..8028d13c39d3 --- /dev/null +++ b/nixos/modules/services/monitoring/beszel-hub.nix @@ -0,0 +1,114 @@ +{ + lib, + config, + pkgs, + ... +}: +let + cfg = config.services.beszel.hub; +in +{ + meta.maintainers = with lib.maintainers; [ + BonusPlay + arunoruto + ]; + + options.services.beszel.hub = { + enable = lib.mkEnableOption "beszel hub"; + + package = lib.mkPackageOption pkgs "beszel" { }; + + host = lib.mkOption { + default = "127.0.0.1"; + type = lib.types.str; + example = "0.0.0.0"; + description = "Host or address this beszel hub listens on."; + }; + port = lib.mkOption { + default = 8090; + type = lib.types.port; + example = 3002; + description = "Port for this beszel hub to listen on."; + }; + + dataDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/beszel-hub"; + description = "Data directory of beszel-hub."; + }; + + environment = lib.mkOption { + type = with lib.types; attrsOf str; + default = { }; + example = { + DISABLE_PASSWORD_AUTH = "true"; + }; + description = '' + Environment variables passed to the systemd service. + See for available options. + ''; + }; + environmentFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + Environment file to be passed to the systemd service. + Useful for passing secrets to the service to prevent them from being + world-readable in the Nix store. See {manpage}`systemd.exec(5)`. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.beszel-hub = { + description = "Beszel Server Monitoring Web App"; + + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + environment = cfg.environment; + + serviceConfig = { + ExecStartPre = [ + "${cfg.package}/bin/beszel-hub migrate up" + "${cfg.package}/bin/beszel-hub history-sync" + ]; + ExecStart = '' + ${cfg.package}/bin/beszel-hub serve --http='${cfg.host}:${toString cfg.port}' + ''; + + EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile; + WorkingDirectory = cfg.dataDir; + StateDirectory = baseNameOf cfg.dataDir; + RuntimeDirectory = baseNameOf cfg.dataDir; + ReadWritePaths = cfg.dataDir; + + DynamicUser = true; + User = "beszel-hub"; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = "strict"; + ProtectHome = "read-only"; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectSystem = "strict"; + DevicePolicy = "closed"; + Restart = "on-failure"; + RestartSec = "30s"; + RestrictRealtime = true; + RestrictSUIDSGID = true; + RestrictNamespaces = true; + SystemCallArchitectures = "native"; + SystemCallErrorNumber = "EPERM"; + SystemCallFilter = [ "@system-service" ]; + UMask = 27; + }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 7ef0a3a4a368..3af4002a6aa2 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -266,6 +266,7 @@ in beanstalkd = runTest ./beanstalkd.nix; bees = runTest ./bees.nix; benchexec = runTest ./benchexec.nix; + beszel = runTest ./beszel.nix; binary-cache = runTest { imports = [ ./binary-cache.nix ]; _module.args.compression = "zstd"; diff --git a/nixos/tests/beszel.nix b/nixos/tests/beszel.nix new file mode 100644 index 000000000000..77a4a32a3747 --- /dev/null +++ b/nixos/tests/beszel.nix @@ -0,0 +1,119 @@ +{ pkgs, lib, ... }: +{ + name = "beszel"; + meta.maintainers = with lib.maintainers; [ h7x4 ]; + + nodes = { + hubHost = + { config, pkgs, ... }: + { + virtualisation.vlans = [ 1 ]; + + systemd.network.networks."01-eth1" = { + name = "eth1"; + networkConfig.Address = "10.0.0.1/24"; + }; + + networking = { + useNetworkd = true; + useDHCP = false; + }; + + services.beszel.hub = { + enable = true; + host = "10.0.0.1"; + }; + + networking.firewall.allowedTCPPorts = [ + config.services.beszel.hub.port + ]; + + environment.systemPackages = [ + config.services.beszel.hub.package + ]; + }; + + agentHost = + { config, pkgs, ... }: + { + virtualisation.vlans = [ 1 ]; + + systemd.network.networks."01-eth1" = { + name = "eth1"; + networkConfig.Address = "10.0.0.2/24"; + }; + + networking = { + useNetworkd = true; + useDHCP = false; + }; + + environment.systemPackages = with pkgs; [ jq ]; + + specialisation."agent".configuration = { + services.beszel.agent = { + enable = true; + environment.HUB_URL = "http://10.0.0.1:8090"; + environment.KEY_FILE = "/var/lib/beszel-agent/id_ed25519.pub"; + environment.TOKEN_FILE = "/var/lib/beszel-agent/token"; + openFirewall = true; + }; + }; + }; + }; + + testScript = + { nodes, ... }: + let + hubCfg = nodes.hubHost.services.beszel.hub; + agentCfg = nodes.agentHost.specialisation."agent".configuration.services.beszel.agent; + in + '' + import json + + start_all() + + with subtest("Start hub"): + hubHost.wait_for_unit("beszel-hub.service") + hubHost.wait_for_open_port(${toString hubCfg.port}, "${toString hubCfg.host}") + + with subtest("Register user"): + agentHost.succeed('curl -f --json \'${ + builtins.toJSON { + email = "admin@example.com"; + password = "password"; + } + }\' "${agentCfg.environment.HUB_URL}/api/beszel/create-user"') + user = json.loads(agentHost.succeed('curl -f --json \'${ + builtins.toJSON { + identity = "admin@example.com"; + password = "password"; + } + }\' ${agentCfg.environment.HUB_URL}/api/collections/users/auth-with-password').strip()) + + with subtest("Install agent credentials"): + agentHost.succeed("mkdir -p \"$(dirname '${agentCfg.environment.KEY_FILE}')\" \"$(dirname '${agentCfg.environment.TOKEN_FILE}')\"") + sshkey = agentHost.succeed(f"curl -H 'Authorization: {user["token"]}' -f ${agentCfg.environment.HUB_URL}/api/beszel/getkey | jq -r .key").strip() + utoken = agentHost.succeed(f"curl -H 'Authorization: {user["token"]}' -f ${agentCfg.environment.HUB_URL}/api/beszel/universal-token | jq -r .token").strip() + agentHost.succeed(f"echo '{sshkey}' > '${agentCfg.environment.KEY_FILE}'") + agentHost.succeed(f"echo '{utoken}' > '${agentCfg.environment.TOKEN_FILE}'") + + with subtest("Register agent in hub"): + agentHost.succeed(f'curl -H \'Authorization: {user["token"]}\' -f --json \'{${ + builtins.toJSON { + "host" = "10.0.0.2"; + "name" = "agent"; + "pkey" = "{sshkey}"; + "port" = "45876"; + "tkn" = "{utoken}"; + "users" = ''{user['record']['id']}''; + } + }}\' "${agentCfg.environment.HUB_URL}/api/collections/systems/records"') + + with subtest("Start agent"): + agentHost.succeed("/run/current-system/specialisation/agent/bin/switch-to-configuration switch") + agentHost.wait_for_unit("beszel-agent.service") + agentHost.wait_until_succeeds("journalctl -eu beszel-agent --grep 'SSH connection established'") + agentHost.wait_until_succeeds(f'curl -H \'Authorization: {user["token"]}\' -f ${agentCfg.environment.HUB_URL}/api/collections/systems/records | grep agentHost') + ''; +} diff --git a/pkgs/by-name/be/beszel/package.nix b/pkgs/by-name/be/beszel/package.nix index 79f012fdaea4..930f7b281482 100644 --- a/pkgs/by-name/be/beszel/package.nix +++ b/pkgs/by-name/be/beszel/package.nix @@ -4,6 +4,7 @@ fetchFromGitHub, nix-update-script, buildNpmPackage, + nixosTests, }: buildGoModule rec { pname = "beszel"; @@ -62,11 +63,14 @@ buildGoModule rec { mv $out/bin/hub $out/bin/beszel-hub ''; - passthru.updateScript = nix-update-script { - extraArgs = [ - "--subpackage" - "webui" - ]; + passthru = { + updateScript = nix-update-script { + extraArgs = [ + "--subpackage" + "webui" + ]; + }; + tests.nixos = nixosTests.beszel; }; meta = {