1{
2 config,
3 lib,
4 pkgs,
5 utils,
6 ...
7}:
8
9let
10 inherit (lib)
11 mkEnableOption
12 mkOption
13 types
14 mkMerge
15 mkIf
16 optionals
17 mkDefault
18 nameValuePair
19 listToAttrs
20 filterAttrs
21 mapAttrsToList
22 foldl'
23 ;
24
25 inInitrd = config.boot.initrd.supportedFilesystems.btrfs or false;
26 inSystem = config.boot.supportedFilesystems.btrfs or false;
27
28 cfgScrub = config.services.btrfs.autoScrub;
29
30 enableAutoScrub = cfgScrub.enable;
31 enableBtrfs = inInitrd || inSystem || enableAutoScrub;
32
33in
34
35{
36 options = {
37 # One could also do regular btrfs balances, but that shouldn't be necessary
38 # during normal usage and as long as the filesystems aren't filled near capacity
39 services.btrfs.autoScrub = {
40 enable = mkEnableOption "regular btrfs scrub";
41
42 fileSystems = mkOption {
43 type = types.listOf types.path;
44 example = [ "/" ];
45 description = ''
46 List of paths to btrfs filesystems to regularly call {command}`btrfs scrub` on.
47 Defaults to all mount points with btrfs filesystems.
48 Note that if you have filesystems that span multiple devices (e.g. RAID), you should
49 take care to use the same device for any given mount point and let btrfs take care
50 of automatically mounting the rest, in order to avoid scrubbing the same data multiple times.
51 '';
52 };
53
54 interval = mkOption {
55 default = "monthly";
56 type = types.str;
57 example = "weekly";
58 description = ''
59 Systemd calendar expression for when to scrub btrfs filesystems.
60 The recommended period is a month but could be less
61 ({manpage}`btrfs-scrub(8)`).
62 See
63 {manpage}`systemd.time(7)`
64 for more information on the syntax.
65 '';
66 };
67
68 };
69 };
70
71 config = mkMerge [
72 (mkIf enableBtrfs {
73 system.fsPackages = [ pkgs.btrfs-progs ];
74 })
75
76 (mkIf inInitrd {
77 boot.initrd.kernelModules = [ "btrfs" ];
78 boot.initrd.availableKernelModules = [
79 "crc32c"
80 ]
81 ++ optionals (config.boot.kernelPackages.kernel.kernelAtLeast "5.5") [
82 # Needed for mounting filesystems with new checksums
83 "xxhash_generic"
84 "blake2b_generic"
85
86 # `sha256` is always available, whereas `sha256_generic` is not available from 6.17 onwards
87 "sha256" # Should be baked into our kernel, just to be sure
88 ];
89
90 boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) ''
91 copy_bin_and_libs ${pkgs.btrfs-progs}/bin/btrfs
92 ln -sv btrfs $out/bin/btrfsck
93 ln -sv btrfsck $out/bin/fsck.btrfs
94 '';
95
96 boot.initrd.extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) ''
97 $out/bin/btrfs --version
98 '';
99
100 boot.initrd.postDeviceCommands = mkIf (!config.boot.initrd.systemd.enable) ''
101 btrfs device scan
102 '';
103
104 boot.initrd.systemd.initrdBin = [ pkgs.btrfs-progs ];
105 })
106
107 (mkIf enableAutoScrub {
108 assertions = [
109 {
110 assertion = cfgScrub.enable -> (cfgScrub.fileSystems != [ ]);
111 message = ''
112 If 'services.btrfs.autoScrub' is enabled, you need to have at least one
113 btrfs file system mounted via 'fileSystems' or specify a list manually
114 in 'services.btrfs.autoScrub.fileSystems'.
115 '';
116 }
117 ];
118
119 # This will remove duplicated units from either having a filesystem mounted multiple
120 # time, or additionally mounted subvolumes, as well as having a filesystem span
121 # multiple devices (provided the same device is used to mount said filesystem).
122 services.btrfs.autoScrub.fileSystems =
123 let
124 isDeviceInList = list: device: builtins.filter (e: e.device == device) list != [ ];
125
126 uniqueDeviceList = foldl' (acc: e: if isDeviceInList acc e.device then acc else acc ++ [ e ]) [ ];
127 in
128 mkDefault (
129 map (e: e.mountPoint) (
130 uniqueDeviceList (
131 mapAttrsToList (name: fs: {
132 mountPoint = fs.mountPoint;
133 device = fs.device;
134 }) (filterAttrs (name: fs: fs.fsType == "btrfs") config.fileSystems)
135 )
136 )
137 );
138
139 # TODO: Did not manage to do it via the usual btrfs-scrub@.timer/.service
140 # template units due to problems enabling the parameterized units,
141 # so settled with many units and templating via nix for now.
142 # https://github.com/NixOS/nixpkgs/pull/32496#discussion_r156527544
143 systemd.timers =
144 let
145 scrubTimer =
146 fs:
147 let
148 fs' = utils.escapeSystemdPath fs;
149 in
150 nameValuePair "btrfs-scrub-${fs'}" {
151 description = "regular btrfs scrub timer on ${fs}";
152
153 wantedBy = [ "timers.target" ];
154 timerConfig = {
155 OnCalendar = cfgScrub.interval;
156 AccuracySec = "1d";
157 Persistent = true;
158 };
159 };
160 in
161 listToAttrs (map scrubTimer cfgScrub.fileSystems);
162
163 systemd.services =
164 let
165 scrubService =
166 fs:
167 let
168 fs' = utils.escapeSystemdPath fs;
169 in
170 nameValuePair "btrfs-scrub-${fs'}" {
171 description = "btrfs scrub on ${fs}";
172 documentation = [ "man:btrfs-scrub(8)" ];
173 # scrub prevents suspend2ram or proper shutdown
174 conflicts = [
175 "shutdown.target"
176 "sleep.target"
177 ];
178 before = [
179 "shutdown.target"
180 "sleep.target"
181 ];
182
183 serviceConfig = {
184 # simple and not oneshot, otherwise ExecStop is not used
185 Type = "simple";
186 Nice = 19;
187 IOSchedulingClass = "idle";
188 ExecStart = "${pkgs.btrfs-progs}/bin/btrfs scrub start -B ${fs}";
189 # if the service is stopped before scrub end, cancel it
190 ExecStop = pkgs.writeShellScript "btrfs-scrub-maybe-cancel" ''
191 (${pkgs.btrfs-progs}/bin/btrfs scrub status ${fs} | ${pkgs.gnugrep}/bin/grep finished) || ${pkgs.btrfs-progs}/bin/btrfs scrub cancel ${fs}
192 '';
193 };
194 };
195 in
196 listToAttrs (map scrubService cfgScrub.fileSystems);
197 })
198 ];
199}