nixos/nvme-rs: init (#410730)

Changed files
+365
nixos
doc
manual
release-notes
modules
services
system
tests
+2
nixos/doc/manual/release-notes/rl-2511.section.md
···
- [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}
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
+1
nixos/modules/module-list.nix
···
./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
+204
nixos/modules/services/system/nvme-rs.nix
···
+
{
+
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 ];
+
};
+
}
+1
nixos/tests/all-tests.nix
···
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;
+157
nixos/tests/nvme-rs.nix
···
+
{ 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 <nvme-monitor@example.com>";
+
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'")
+
'';
+
}