1{ config, lib, pkgs, utils, ... }:
2
3with lib;
4
5let
6
7 inInitrd = any (fs: fs == "btrfs") config.boot.initrd.supportedFilesystems;
8 inSystem = any (fs: fs == "btrfs") config.boot.supportedFilesystems;
9
10 cfgScrub = config.services.btrfs.autoScrub;
11
12 enableAutoScrub = cfgScrub.enable;
13 enableBtrfs = inInitrd || inSystem || enableAutoScrub;
14
15in
16
17{
18 options = {
19 # One could also do regular btrfs balances, but that shouldn't be necessary
20 # during normal usage and as long as the filesystems aren't filled near capacity
21 services.btrfs.autoScrub = {
22 enable = mkEnableOption "regular btrfs scrub";
23
24 fileSystems = mkOption {
25 type = types.listOf types.path;
26 example = [ "/" ];
27 description = ''
28 List of paths to btrfs filesystems to regularily call <command>btrfs scrub</command> on.
29 Defaults to all mount points with btrfs filesystems.
30 If you mount a filesystem multiple times or additionally mount subvolumes,
31 you need to manually specify this list to avoid scrubbing multiple times.
32 '';
33 };
34
35 interval = mkOption {
36 default = "monthly";
37 type = types.str;
38 example = "weekly";
39 description = ''
40 Systemd calendar expression for when to scrub btrfs filesystems.
41 The recommended period is a month but could be less
42 (<citerefentry><refentrytitle>btrfs-scrub</refentrytitle>
43 <manvolnum>8</manvolnum></citerefentry>).
44 See
45 <citerefentry><refentrytitle>systemd.time</refentrytitle>
46 <manvolnum>7</manvolnum></citerefentry>
47 for more information on the syntax.
48 '';
49 };
50
51 };
52 };
53
54 config = mkMerge [
55 (mkIf enableBtrfs {
56 system.fsPackages = [ pkgs.btrfs-progs ];
57
58 boot.initrd.kernelModules = mkIf inInitrd [ "btrfs" ];
59 boot.initrd.availableKernelModules = mkIf inInitrd (
60 [ "crc32c" ]
61 ++ optionals (config.boot.kernelPackages.kernel.kernelAtLeast "5.5") [
62 # Needed for mounting filesystems with new checksums
63 "xxhash_generic"
64 "blake2b_generic"
65 "sha256_generic" # Should be baked into our kernel, just to be sure
66 ]
67 );
68
69 boot.initrd.extraUtilsCommands = mkIf inInitrd
70 ''
71 copy_bin_and_libs ${pkgs.btrfs-progs}/bin/btrfs
72 ln -sv btrfs $out/bin/btrfsck
73 ln -sv btrfsck $out/bin/fsck.btrfs
74 '';
75
76 boot.initrd.extraUtilsCommandsTest = mkIf inInitrd
77 ''
78 $out/bin/btrfs --version
79 '';
80
81 boot.initrd.postDeviceCommands = mkIf inInitrd
82 ''
83 btrfs device scan
84 '';
85 })
86
87 (mkIf enableAutoScrub {
88 assertions = [
89 {
90 assertion = cfgScrub.enable -> (cfgScrub.fileSystems != []);
91 message = ''
92 If 'services.btrfs.autoScrub' is enabled, you need to have at least one
93 btrfs file system mounted via 'fileSystems' or specify a list manually
94 in 'services.btrfs.autoScrub.fileSystems'.
95 '';
96 }
97 ];
98
99 # This will yield duplicated units if the user mounts a filesystem multiple times
100 # or additionally mounts subvolumes, but going the other way around via devices would
101 # yield duplicated units when a filesystem spans multiple devices.
102 # This way around seems like the more sensible default.
103 services.btrfs.autoScrub.fileSystems = mkDefault (mapAttrsToList (name: fs: fs.mountPoint)
104 (filterAttrs (name: fs: fs.fsType == "btrfs") config.fileSystems));
105
106 # TODO: Did not manage to do it via the usual btrfs-scrub@.timer/.service
107 # template units due to problems enabling the parameterized units,
108 # so settled with many units and templating via nix for now.
109 # https://github.com/NixOS/nixpkgs/pull/32496#discussion_r156527544
110 systemd.timers = let
111 scrubTimer = fs: let
112 fs' = utils.escapeSystemdPath fs;
113 in nameValuePair "btrfs-scrub-${fs'}" {
114 description = "regular btrfs scrub timer on ${fs}";
115
116 wantedBy = [ "timers.target" ];
117 timerConfig = {
118 OnCalendar = cfgScrub.interval;
119 AccuracySec = "1d";
120 Persistent = true;
121 };
122 };
123 in listToAttrs (map scrubTimer cfgScrub.fileSystems);
124
125 systemd.services = let
126 scrubService = fs: let
127 fs' = utils.escapeSystemdPath fs;
128 in nameValuePair "btrfs-scrub-${fs'}" {
129 description = "btrfs scrub on ${fs}";
130 # scrub prevents suspend2ram or proper shutdown
131 conflicts = [ "shutdown.target" "sleep.target" ];
132 before = [ "shutdown.target" "sleep.target" ];
133
134 serviceConfig = {
135 # simple and not oneshot, otherwise ExecStop is not used
136 Type = "simple";
137 Nice = 19;
138 IOSchedulingClass = "idle";
139 ExecStart = "${pkgs.btrfs-progs}/bin/btrfs scrub start -B ${fs}";
140 # if the service is stopped before scrub end, cancel it
141 ExecStop = pkgs.writeShellScript "btrfs-scrub-maybe-cancel" ''
142 (${pkgs.btrfs-progs}/bin/btrfs scrub status ${fs} | ${pkgs.gnugrep}/bin/grep finished) || ${pkgs.btrfs-progs}/bin/btrfs scrub cancel ${fs}
143 '';
144 };
145 };
146 in listToAttrs (map scrubService cfgScrub.fileSystems);
147 })
148 ];
149}