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 ++ optionals (config.boot.kernelPackages.kernel.kernelAtLeast "5.5") [
81 # Needed for mounting filesystems with new checksums
82 "xxhash_generic"
83 "blake2b_generic"
84 "sha256_generic" # Should be baked into our kernel, just to be sure
85 ];
86
87 boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) ''
88 copy_bin_and_libs ${pkgs.btrfs-progs}/bin/btrfs
89 ln -sv btrfs $out/bin/btrfsck
90 ln -sv btrfsck $out/bin/fsck.btrfs
91 '';
92
93 boot.initrd.extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) ''
94 $out/bin/btrfs --version
95 '';
96
97 boot.initrd.postDeviceCommands = mkIf (!config.boot.initrd.systemd.enable) ''
98 btrfs device scan
99 '';
100
101 boot.initrd.systemd.initrdBin = [ pkgs.btrfs-progs ];
102 })
103
104 (mkIf enableAutoScrub {
105 assertions = [
106 {
107 assertion = cfgScrub.enable -> (cfgScrub.fileSystems != [ ]);
108 message = ''
109 If 'services.btrfs.autoScrub' is enabled, you need to have at least one
110 btrfs file system mounted via 'fileSystems' or specify a list manually
111 in 'services.btrfs.autoScrub.fileSystems'.
112 '';
113 }
114 ];
115
116 # This will remove duplicated units from either having a filesystem mounted multiple
117 # time, or additionally mounted subvolumes, as well as having a filesystem span
118 # multiple devices (provided the same device is used to mount said filesystem).
119 services.btrfs.autoScrub.fileSystems =
120 let
121 isDeviceInList = list: device: builtins.filter (e: e.device == device) list != [ ];
122
123 uniqueDeviceList = foldl' (acc: e: if isDeviceInList acc e.device then acc else acc ++ [ e ]) [ ];
124 in
125 mkDefault (
126 map (e: e.mountPoint) (
127 uniqueDeviceList (
128 mapAttrsToList (name: fs: {
129 mountPoint = fs.mountPoint;
130 device = fs.device;
131 }) (filterAttrs (name: fs: fs.fsType == "btrfs") config.fileSystems)
132 )
133 )
134 );
135
136 # TODO: Did not manage to do it via the usual btrfs-scrub@.timer/.service
137 # template units due to problems enabling the parameterized units,
138 # so settled with many units and templating via nix for now.
139 # https://github.com/NixOS/nixpkgs/pull/32496#discussion_r156527544
140 systemd.timers =
141 let
142 scrubTimer =
143 fs:
144 let
145 fs' = utils.escapeSystemdPath fs;
146 in
147 nameValuePair "btrfs-scrub-${fs'}" {
148 description = "regular btrfs scrub timer on ${fs}";
149
150 wantedBy = [ "timers.target" ];
151 timerConfig = {
152 OnCalendar = cfgScrub.interval;
153 AccuracySec = "1d";
154 Persistent = true;
155 };
156 };
157 in
158 listToAttrs (map scrubTimer cfgScrub.fileSystems);
159
160 systemd.services =
161 let
162 scrubService =
163 fs:
164 let
165 fs' = utils.escapeSystemdPath fs;
166 in
167 nameValuePair "btrfs-scrub-${fs'}" {
168 description = "btrfs scrub on ${fs}";
169 documentation = [ "man:btrfs-scrub(8)" ];
170 # scrub prevents suspend2ram or proper shutdown
171 conflicts = [
172 "shutdown.target"
173 "sleep.target"
174 ];
175 before = [
176 "shutdown.target"
177 "sleep.target"
178 ];
179
180 serviceConfig = {
181 # simple and not oneshot, otherwise ExecStop is not used
182 Type = "simple";
183 Nice = 19;
184 IOSchedulingClass = "idle";
185 ExecStart = "${pkgs.btrfs-progs}/bin/btrfs scrub start -B ${fs}";
186 # if the service is stopped before scrub end, cancel it
187 ExecStop = pkgs.writeShellScript "btrfs-scrub-maybe-cancel" ''
188 (${pkgs.btrfs-progs}/bin/btrfs scrub status ${fs} | ${pkgs.gnugrep}/bin/grep finished) || ${pkgs.btrfs-progs}/bin/btrfs scrub cancel ${fs}
189 '';
190 };
191 };
192 in
193 listToAttrs (map scrubService cfgScrub.fileSystems);
194 })
195 ];
196}