1{
2 config,
3 lib,
4 pkgs,
5 utils,
6 ...
7}:
8let
9 inherit (lib) maintainers;
10 inherit (lib.meta) getExe;
11 inherit (lib.modules) mkIf mkMerge;
12 inherit (lib.options)
13 literalExpression
14 mkEnableOption
15 mkOption
16 mkPackageOption
17 ;
18 inherit (lib.types)
19 bool
20 enum
21 nullOr
22 port
23 str
24 submodule
25 ;
26 inherit (utils) genJqSecretsReplacementSnippet;
27
28 cfg = config.services.scrutiny;
29 # Define the settings format used for this program
30 settingsFormat = pkgs.formats.yaml { };
31in
32{
33 options = {
34 services.scrutiny = {
35 enable = mkEnableOption "Scrutiny, a web application for drive monitoring";
36
37 package = mkPackageOption pkgs "scrutiny" { };
38
39 openFirewall = mkEnableOption "opening the default ports in the firewall for Scrutiny";
40
41 influxdb.enable = mkOption {
42 type = bool;
43 default = true;
44 description = ''
45 Enables InfluxDB on the host system using the `services.influxdb2` NixOS module
46 with default options.
47
48 If you already have InfluxDB configured, or wish to connect to an external InfluxDB
49 instance, disable this option.
50 '';
51 };
52
53 settings = mkOption {
54 description = ''
55 Scrutiny settings to be rendered into the configuration file.
56
57 See <https://github.com/AnalogJ/scrutiny/blob/master/example.scrutiny.yaml>.
58
59 Options containing secret data should be set to an attribute set
60 containing the attribute `_secret`. This attribute should be a string
61 or structured JSON with `quote = false;`, pointing to a file that
62 contains the value the option should be set to.
63 '';
64 default = { };
65 type = submodule {
66 freeformType = settingsFormat.type;
67
68 options.web.listen.port = mkOption {
69 type = port;
70 default = 8080;
71 description = "Port for web application to listen on.";
72 };
73
74 options.web.listen.host = mkOption {
75 type = str;
76 default = "0.0.0.0";
77 description = "Interface address for web application to bind to.";
78 };
79
80 options.web.listen.basepath = mkOption {
81 type = str;
82 default = "";
83 example = "/scrutiny";
84 description = ''
85 If Scrutiny will be behind a path prefixed reverse proxy, you can override this
86 value to serve Scrutiny on a subpath.
87 '';
88 };
89
90 options.log.level = mkOption {
91 type = enum [
92 "INFO"
93 "DEBUG"
94 ];
95 default = "INFO";
96 description = "Log level for Scrutiny.";
97 };
98
99 options.web.influxdb.scheme = mkOption {
100 type = str;
101 default = "http";
102 description = "URL scheme to use when connecting to InfluxDB.";
103 };
104
105 options.web.influxdb.host = mkOption {
106 type = str;
107 default = "0.0.0.0";
108 description = "IP or hostname of the InfluxDB instance.";
109 };
110
111 options.web.influxdb.port = mkOption {
112 type = port;
113 default = 8086;
114 description = "The port of the InfluxDB instance.";
115 };
116
117 options.web.influxdb.tls.insecure_skip_verify =
118 mkEnableOption "skipping TLS verification when connecting to InfluxDB";
119
120 options.web.influxdb.token = mkOption {
121 type = nullOr str;
122 default = null;
123 description = "Authentication token for connecting to InfluxDB.";
124 };
125
126 options.web.influxdb.org = mkOption {
127 type = nullOr str;
128 default = null;
129 description = "InfluxDB organisation under which to store data.";
130 };
131
132 options.web.influxdb.bucket = mkOption {
133 type = nullOr str;
134 default = null;
135 description = "InfluxDB bucket in which to store data.";
136 };
137 };
138 };
139
140 collector = {
141 enable = mkEnableOption "the Scrutiny metrics collector" // {
142 default = cfg.enable;
143 defaultText = lib.literalExpression "config.services.scrutiny.enable";
144 };
145
146 package = mkPackageOption pkgs "scrutiny-collector" { };
147
148 schedule = mkOption {
149 type = str;
150 default = "*:0/15";
151 description = ''
152 How often to run the collector in systemd calendar format.
153 '';
154 };
155
156 settings = mkOption {
157 description = ''
158 Collector settings to be rendered into the collector configuration file.
159
160 See <https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml>.
161
162 Options containing secret data should be set to an attribute set
163 containing the attribute `_secret`. This attribute should be a string
164 or structured JSON with `quote = false;`, pointing to a file that
165 contains the value the option should be set to.
166 '';
167 default = { };
168 type = submodule {
169 freeformType = settingsFormat.type;
170
171 options.host.id = mkOption {
172 type = nullOr str;
173 default = null;
174 description = "Host ID for identifying/labelling groups of disks";
175 };
176
177 options.api.endpoint = mkOption {
178 type = str;
179 default = "http://${cfg.settings.web.listen.host}:${toString cfg.settings.web.listen.port}";
180 defaultText = literalExpression ''"http://''${config.services.scrutiny.settings.web.listen.host}:''${config.services.scrutiny.settings.web.listen.port}"'';
181 description = "Scrutiny app API endpoint for sending metrics to.";
182 };
183
184 options.log.level = mkOption {
185 type = enum [
186 "INFO"
187 "DEBUG"
188 ];
189 default = "INFO";
190 description = "Log level for Scrutiny collector.";
191 };
192 };
193 };
194 };
195 };
196 };
197
198 config = mkMerge [
199 (mkIf cfg.enable {
200 services.influxdb2.enable = cfg.influxdb.enable;
201
202 networking.firewall = mkIf cfg.openFirewall {
203 allowedTCPPorts = [ cfg.settings.web.listen.port ];
204 };
205
206 systemd.services.scrutiny = {
207 description = "Hard Drive S.M.A.R.T Monitoring, Historical Trends & Real World Failure Thresholds";
208 wantedBy = [ "multi-user.target" ];
209 after = [ "network.target" ] ++ lib.optional cfg.influxdb.enable "influxdb2.service";
210 wants = lib.optional cfg.influxdb.enable "influxdb2.service";
211 environment = {
212 SCRUTINY_VERSION = "1";
213 SCRUTINY_WEB_DATABASE_LOCATION = "/var/lib/scrutiny/scrutiny.db";
214 SCRUTINY_WEB_SRC_FRONTEND_PATH = "${cfg.package}/share/scrutiny";
215 };
216 preStart = ''
217 ${genJqSecretsReplacementSnippet cfg.settings "/run/scrutiny/config.yaml"}
218 '';
219 postStart = ''
220 for i in $(seq 300); do
221 if "${lib.getExe pkgs.curl}" --fail --silent --head "http://${cfg.settings.web.listen.host}:${toString cfg.settings.web.listen.port}" >/dev/null; then
222 echo "Scrutiny is ready (port is open)"
223 exit 0
224 fi
225 echo "Waiting for Scrutiny to open port..."
226 sleep 0.2
227 done
228 echo "Timeout waiting for Scrutiny to open port" >&2
229 exit 1
230 '';
231 serviceConfig = {
232 DynamicUser = true;
233 ExecStart = "${getExe cfg.package} start --config /run/scrutiny/config.yaml";
234 Restart = "always";
235 RuntimeDirectory = "scrutiny";
236 RuntimeDirectoryMode = "0700";
237 StateDirectory = "scrutiny";
238 StateDirectoryMode = "0750";
239 };
240 };
241 })
242 (mkIf cfg.collector.enable {
243 services.smartd = {
244 enable = true;
245 extraOptions = [
246 "-A /var/log/smartd/"
247 "--interval=600"
248 ];
249 };
250
251 systemd = {
252 services.scrutiny-collector = {
253 description = "Scrutiny Collector Service";
254 after = lib.optional cfg.enable "scrutiny.service";
255 wants = lib.optional cfg.enable "scrutiny.service";
256 environment = {
257 COLLECTOR_VERSION = "1";
258 COLLECTOR_API_ENDPOINT = cfg.collector.settings.api.endpoint;
259 };
260 preStart = ''
261 ${genJqSecretsReplacementSnippet cfg.collector.settings "/run/scrutiny-collector/config.yaml"}
262 '';
263 serviceConfig = {
264 Type = "oneshot";
265 ExecStart = "${getExe cfg.collector.package} run --config /run/scrutiny-collector/config.yaml";
266 RuntimeDirectory = "scrutiny-collector";
267 RuntimeDirectoryMode = "0700";
268 };
269 startAt = cfg.collector.schedule;
270 };
271
272 timers.scrutiny-collector.timerConfig.Persistent = true;
273 };
274 })
275 ];
276
277 meta.maintainers = [ maintainers.jnsgruk ];
278}