From 74a08886b1ae63c6ac53b54dd8cf49a761c9eef7 Mon Sep 17 00:00:00 2001 From: liberodark Date: Sun, 25 May 2025 10:02:57 +0200 Subject: [PATCH] nixos/nvme-rs: init --- .../manual/release-notes/rl-2511.section.md | 2 + nixos/modules/module-list.nix | 1 + nixos/modules/services/system/nvme-rs.nix | 204 ++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/nvme-rs.nix | 157 ++++++++++++++ 5 files changed, 365 insertions(+) create mode 100644 nixos/modules/services/system/nvme-rs.nix create mode 100644 nixos/tests/nvme-rs.nix diff --git a/nixos/doc/manual/release-notes/rl-2511.section.md b/nixos/doc/manual/release-notes/rl-2511.section.md index 8743247ad8ce..552eff1156f2 100644 --- a/nixos/doc/manual/release-notes/rl-2511.section.md +++ b/nixos/doc/manual/release-notes/rl-2511.section.md @@ -130,6 +130,8 @@ - [Sshwifty](https://github.com/nirui/sshwifty), a Telnet and SSH client for your browser. Available as [services.sshwifty](#opt-services.sshwifty.enable). +- [nvme-rs](https://github.com/liberodark/nvme-rs), NVMe monitoring [services.nvme-rs](#opt-services.nvme-rs.enable). + ## Backward Incompatibilities {#sec-release-25.11-incompatibilities} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 7f1eb380cd40..2027247e22f6 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1499,6 +1499,7 @@ ./services/system/localtimed.nix ./services/system/nix-daemon.nix ./services/system/nscd.nix + ./services/system/nvme-rs.nix ./services/system/saslauthd.nix ./services/system/self-deploy.nix ./services/system/swapspace.nix diff --git a/nixos/modules/services/system/nvme-rs.nix b/nixos/modules/services/system/nvme-rs.nix new file mode 100644 index 000000000000..ba02ca3cde17 --- /dev/null +++ b/nixos/modules/services/system/nvme-rs.nix @@ -0,0 +1,204 @@ +{ + config, + options, + lib, + pkgs, + ... +}: + +let + inherit (lib) types; + cfg = config.services.nvme-rs; + opt = options.services.nvme-rs; + settingsFormat = pkgs.formats.toml { }; +in +{ + options.services.nvme-rs = { + enable = lib.mkEnableOption "nvme-rs, a monitoring service"; + + package = lib.mkPackageOption pkgs "nvme-rs" { }; + + settings = lib.mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + options = { + check_interval_secs = lib.mkOption { + type = types.int; + default = 3600; + description = "Check interval in seconds"; + example = 86400; + }; + + thresholds = lib.mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + options = { + temp_warning = lib.mkOption { + type = types.int; + default = 55; + description = "Temperature warning threshold (°C)"; + }; + + temp_critical = lib.mkOption { + type = types.int; + default = 65; + description = "Temperature critical threshold (°C)"; + }; + + wear_warning = lib.mkOption { + type = types.int; + default = 20; + description = "Wear warning threshold (%)"; + }; + + wear_critical = lib.mkOption { + type = types.int; + default = 50; + description = "Wear critical threshold (%)"; + }; + + spare_warning = lib.mkOption { + type = types.int; + default = 50; + description = "Available spare warning threshold (%)"; + }; + + error_threshold = lib.mkOption { + type = types.int; + default = 100; + description = "Error count warning threshold"; + }; + }; + }; + default = { }; + description = "Threshold configuration for NVMe monitoring"; + }; + + email = lib.mkOption { + type = types.nullOr ( + types.submodule { + freeformType = settingsFormat.type; + options = { + smtp_server = lib.mkOption { + type = types.str; + default = "smtp.gmail.com"; + description = "SMTP server address"; + example = "mail.example.com"; + }; + + smtp_port = lib.mkOption { + type = types.port; + default = 587; + description = "SMTP server port"; + }; + + smtp_username = lib.mkOption { + type = types.str; + description = "SMTP username"; + example = "your-email@gmail.com"; + }; + + smtp_password_file = lib.mkOption { + type = types.path; + description = "File containing SMTP password"; + example = "/run/secrets/smtp-password"; + }; + + from = lib.mkOption { + type = types.str; + description = "Sender email address"; + example = "nvme-monitor@example.com"; + }; + + to = lib.mkOption { + type = types.str; + description = "Recipient email address"; + example = "admin@example.com"; + }; + + use_tls = lib.mkOption { + type = types.bool; + default = true; + description = "Use TLS for SMTP connection"; + }; + }; + } + ); + default = null; + description = "Email notification configuration"; + }; + }; + }; + default = { }; + description = '' + Configuration for nvme-rs in TOML format. + See the config.toml example for all available options. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + services.nvme-rs.settings = opt.settings.default; + + systemd.services.nvme-rs = { + description = "NVMe health monitoring service"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = + let + settingsWithoutNull = + if cfg.settings.email == null then lib.removeAttrs cfg.settings [ "email" ] else cfg.settings; + configFile = settingsFormat.generate "nvme-rs.toml" settingsWithoutNull; + in + { + ExecStart = lib.escapeShellArgs [ + "${lib.getExe cfg.package}" + "daemon" + "--config" + "${configFile}" + ]; + + DynamicUser = true; + SupplementaryGroups = [ "disk" ]; + CapabilityBoundingSet = [ "CAP_SYS_ADMIN" ]; + AmbientCapabilities = [ "CAP_SYS_ADMIN" ]; + LimitCORE = 0; + LimitNOFILE = 65535; + LockPersonality = true; + MemorySwapMax = 0; + MemoryZSwapMax = 0; + PrivateTmp = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + Restart = "on-failure"; + RestartSec = "10s"; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "@resources" + "~@privileged" + ]; + NoNewPrivileges = true; + UMask = "0077"; + }; + }; + + environment.systemPackages = [ cfg.package ]; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 93243f29082a..66e34a08edad 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1072,6 +1072,7 @@ in ntpd = runTest ./ntpd.nix; ntpd-rs = runTest ./ntpd-rs.nix; nvidia-container-toolkit = runTest ./nvidia-container-toolkit.nix; + nvme-rs = runTest ./nvme-rs.nix; nvmetcfg = runTest ./nvmetcfg.nix; nyxt = runTest ./nyxt.nix; nzbget = runTest ./nzbget.nix; diff --git a/nixos/tests/nvme-rs.nix b/nixos/tests/nvme-rs.nix new file mode 100644 index 000000000000..8f7337916c65 --- /dev/null +++ b/nixos/tests/nvme-rs.nix @@ -0,0 +1,157 @@ +{ lib, pkgs, ... }: +{ + name = "nvme-rs"; + + meta = { + maintainers = with lib.maintainers; [ liberodark ]; + }; + + nodes = { + monitor = + { config, pkgs, ... }: + { + virtualisation = { + emptyDiskImages = [ + 512 + 512 + ]; + }; + + environment.systemPackages = with pkgs; [ + nvme-rs + jq + ]; + + services.nvme-rs = { + enable = true; + package = pkgs.nvme-rs; + settings = { + check_interval_secs = 60; + + thresholds = { + temp_warning = 50; + temp_critical = 60; + wear_warning = 15; + wear_critical = 40; + spare_warning = 60; + error_threshold = 100; + }; + + email = { + smtp_server = "mail"; + smtp_port = 25; + smtp_username = "nvme-monitor@example.com"; + smtp_password_file = "/run/secrets/smtp-password"; + from = "NVMe Monitor "; + to = "admin@example.com"; + use_tls = false; + }; + }; + }; + + systemd.tmpfiles.rules = [ + "f /run/secrets/smtp-password 0600 root root - testpassword" + ]; + + networking.firewall.enable = false; + }; + + mail = + { config, pkgs, ... }: + { + services.postfix = { + enable = true; + hostname = "mail"; + domain = "example.com"; + + networks = [ "0.0.0.0/0" ]; + relayDomains = [ "example.com" ]; + localRecipients = [ "admin" ]; + + settings = { + main = { + inet_interfaces = "all"; + inet_protocols = "ipv4"; + smtpd_recipient_restrictions = "permit_mynetworks"; + smtpd_relay_restrictions = "permit_mynetworks"; + }; + }; + }; + + users.users.admin = { + isNormalUser = true; + home = "/home/admin"; + }; + + networking.firewall = { + allowedTCPPorts = [ 25 ]; + }; + }; + + client = + { config, pkgs, ... }: + { + virtualisation = { + emptyDiskImages = [ 256 ]; + }; + + environment.systemPackages = with pkgs; [ + nvme-rs + jq + ]; + + environment.etc."nvme-rs/config.toml".text = '' + check_interval_secs = 3600 + + [thresholds] + temp_warning = 55 + temp_critical = 65 + wear_warning = 20 + wear_critical = 50 + spare_warning = 50 + error_threshold = 5000 + ''; + }; + }; + + testScript = + { nodes, ... }: + '' + import json + + start_all() + + for machine in [monitor, mail, client]: + machine.wait_for_unit("multi-user.target") + + mail.wait_for_unit("postfix.service") + mail.wait_for_open_port(25) + + client.succeed("nvme-rs check || true") + client.succeed("nvme-rs check --config /etc/nvme-rs/config.toml || true") + + output = client.succeed("nvme-rs check --format json || echo '[]'") + data = json.loads(output) + assert isinstance(data, list), "JSON output should be a list" + + monitor.wait_for_unit("nvme-rs.service") + monitor.succeed("systemctl is-active nvme-rs.service") + + config_path = monitor.succeed( + "systemctl status nvme-rs | grep -oE '/nix/store[^ ]*nvme-rs.toml' | head -1" + ).strip() + + if config_path: + monitor.succeed(f"grep 'check_interval_secs = 60' {config_path}") + monitor.succeed(f"grep 'temp_warning = 50' {config_path}") + monitor.succeed(f"grep 'smtp_server = \"mail\"' {config_path}") + + logs = monitor.succeed("journalctl -u nvme-rs.service -n 20 --no-pager") + assert "Starting NVMe monitor daemon" in logs or "Check interval" in logs + + monitor.succeed("test -f /run/secrets/smtp-password") + + monitor.succeed("nc -zv mail 25") + monitor.fail("nvme-rs daemon --config /nonexistent.toml 2>&1 | grep -E 'Failed to read'") + ''; +}