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 "Enable 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" "crc32c" ];
59
60 boot.initrd.extraUtilsCommands = mkIf inInitrd
61 ''
62 copy_bin_and_libs ${pkgs.btrfs-progs}/bin/btrfs
63 ln -sv btrfs $out/bin/btrfsck
64 ln -sv btrfsck $out/bin/fsck.btrfs
65 '';
66
67 boot.initrd.extraUtilsCommandsTest = mkIf inInitrd
68 ''
69 $out/bin/btrfs --version
70 '';
71
72 boot.initrd.postDeviceCommands = mkIf inInitrd
73 ''
74 btrfs device scan
75 '';
76 })
77
78 (mkIf enableAutoScrub {
79 assertions = [
80 {
81 assertion = cfgScrub.enable -> (cfgScrub.fileSystems != []);
82 message = ''
83 If 'services.btrfs.autoScrub' is enabled, you need to have at least one
84 btrfs file system mounted via 'fileSystems' or specify a list manually
85 in 'services.btrfs.autoScrub.fileSystems'.
86 '';
87 }
88 ];
89
90 # This will yield duplicated units if the user mounts a filesystem multiple times
91 # or additionally mounts subvolumes, but going the other way around via devices would
92 # yield duplicated units when a filesystem spans multiple devices.
93 # This way around seems like the more sensible default.
94 services.btrfs.autoScrub.fileSystems = mkDefault (mapAttrsToList (name: fs: fs.mountPoint)
95 (filterAttrs (name: fs: fs.fsType == "btrfs") config.fileSystems));
96
97 # TODO: Did not manage to do it via the usual btrfs-scrub@.timer/.service
98 # template units due to problems enabling the parameterized units,
99 # so settled with many units and templating via nix for now.
100 # https://github.com/NixOS/nixpkgs/pull/32496#discussion_r156527544
101 systemd.timers = let
102 scrubTimer = fs: let
103 fs' = utils.escapeSystemdPath fs;
104 in nameValuePair "btrfs-scrub-${fs'}" {
105 description = "regular btrfs scrub timer on ${fs}";
106
107 wantedBy = [ "timers.target" ];
108 timerConfig = {
109 OnCalendar = cfgScrub.interval;
110 AccuracySec = "1d";
111 Persistent = true;
112 };
113 };
114 in listToAttrs (map scrubTimer cfgScrub.fileSystems);
115
116 systemd.services = let
117 scrubService = fs: let
118 fs' = utils.escapeSystemdPath fs;
119 in nameValuePair "btrfs-scrub-${fs'}" {
120 description = "btrfs scrub on ${fs}";
121
122 serviceConfig = {
123 Type = "oneshot";
124 Nice = 19;
125 IOSchedulingClass = "idle";
126 ExecStart = "${pkgs.btrfs-progs}/bin/btrfs scrub start -B ${fs}";
127 };
128 };
129 in listToAttrs (map scrubService cfgScrub.fileSystems);
130 })
131 ];
132}