@@ -0,0 +1,230 @@
1+{ config, lib, pkgs, ... }:
2+
3+with lib;
4+
5+let cfg = config.snapraid;
6+in
7+{
8+ options.snapraid = with types; {
9+ enable = mkEnableOption "SnapRAID";
10+ dataDisks = mkOption {
11+ default = { };
12+ example = {
13+ d1 = "/mnt/disk1/";
14+ d2 = "/mnt/disk2/";
15+ d3 = "/mnt/disk3/";
16+ };
17+ description = "SnapRAID data disks.";
18+ type = attrsOf str;
19+ };
20+ parityFiles = mkOption {
21+ default = [ ];
22+ example = [
23+ "/mnt/diskp/snapraid.parity"
24+ "/mnt/diskq/snapraid.2-parity"
25+ "/mnt/diskr/snapraid.3-parity"
26+ "/mnt/disks/snapraid.4-parity"
27+ "/mnt/diskt/snapraid.5-parity"
28+ "/mnt/disku/snapraid.6-parity"
29+ ];
30+ description = "SnapRAID parity files.";
31+ type = listOf str;
32+ };
33+ contentFiles = mkOption {
34+ default = [ ];
35+ example = [
36+ "/var/snapraid.content"
37+ "/mnt/disk1/snapraid.content"
38+ "/mnt/disk2/snapraid.content"
39+ ];
40+ description = "SnapRAID content list files.";
41+ type = listOf str;
42+ };
43+ exclude = mkOption {
44+ default = [ ];
45+ example = [ "*.unrecoverable" "/tmp/" "/lost+found/" ];
46+ description = "SnapRAID exclude directives.";
47+ type = listOf str;
48+ };
49+ touchBeforeSync = mkOption {
50+ default = true;
51+ example = false;
52+ description =
53+ "Whether <command>snapraid touch</command> should be run before <command>snapraid sync</command>.";
54+ type = bool;
55+ };
56+ sync.interval = mkOption {
57+ default = "01:00";
58+ example = "daily";
59+ description = "How often to run <command>snapraid sync</command>.";
60+ type = str;
61+ };
62+ scrub = {
63+ interval = mkOption {
64+ default = "Mon *-*-* 02:00:00";
65+ example = "weekly";
66+ description = "How often to run <command>snapraid scrub</command>.";
67+ type = str;
68+ };
69+ plan = mkOption {
70+ default = 8;
71+ example = 5;
72+ description =
73+ "Percent of the array that should be checked by <command>snapraid scrub</command>.";
74+ type = int;
75+ };
76+ olderThan = mkOption {
77+ default = 10;
78+ example = 20;
79+ description =
80+ "Number of days since data was last scrubbed before it can be scrubbed again.";
81+ type = int;
82+ };
83+ };
84+ extraConfig = mkOption {
85+ default = "";
86+ example = ''
87+ nohidden
88+ blocksize 256
89+ hashsize 16
90+ autosave 500
91+ pool /pool
92+ '';
93+ description = "Extra config options for SnapRAID.";
94+ type = lines;
95+ };
96+ };
97+
98+ config =
99+ let
100+ nParity = builtins.length cfg.parityFiles;
101+ mkPrepend = pre: s: pre + s;
102+ in
103+ mkIf cfg.enable {
104+ assertions = [
105+ {
106+ assertion = nParity <= 6;
107+ message = "You can have no more than six SnapRAID parity files.";
108+ }
109+ {
110+ assertion = builtins.length cfg.contentFiles >= nParity + 1;
111+ message =
112+ "There must be at least one SnapRAID content file for each SnapRAID parity file plus one.";
113+ }
114+ ];
115+
116+ environment = {
117+ systemPackages = with pkgs; [ snapraid ];
118+
119+ etc."snapraid.conf" = {
120+ text = with cfg;
121+ let
122+ prependData = mkPrepend "data ";
123+ prependContent = mkPrepend "content ";
124+ prependExclude = mkPrepend "exclude ";
125+ in
126+ concatStringsSep "\n"
127+ (map prependData
128+ ((mapAttrsToList (name: value: name + " " + value)) dataDisks)
129+ ++ zipListsWith (a: b: a + b)
130+ ([ "parity " ] ++ map (i: toString i + "-parity ") (range 2 6))
131+ parityFiles ++ map prependContent contentFiles
132+ ++ map prependExclude exclude) + "\n" + extraConfig;
133+ };
134+ };
135+
136+ systemd.services = with cfg; {
137+ snapraid-scrub = {
138+ description = "Scrub the SnapRAID array";
139+ startAt = scrub.interval;
140+ serviceConfig = {
141+ Type = "oneshot";
142+ ExecStart = "${pkgs.snapraid}/bin/snapraid scrub -p ${
143+ toString scrub.plan
144+ } -o ${toString scrub.olderThan}";
145+ Nice = 19;
146+ IOSchedulingPriority = 7;
147+ CPUSchedulingPolicy = "batch";
148+
149+ LockPersonality = true;
150+ MemoryDenyWriteExecute = true;
151+ NoNewPrivileges = true;
152+ PrivateDevices = true;
153+ PrivateTmp = true;
154+ ProtectClock = true;
155+ ProtectControlGroups = true;
156+ ProtectHostname = true;
157+ ProtectKernelLogs = true;
158+ ProtectKernelModules = true;
159+ ProtectKernelTunables = true;
160+ RestrictAddressFamilies = "none";
161+ RestrictNamespaces = true;
162+ RestrictRealtime = true;
163+ RestrictSUIDSGID = true;
164+ SystemCallArchitectures = "native";
165+ SystemCallFilter = "@system-service";
166+ SystemCallErrorNumber = "EPERM";
167+ CapabilityBoundingSet = "CAP_DAC_OVERRIDE";
168+
169+ ProtectSystem = "strict";
170+ ProtectHome = "read-only";
171+ ReadWritePaths =
172+ # scrub requires access to directories containing content files
173+ # to remove them if they are stale
174+ let
175+ contentDirs = map dirOf contentFiles;
176+ in
177+ unique (
178+ attrValues dataDisks ++ contentDirs
179+ );
180+ };
181+ unitConfig.After = "snapraid-sync.service";
182+ };
183+ snapraid-sync = {
184+ description = "Synchronize the state of the SnapRAID array";
185+ startAt = sync.interval;
186+ serviceConfig = {
187+ Type = "oneshot";
188+ ExecStart = "${pkgs.snapraid}/bin/snapraid sync";
189+ Nice = 19;
190+ IOSchedulingPriority = 7;
191+ CPUSchedulingPolicy = "batch";
192+
193+ LockPersonality = true;
194+ MemoryDenyWriteExecute = true;
195+ NoNewPrivileges = true;
196+ PrivateDevices = true;
197+ PrivateTmp = true;
198+ ProtectClock = true;
199+ ProtectControlGroups = true;
200+ ProtectHostname = true;
201+ ProtectKernelLogs = true;
202+ ProtectKernelModules = true;
203+ ProtectKernelTunables = true;
204+ RestrictAddressFamilies = "none";
205+ RestrictNamespaces = true;
206+ RestrictRealtime = true;
207+ RestrictSUIDSGID = true;
208+ SystemCallArchitectures = "native";
209+ SystemCallFilter = "@system-service";
210+ SystemCallErrorNumber = "EPERM";
211+ CapabilityBoundingSet = "CAP_DAC_OVERRIDE";
212+
213+ ProtectSystem = "strict";
214+ ProtectHome = "read-only";
215+ ReadWritePaths =
216+ # sync requires access to directories containing content files
217+ # to remove them if they are stale
218+ let
219+ contentDirs = map dirOf contentFiles;
220+ in
221+ unique (
222+ attrValues dataDisks ++ parityFiles ++ contentDirs
223+ );
224+ } // optionalAttrs touchBeforeSync {
225+ ExecStartPre = "${pkgs.snapraid}/bin/snapraid touch";
226+ };
227+ };
228+ };
229+ };
230+}