nixos/nvme-rs: init (#410730)

authored by Masum Reza and committed by GitHub 1d84eb3d 5ee06ac3

Changed files
+365
nixos
doc
manual
release-notes
modules
services
system
tests
+2
nixos/doc/manual/release-notes/rl-2511.section.md
··· 138 139 - [Sshwifty](https://github.com/nirui/sshwifty), a Telnet and SSH client for your browser. Available as [services.sshwifty](#opt-services.sshwifty.enable). 140 141 ## Backward Incompatibilities {#sec-release-25.11-incompatibilities} 142 143 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
··· 138 139 - [Sshwifty](https://github.com/nirui/sshwifty), a Telnet and SSH client for your browser. Available as [services.sshwifty](#opt-services.sshwifty.enable). 140 141 + - [nvme-rs](https://github.com/liberodark/nvme-rs), NVMe monitoring [services.nvme-rs](#opt-services.nvme-rs.enable). 142 + 143 ## Backward Incompatibilities {#sec-release-25.11-incompatibilities} 144 145 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
+1
nixos/modules/module-list.nix
··· 1502 ./services/system/localtimed.nix 1503 ./services/system/nix-daemon.nix 1504 ./services/system/nscd.nix 1505 ./services/system/saslauthd.nix 1506 ./services/system/self-deploy.nix 1507 ./services/system/swapspace.nix
··· 1502 ./services/system/localtimed.nix 1503 ./services/system/nix-daemon.nix 1504 ./services/system/nscd.nix 1505 + ./services/system/nvme-rs.nix 1506 ./services/system/saslauthd.nix 1507 ./services/system/self-deploy.nix 1508 ./services/system/swapspace.nix
+204
nixos/modules/services/system/nvme-rs.nix
···
··· 1 + { 2 + config, 3 + options, 4 + lib, 5 + pkgs, 6 + ... 7 + }: 8 + 9 + let 10 + inherit (lib) types; 11 + cfg = config.services.nvme-rs; 12 + opt = options.services.nvme-rs; 13 + settingsFormat = pkgs.formats.toml { }; 14 + in 15 + { 16 + options.services.nvme-rs = { 17 + enable = lib.mkEnableOption "nvme-rs, a monitoring service"; 18 + 19 + package = lib.mkPackageOption pkgs "nvme-rs" { }; 20 + 21 + settings = lib.mkOption { 22 + type = types.submodule { 23 + freeformType = settingsFormat.type; 24 + options = { 25 + check_interval_secs = lib.mkOption { 26 + type = types.int; 27 + default = 3600; 28 + description = "Check interval in seconds"; 29 + example = 86400; 30 + }; 31 + 32 + thresholds = lib.mkOption { 33 + type = types.submodule { 34 + freeformType = settingsFormat.type; 35 + options = { 36 + temp_warning = lib.mkOption { 37 + type = types.int; 38 + default = 55; 39 + description = "Temperature warning threshold (°C)"; 40 + }; 41 + 42 + temp_critical = lib.mkOption { 43 + type = types.int; 44 + default = 65; 45 + description = "Temperature critical threshold (°C)"; 46 + }; 47 + 48 + wear_warning = lib.mkOption { 49 + type = types.int; 50 + default = 20; 51 + description = "Wear warning threshold (%)"; 52 + }; 53 + 54 + wear_critical = lib.mkOption { 55 + type = types.int; 56 + default = 50; 57 + description = "Wear critical threshold (%)"; 58 + }; 59 + 60 + spare_warning = lib.mkOption { 61 + type = types.int; 62 + default = 50; 63 + description = "Available spare warning threshold (%)"; 64 + }; 65 + 66 + error_threshold = lib.mkOption { 67 + type = types.int; 68 + default = 100; 69 + description = "Error count warning threshold"; 70 + }; 71 + }; 72 + }; 73 + default = { }; 74 + description = "Threshold configuration for NVMe monitoring"; 75 + }; 76 + 77 + email = lib.mkOption { 78 + type = types.nullOr ( 79 + types.submodule { 80 + freeformType = settingsFormat.type; 81 + options = { 82 + smtp_server = lib.mkOption { 83 + type = types.str; 84 + default = "smtp.gmail.com"; 85 + description = "SMTP server address"; 86 + example = "mail.example.com"; 87 + }; 88 + 89 + smtp_port = lib.mkOption { 90 + type = types.port; 91 + default = 587; 92 + description = "SMTP server port"; 93 + }; 94 + 95 + smtp_username = lib.mkOption { 96 + type = types.str; 97 + description = "SMTP username"; 98 + example = "your-email@gmail.com"; 99 + }; 100 + 101 + smtp_password_file = lib.mkOption { 102 + type = types.path; 103 + description = "File containing SMTP password"; 104 + example = "/run/secrets/smtp-password"; 105 + }; 106 + 107 + from = lib.mkOption { 108 + type = types.str; 109 + description = "Sender email address"; 110 + example = "nvme-monitor@example.com"; 111 + }; 112 + 113 + to = lib.mkOption { 114 + type = types.str; 115 + description = "Recipient email address"; 116 + example = "admin@example.com"; 117 + }; 118 + 119 + use_tls = lib.mkOption { 120 + type = types.bool; 121 + default = true; 122 + description = "Use TLS for SMTP connection"; 123 + }; 124 + }; 125 + } 126 + ); 127 + default = null; 128 + description = "Email notification configuration"; 129 + }; 130 + }; 131 + }; 132 + default = { }; 133 + description = '' 134 + Configuration for nvme-rs in TOML format. 135 + See the config.toml example for all available options. 136 + ''; 137 + }; 138 + }; 139 + 140 + config = lib.mkIf cfg.enable { 141 + services.nvme-rs.settings = opt.settings.default; 142 + 143 + systemd.services.nvme-rs = { 144 + description = "NVMe health monitoring service"; 145 + after = [ "network.target" ]; 146 + wantedBy = [ "multi-user.target" ]; 147 + 148 + serviceConfig = 149 + let 150 + settingsWithoutNull = 151 + if cfg.settings.email == null then lib.removeAttrs cfg.settings [ "email" ] else cfg.settings; 152 + configFile = settingsFormat.generate "nvme-rs.toml" settingsWithoutNull; 153 + in 154 + { 155 + ExecStart = lib.escapeShellArgs [ 156 + "${lib.getExe cfg.package}" 157 + "daemon" 158 + "--config" 159 + "${configFile}" 160 + ]; 161 + 162 + DynamicUser = true; 163 + SupplementaryGroups = [ "disk" ]; 164 + CapabilityBoundingSet = [ "CAP_SYS_ADMIN" ]; 165 + AmbientCapabilities = [ "CAP_SYS_ADMIN" ]; 166 + LimitCORE = 0; 167 + LimitNOFILE = 65535; 168 + LockPersonality = true; 169 + MemorySwapMax = 0; 170 + MemoryZSwapMax = 0; 171 + PrivateTmp = true; 172 + ProcSubset = "pid"; 173 + ProtectClock = true; 174 + ProtectControlGroups = true; 175 + ProtectHome = true; 176 + ProtectHostname = true; 177 + ProtectKernelLogs = true; 178 + ProtectKernelModules = true; 179 + ProtectKernelTunables = true; 180 + ProtectProc = "invisible"; 181 + ProtectSystem = "strict"; 182 + Restart = "on-failure"; 183 + RestartSec = "10s"; 184 + RestrictAddressFamilies = [ 185 + "AF_INET" 186 + "AF_INET6" 187 + "AF_UNIX" 188 + ]; 189 + RestrictNamespaces = true; 190 + RestrictRealtime = true; 191 + SystemCallArchitectures = "native"; 192 + SystemCallFilter = [ 193 + "@system-service" 194 + "@resources" 195 + "~@privileged" 196 + ]; 197 + NoNewPrivileges = true; 198 + UMask = "0077"; 199 + }; 200 + }; 201 + 202 + environment.systemPackages = [ cfg.package ]; 203 + }; 204 + }
+1
nixos/tests/all-tests.nix
··· 1075 ntpd = runTest ./ntpd.nix; 1076 ntpd-rs = runTest ./ntpd-rs.nix; 1077 nvidia-container-toolkit = runTest ./nvidia-container-toolkit.nix; 1078 nvmetcfg = runTest ./nvmetcfg.nix; 1079 nyxt = runTest ./nyxt.nix; 1080 nzbget = runTest ./nzbget.nix;
··· 1075 ntpd = runTest ./ntpd.nix; 1076 ntpd-rs = runTest ./ntpd-rs.nix; 1077 nvidia-container-toolkit = runTest ./nvidia-container-toolkit.nix; 1078 + nvme-rs = runTest ./nvme-rs.nix; 1079 nvmetcfg = runTest ./nvmetcfg.nix; 1080 nyxt = runTest ./nyxt.nix; 1081 nzbget = runTest ./nzbget.nix;
+157
nixos/tests/nvme-rs.nix
···
··· 1 + { lib, pkgs, ... }: 2 + { 3 + name = "nvme-rs"; 4 + 5 + meta = { 6 + maintainers = with lib.maintainers; [ liberodark ]; 7 + }; 8 + 9 + nodes = { 10 + monitor = 11 + { config, pkgs, ... }: 12 + { 13 + virtualisation = { 14 + emptyDiskImages = [ 15 + 512 16 + 512 17 + ]; 18 + }; 19 + 20 + environment.systemPackages = with pkgs; [ 21 + nvme-rs 22 + jq 23 + ]; 24 + 25 + services.nvme-rs = { 26 + enable = true; 27 + package = pkgs.nvme-rs; 28 + settings = { 29 + check_interval_secs = 60; 30 + 31 + thresholds = { 32 + temp_warning = 50; 33 + temp_critical = 60; 34 + wear_warning = 15; 35 + wear_critical = 40; 36 + spare_warning = 60; 37 + error_threshold = 100; 38 + }; 39 + 40 + email = { 41 + smtp_server = "mail"; 42 + smtp_port = 25; 43 + smtp_username = "nvme-monitor@example.com"; 44 + smtp_password_file = "/run/secrets/smtp-password"; 45 + from = "NVMe Monitor <nvme-monitor@example.com>"; 46 + to = "admin@example.com"; 47 + use_tls = false; 48 + }; 49 + }; 50 + }; 51 + 52 + systemd.tmpfiles.rules = [ 53 + "f /run/secrets/smtp-password 0600 root root - testpassword" 54 + ]; 55 + 56 + networking.firewall.enable = false; 57 + }; 58 + 59 + mail = 60 + { config, pkgs, ... }: 61 + { 62 + services.postfix = { 63 + enable = true; 64 + hostname = "mail"; 65 + domain = "example.com"; 66 + 67 + networks = [ "0.0.0.0/0" ]; 68 + relayDomains = [ "example.com" ]; 69 + localRecipients = [ "admin" ]; 70 + 71 + settings = { 72 + main = { 73 + inet_interfaces = "all"; 74 + inet_protocols = "ipv4"; 75 + smtpd_recipient_restrictions = "permit_mynetworks"; 76 + smtpd_relay_restrictions = "permit_mynetworks"; 77 + }; 78 + }; 79 + }; 80 + 81 + users.users.admin = { 82 + isNormalUser = true; 83 + home = "/home/admin"; 84 + }; 85 + 86 + networking.firewall = { 87 + allowedTCPPorts = [ 25 ]; 88 + }; 89 + }; 90 + 91 + client = 92 + { config, pkgs, ... }: 93 + { 94 + virtualisation = { 95 + emptyDiskImages = [ 256 ]; 96 + }; 97 + 98 + environment.systemPackages = with pkgs; [ 99 + nvme-rs 100 + jq 101 + ]; 102 + 103 + environment.etc."nvme-rs/config.toml".text = '' 104 + check_interval_secs = 3600 105 + 106 + [thresholds] 107 + temp_warning = 55 108 + temp_critical = 65 109 + wear_warning = 20 110 + wear_critical = 50 111 + spare_warning = 50 112 + error_threshold = 5000 113 + ''; 114 + }; 115 + }; 116 + 117 + testScript = 118 + { nodes, ... }: 119 + '' 120 + import json 121 + 122 + start_all() 123 + 124 + for machine in [monitor, mail, client]: 125 + machine.wait_for_unit("multi-user.target") 126 + 127 + mail.wait_for_unit("postfix.service") 128 + mail.wait_for_open_port(25) 129 + 130 + client.succeed("nvme-rs check || true") 131 + client.succeed("nvme-rs check --config /etc/nvme-rs/config.toml || true") 132 + 133 + output = client.succeed("nvme-rs check --format json || echo '[]'") 134 + data = json.loads(output) 135 + assert isinstance(data, list), "JSON output should be a list" 136 + 137 + monitor.wait_for_unit("nvme-rs.service") 138 + monitor.succeed("systemctl is-active nvme-rs.service") 139 + 140 + config_path = monitor.succeed( 141 + "systemctl status nvme-rs | grep -oE '/nix/store[^ ]*nvme-rs.toml' | head -1" 142 + ).strip() 143 + 144 + if config_path: 145 + monitor.succeed(f"grep 'check_interval_secs = 60' {config_path}") 146 + monitor.succeed(f"grep 'temp_warning = 50' {config_path}") 147 + monitor.succeed(f"grep 'smtp_server = \"mail\"' {config_path}") 148 + 149 + logs = monitor.succeed("journalctl -u nvme-rs.service -n 20 --no-pager") 150 + assert "Starting NVMe monitor daemon" in logs or "Check interval" in logs 151 + 152 + monitor.succeed("test -f /run/secrets/smtp-password") 153 + 154 + monitor.succeed("nc -zv mail 25") 155 + monitor.fail("nvme-rs daemon --config /nonexistent.toml 2>&1 | grep -E 'Failed to read'") 156 + ''; 157 + }