+2
nixos/doc/manual/release-notes/rl-2511.section.md
+2
nixos/doc/manual/release-notes/rl-2511.section.md
···
138
138
139
139
- [Sshwifty](https://github.com/nirui/sshwifty), a Telnet and SSH client for your browser. Available as [services.sshwifty](#opt-services.sshwifty.enable).
140
140
141
+
- [nvme-rs](https://github.com/liberodark/nvme-rs), NVMe monitoring [services.nvme-rs](#opt-services.nvme-rs.enable).
142
+
141
143
## Backward Incompatibilities {#sec-release-25.11-incompatibilities}
142
144
143
145
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
+1
nixos/modules/module-list.nix
+1
nixos/modules/module-list.nix
+204
nixos/modules/services/system/nvme-rs.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
+1
nixos/tests/all-tests.nix
···
1075
1075
ntpd = runTest ./ntpd.nix;
1076
1076
ntpd-rs = runTest ./ntpd-rs.nix;
1077
1077
nvidia-container-toolkit = runTest ./nvidia-container-toolkit.nix;
1078
+
nvme-rs = runTest ./nvme-rs.nix;
1078
1079
nvmetcfg = runTest ./nvmetcfg.nix;
1079
1080
nyxt = runTest ./nyxt.nix;
1080
1081
nzbget = runTest ./nzbget.nix;
+157
nixos/tests/nvme-rs.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
+
}