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