nixos/nvme-rs: init

This commit is contained in:
liberodark
2025-05-25 10:02:57 +02:00
parent 242de16867
commit 74a08886b1
5 changed files with 365 additions and 0 deletions

View File

@@ -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}
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->

View File

@@ -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

View File

@@ -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 ];
};
}

View File

@@ -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;

157
nixos/tests/nvme-rs.nix Normal file
View File

@@ -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 <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'")
'';
}