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