1{
2 config,
3 lib,
4 pkgs,
5 utils,
6 ...
7}:
8
9let
10 cfg = config.boot.bcachefs;
11 cfgScrub = config.services.bcachefs.autoScrub;
12
13 bootFs = lib.filterAttrs (
14 n: fs: (fs.fsType == "bcachefs") && (utils.fsNeededForBoot fs)
15 ) config.fileSystems;
16
17 commonFunctions = ''
18 prompt() {
19 local name="$1"
20 printf "enter passphrase for $name: "
21 }
22
23 tryUnlock() {
24 local name="$1"
25 local path="$2"
26 local success=false
27 local target
28 local uuid=$(echo -n $path | sed -e 's,UUID=\(.*\),\1,g')
29
30 printf "waiting for device to appear $path"
31 for try in $(seq 10); do
32 if [ -e $path ]; then
33 target=$(readlink -f $path)
34 success=true
35 break
36 else
37 target=$(blkid --uuid $uuid)
38 if [ $? == 0 ]; then
39 success=true
40 break
41 fi
42 fi
43 echo -n "."
44 sleep 1
45 done
46 printf "\n"
47 if [ $success == true ]; then
48 path=$target
49 fi
50
51 if bcachefs unlock -c $path > /dev/null 2> /dev/null; then # test for encryption
52 prompt $name
53 until bcachefs unlock $path 2> /dev/null; do # repeat until successfully unlocked
54 printf "unlocking failed!\n"
55 prompt $name
56 done
57 printf "unlocking successful.\n"
58 else
59 echo "Cannot unlock device $uuid with path $path" >&2
60 fi
61 }
62 '';
63
64 # we need only unlock one device manually, and cannot pass multiple at once
65 # remove this adaptation when bcachefs implements mounting by filesystem uuid
66 # also, implement automatic waiting for the constituent devices when that happens
67 # bcachefs does not support mounting devices with colons in the path, ergo we don't (see #49671)
68 firstDevice = fs: lib.head (lib.splitString ":" fs.device);
69
70 useClevis =
71 fs:
72 config.boot.initrd.clevis.enable
73 && (lib.hasAttr (firstDevice fs) config.boot.initrd.clevis.devices);
74
75 openCommand =
76 name: fs:
77 if useClevis fs then
78 ''
79 if clevis decrypt < /etc/clevis/${firstDevice fs}.jwe | bcachefs unlock ${firstDevice fs}
80 then
81 printf "unlocked ${name} using clevis\n"
82 else
83 printf "falling back to interactive unlocking...\n"
84 tryUnlock ${name} ${firstDevice fs}
85 fi
86 ''
87 else
88 ''
89 tryUnlock ${name} ${firstDevice fs}
90 '';
91
92 mkUnits =
93 prefix: name: fs:
94 let
95 parseTags =
96 device:
97 if lib.hasPrefix "LABEL=" device then
98 "/dev/disk/by-label/" + lib.removePrefix "LABEL=" device
99 else if lib.hasPrefix "UUID=" device then
100 "/dev/disk/by-uuid/" + lib.removePrefix "UUID=" device
101 else if lib.hasPrefix "PARTLABEL=" device then
102 "/dev/disk/by-partlabel/" + lib.removePrefix "PARTLABEL=" device
103 else if lib.hasPrefix "PARTUUID=" device then
104 "/dev/disk/by-partuuid/" + lib.removePrefix "PARTUUID=" device
105 else if lib.hasPrefix "ID=" device then
106 "/dev/disk/by-id/" + lib.removePrefix "ID=" device
107 else
108 device;
109 device = parseTags (firstDevice fs);
110 mkDeviceUnit = device: "${utils.escapeSystemdPath device}.device";
111 mkMountUnit = path: "${utils.escapeSystemdPath (lib.removeSuffix "/" path)}.mount";
112 deviceUnit = mkDeviceUnit device;
113 mountUnit = mkMountUnit (prefix + fs.mountPoint);
114 extractProperty =
115 prop: options: (map (lib.removePrefix prop) (builtins.filter (lib.hasPrefix prop) options));
116 normalizeUnits =
117 unit:
118 if lib.hasPrefix "/dev/" unit then
119 mkDeviceUnit unit
120 else if lib.hasPrefix "/" unit then
121 mkMountUnit unit
122 else
123 unit;
124 requiredUnits = map normalizeUnits (extractProperty "x-systemd.requires=" fs.options);
125 wantedUnits = map normalizeUnits (extractProperty "x-systemd.wants=" fs.options);
126 requiredMounts = extractProperty "x-systemd.requires-mounts-for=" fs.options;
127 wantedMounts = extractProperty "x-systemd.wants-mounts-for=" fs.options;
128 in
129 {
130 name = "unlock-bcachefs-${utils.escapeSystemdPath fs.mountPoint}";
131 value = {
132 description = "Unlock bcachefs for ${fs.mountPoint}";
133 requiredBy = [ mountUnit ];
134 after = [ deviceUnit ] ++ requiredUnits ++ wantedUnits;
135 before = [
136 mountUnit
137 "shutdown.target"
138 ];
139 bindsTo = [ deviceUnit ];
140 requires = requiredUnits;
141 wants = wantedUnits;
142 unitConfig = {
143 RequiresMountsFor = requiredMounts;
144 WantsMountsFor = wantedMounts;
145 };
146 conflicts = [ "shutdown.target" ];
147 unitConfig.DefaultDependencies = false;
148 serviceConfig = {
149 Type = "oneshot";
150 ExecCondition = "${cfg.package}/bin/bcachefs unlock -c \"${device}\"";
151 Restart = "on-failure";
152 RestartMode = "direct";
153 # Ideally, this service would lock the key on stop.
154 # As is, RemainAfterExit doesn't accomplish anything.
155 RemainAfterExit = true;
156 };
157 script =
158 let
159 unlock = ''${cfg.package}/bin/bcachefs unlock "${device}"'';
160 unlockInteractively = ''${config.boot.initrd.systemd.package}/bin/systemd-ask-password --timeout=0 "enter passphrase for ${name}" | exec ${unlock}'';
161 in
162 if useClevis fs then
163 ''
164 if ${config.boot.initrd.clevis.package}/bin/clevis decrypt < "/etc/clevis/${device}.jwe" | ${unlock}
165 then
166 printf "unlocked ${name} using clevis\n"
167 else
168 printf "falling back to interactive unlocking...\n"
169 ${unlockInteractively}
170 fi
171 ''
172 else
173 ''
174 ${unlockInteractively}
175 '';
176 };
177 };
178in
179
180{
181 options.boot.bcachefs = {
182 package = lib.mkPackageOption pkgs "bcachefs-tools" {
183 extraDescription = ''
184 This package should also provide a passthru 'kernelModule'
185 attribute to build the out-of-tree kernel module.
186 '';
187 };
188
189 modulePackage = lib.mkOption {
190 type = lib.types.package;
191 # See NOTE in linux-kernels.nix
192 default = config.boot.kernelPackages.callPackage cfg.package.kernelModule { };
193 internal = true;
194 };
195 };
196
197 options.services.bcachefs.autoScrub = {
198 enable = lib.mkEnableOption "regular bcachefs scrub";
199
200 fileSystems = lib.mkOption {
201 type = lib.types.listOf lib.types.path;
202 example = [ "/" ];
203 description = ''
204 List of paths to bcachefs filesystems to regularly call {command}`bcachefs scrub` on.
205 Defaults to all mount points with bcachefs filesystems.
206 '';
207 };
208
209 interval = lib.mkOption {
210 default = "monthly";
211 type = lib.types.str;
212 example = "weekly";
213 description = ''
214 Systemd calendar expression for when to scrub bcachefs filesystems.
215 The recommended period is a month but could be less.
216 See
217 {manpage}`systemd.time(7)`
218 for more information on the syntax.
219 '';
220 };
221 };
222
223 config = lib.mkIf (config.boot.supportedFilesystems.bcachefs or false) (
224 lib.mkMerge [
225 {
226 assertions = [
227 {
228 assertion =
229 let
230 kernel = config.boot.kernelPackages.kernel;
231 in
232 (
233 kernel.kernelAtLeast "6.7"
234 || (lib.elem (kernel.structuredExtraConfig.BCACHEFS_FS or null) [
235 lib.kernel.module
236 lib.kernel.yes
237 (lib.kernel.option lib.kernel.yes)
238 ])
239 );
240
241 message = "Linux 6.7-rc1 at minimum or a custom linux kernel with bcachefs support is required";
242 }
243 ];
244
245 warnings = lib.mkIf cfg.modulePackage.meta.broken [
246 ''
247 Using unmaintained in-tree bcachefs kernel module. This
248 will be removed in 26.05. Please use a kernel supported
249 by the out-of-tree module package.
250 ''
251 ];
252
253 # Bcachefs upstream recommends using the latest kernel
254 boot.kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
255
256 # needed for systemd-remount-fs
257 system.fsPackages = [ cfg.package ];
258 services.udev.packages = [ cfg.package ];
259
260 boot.extraModulePackages = lib.optionals (!cfg.modulePackage.meta.broken) [
261 cfg.modulePackage
262 ];
263
264 systemd = {
265 packages = [ cfg.package ];
266 services = lib.mapAttrs' (mkUnits "") (
267 lib.filterAttrs (n: fs: (fs.fsType == "bcachefs") && (!utils.fsNeededForBoot fs)) config.fileSystems
268 );
269 };
270 }
271
272 (lib.mkIf ((config.boot.initrd.supportedFilesystems.bcachefs or false) || (bootFs != { })) {
273 boot.initrd.availableKernelModules = [
274 "bcachefs"
275 "sha256"
276 ]
277 ++ lib.optionals (config.boot.kernelPackages.kernel.kernelOlder "6.15") [
278 # chacha20 and poly1305 are required only for decryption attempts
279 # kernel 6.15 uses kernel api libraries for poly1305/chacha20: 4bf4b5046de0ef7f9dc50f3a9ef8a6dcda178a6d
280 # kernel 6.16 removes poly1305: ceef731b0e22df80a13d67773ae9afd55a971f9e
281 "poly1305"
282 "chacha20"
283 ];
284 boot.initrd.systemd.extraBin = {
285 # do we need this? boot/systemd.nix:566 & boot/systemd/initrd.nix:357
286 "bcachefs" = "${cfg.package}/bin/bcachefs";
287 "mount.bcachefs" = "${cfg.package}/bin/mount.bcachefs";
288 };
289 boot.initrd.extraUtilsCommands = lib.mkIf (!config.boot.initrd.systemd.enable) ''
290 copy_bin_and_libs ${cfg.package}/bin/bcachefs
291 copy_bin_and_libs ${cfg.package}/bin/mount.bcachefs
292 '';
293 boot.initrd.extraUtilsCommandsTest = lib.mkIf (!config.boot.initrd.systemd.enable) ''
294 $out/bin/bcachefs version
295 '';
296
297 boot.initrd.postDeviceCommands = lib.mkIf (!config.boot.initrd.systemd.enable) (
298 commonFunctions + lib.concatStrings (lib.mapAttrsToList openCommand bootFs)
299 );
300
301 boot.initrd.systemd.services = lib.mapAttrs' (mkUnits "/sysroot") bootFs;
302 })
303
304 (lib.mkIf (cfgScrub.enable) {
305 assertions = [
306 {
307 assertion = lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.14";
308 message = "Bcachefs scrubbing is supported from kernel version 6.14 or later.";
309 }
310 {
311 assertion = cfgScrub.enable -> (cfgScrub.fileSystems != [ ]);
312 message = ''
313 If 'services.bcachefs.autoScrub' is enabled, you need to have at least one
314 bcachefs file system mounted via 'fileSystems' or specify a list manually
315 in 'services.bcachefs.autoScrub.fileSystems'.
316 '';
317 }
318 ];
319
320 # This will remove duplicated units from either having a filesystem mounted multiple
321 # time, or additionally mounted subvolumes, as well as having a filesystem span
322 # multiple devices (provided the same device is used to mount said filesystem).
323 services.bcachefs.autoScrub.fileSystems =
324 let
325 isDeviceInList = list: device: builtins.filter (e: e.device == device) list != [ ];
326
327 uniqueDeviceList = lib.foldl' (
328 acc: e: if isDeviceInList acc e.device then acc else acc ++ [ e ]
329 ) [ ];
330 in
331 lib.mkDefault (
332 map (e: e.mountPoint) (
333 uniqueDeviceList (
334 lib.mapAttrsToList (name: fs: {
335 mountPoint = fs.mountPoint;
336 device = fs.device;
337 }) (lib.filterAttrs (name: fs: fs.fsType == "bcachefs") config.fileSystems)
338 )
339 )
340 );
341
342 systemd.timers =
343 let
344 scrubTimer =
345 fs:
346 let
347 fs' = if fs == "/" then "root" else utils.escapeSystemdPath fs;
348 in
349 lib.nameValuePair "bcachefs-scrub-${fs'}" {
350 description = "regular bcachefs scrub timer on ${fs}";
351
352 wantedBy = [ "timers.target" ];
353 timerConfig = {
354 OnCalendar = cfgScrub.interval;
355 AccuracySec = "1d";
356 Persistent = true;
357 };
358 };
359 in
360 lib.listToAttrs (map scrubTimer cfgScrub.fileSystems);
361
362 systemd.services =
363 let
364 scrubService =
365 fs:
366 let
367 fs' = if fs == "/" then "root" else utils.escapeSystemdPath fs;
368 in
369 lib.nameValuePair "bcachefs-scrub-${fs'}" {
370 description = "bcachefs scrub on ${fs}";
371 # scrub prevents suspend2ram or proper shutdown
372 conflicts = [
373 "shutdown.target"
374 "sleep.target"
375 ];
376 before = [
377 "shutdown.target"
378 "sleep.target"
379 ];
380
381 script = "${lib.getExe cfg.package} data scrub ${fs}";
382
383 serviceConfig = {
384 Type = "oneshot";
385 Nice = 19;
386 IOSchedulingClass = "idle";
387 };
388 };
389 in
390 lib.listToAttrs (map scrubService cfgScrub.fileSystems);
391 })
392 ]
393 );
394
395 meta = {
396 inherit (pkgs.bcachefs-tools.meta) maintainers;
397 };
398}