1{ config, pkgs, lib, ... }:
2let
3 cfg = config.services.btrbk;
4 sshEnabled = cfg.sshAccess != [ ];
5 serviceEnabled = cfg.instances != { };
6 attr2Lines = attr:
7 let
8 pairs = lib.attrsets.mapAttrsToList (name: value: { inherit name value; }) attr;
9 isSubsection = value:
10 if builtins.isAttrs value then true
11 else if builtins.isString value then false
12 else throw "invalid type in btrbk config ${builtins.typeOf value}";
13 sortedPairs = lib.lists.partition (x: isSubsection x.value) pairs;
14 in
15 lib.flatten (
16 # non subsections go first
17 (
18 map (pair: [ "${pair.name} ${pair.value}" ]) sortedPairs.wrong
19 )
20 ++ # subsections go last
21 (
22 map
23 (
24 pair:
25 lib.mapAttrsToList
26 (
27 childname: value:
28 [ "${pair.name} ${childname}" ] ++ (map (x: " " + x) (attr2Lines value))
29 )
30 pair.value
31 )
32 sortedPairs.right
33 )
34 )
35 ;
36 addDefaults = settings: { backend = "btrfs-progs-sudo"; } // settings;
37 mkConfigFile = settings: lib.concatStringsSep "\n" (attr2Lines (addDefaults settings));
38 mkTestedConfigFile = name: settings:
39 let
40 configFile = pkgs.writeText "btrbk-${name}.conf" (mkConfigFile settings);
41 in
42 pkgs.runCommand "btrbk-${name}-tested.conf" { } ''
43 mkdir foo
44 cp ${configFile} $out
45 if (set +o pipefail; ${pkgs.btrbk}/bin/btrbk -c $out ls foo 2>&1 | grep $out);
46 then
47 echo btrbk configuration is invalid
48 cat $out
49 exit 1
50 fi;
51 '';
52in
53{
54 options = {
55 services.btrbk = {
56 extraPackages = lib.mkOption {
57 description = "Extra packages for btrbk, like compression utilities for <literal>stream_compress</literal>";
58 type = lib.types.listOf lib.types.package;
59 default = [ ];
60 example = lib.literalExpression "[ pkgs.xz ]";
61 };
62 niceness = lib.mkOption {
63 description = "Niceness for local instances of btrbk. Also applies to remote ones connecting via ssh when positive.";
64 type = lib.types.ints.between (-20) 19;
65 default = 10;
66 };
67 ioSchedulingClass = lib.mkOption {
68 description = "IO scheduling class for btrbk (see ionice(1) for a quick description). Applies to local instances, and remote ones connecting by ssh if set to idle.";
69 type = lib.types.enum [ "idle" "best-effort" "realtime" ];
70 default = "best-effort";
71 };
72 instances = lib.mkOption {
73 description = "Set of btrbk instances. The instance named <literal>btrbk</literal> is the default one.";
74 type = with lib.types;
75 attrsOf (
76 submodule {
77 options = {
78 onCalendar = lib.mkOption {
79 type = lib.types.str;
80 default = "daily";
81 description = "How often this btrbk instance is started. See systemd.time(7) for more information about the format.";
82 };
83 settings = lib.mkOption {
84 type = let t = lib.types.attrsOf (lib.types.either lib.types.str (t // { description = "instances of this type recursively"; })); in t;
85 default = { };
86 example = {
87 snapshot_preserve_min = "2d";
88 snapshot_preserve = "14d";
89 volume = {
90 "/mnt/btr_pool" = {
91 target = "/mnt/btr_backup/mylaptop";
92 subvolume = {
93 "rootfs" = { };
94 "home" = { snapshot_create = "always"; };
95 };
96 };
97 };
98 };
99 description = "configuration options for btrbk. Nested attrsets translate to subsections.";
100 };
101 };
102 }
103 );
104 default = { };
105 };
106 sshAccess = lib.mkOption {
107 description = "SSH keys that should be able to make or push snapshots on this system remotely with btrbk";
108 type = with lib.types; listOf (
109 submodule {
110 options = {
111 key = lib.mkOption {
112 type = str;
113 description = "SSH public key allowed to login as user <literal>btrbk</literal> to run remote backups.";
114 };
115 roles = lib.mkOption {
116 type = listOf (enum [ "info" "source" "target" "delete" "snapshot" "send" "receive" ]);
117 example = [ "source" "info" "send" ];
118 description = "What actions can be performed with this SSH key. See ssh_filter_btrbk(1) for details";
119 };
120 };
121 }
122 );
123 default = [ ];
124 };
125 };
126
127 };
128 config = lib.mkIf (sshEnabled || serviceEnabled) {
129 environment.systemPackages = [ pkgs.btrbk ] ++ cfg.extraPackages;
130 security.sudo.extraRules = [
131 {
132 users = [ "btrbk" ];
133 commands = [
134 { command = "${pkgs.btrfs-progs}/bin/btrfs"; options = [ "NOPASSWD" ]; }
135 { command = "${pkgs.coreutils}/bin/mkdir"; options = [ "NOPASSWD" ]; }
136 { command = "${pkgs.coreutils}/bin/readlink"; options = [ "NOPASSWD" ]; }
137 # for ssh, they are not the same than the one hard coded in ${pkgs.btrbk}
138 { command = "/run/current-system/bin/btrfs"; options = [ "NOPASSWD" ]; }
139 { command = "/run/current-system/sw/bin/mkdir"; options = [ "NOPASSWD" ]; }
140 { command = "/run/current-system/sw/bin/readlink"; options = [ "NOPASSWD" ]; }
141 ];
142 }
143 ];
144 users.users.btrbk = {
145 isSystemUser = true;
146 # ssh needs a home directory
147 home = "/var/lib/btrbk";
148 createHome = true;
149 shell = "${pkgs.bash}/bin/bash";
150 group = "btrbk";
151 openssh.authorizedKeys.keys = map
152 (
153 v:
154 let
155 options = lib.concatMapStringsSep " " (x: "--" + x) v.roles;
156 ioniceClass = {
157 "idle" = 3;
158 "best-effort" = 2;
159 "realtime" = 1;
160 }.${cfg.ioSchedulingClass};
161 in
162 ''command="${pkgs.util-linux}/bin/ionice -t -c ${toString ioniceClass} ${lib.optionalString (cfg.niceness >= 1) "${pkgs.coreutils}/bin/nice -n ${toString cfg.niceness}"} ${pkgs.btrbk}/share/btrbk/scripts/ssh_filter_btrbk.sh --sudo ${options}" ${v.key}''
163 )
164 cfg.sshAccess;
165 };
166 users.groups.btrbk = { };
167 systemd.tmpfiles.rules = [
168 "d /var/lib/btrbk 0750 btrbk btrbk"
169 "d /var/lib/btrbk/.ssh 0700 btrbk btrbk"
170 "f /var/lib/btrbk/.ssh/config 0700 btrbk btrbk - StrictHostKeyChecking=accept-new"
171 ];
172 environment.etc = lib.mapAttrs'
173 (
174 name: instance: {
175 name = "btrbk/${name}.conf";
176 value.source = mkTestedConfigFile name instance.settings;
177 }
178 )
179 cfg.instances;
180 systemd.services = lib.mapAttrs'
181 (
182 name: _: {
183 name = "btrbk-${name}";
184 value = {
185 description = "Takes BTRFS snapshots and maintains retention policies.";
186 unitConfig.Documentation = "man:btrbk(1)";
187 path = [ "/run/wrappers" ] ++ cfg.extraPackages;
188 serviceConfig = {
189 User = "btrbk";
190 Group = "btrbk";
191 Type = "oneshot";
192 ExecStart = "${pkgs.btrbk}/bin/btrbk -c /etc/btrbk/${name}.conf run";
193 Nice = cfg.niceness;
194 IOSchedulingClass = cfg.ioSchedulingClass;
195 StateDirectory = "btrbk";
196 };
197 };
198 }
199 )
200 cfg.instances;
201
202 systemd.timers = lib.mapAttrs'
203 (
204 name: instance: {
205 name = "btrbk-${name}";
206 value = {
207 description = "Timer to take BTRFS snapshots and maintain retention policies.";
208 wantedBy = [ "timers.target" ];
209 timerConfig = {
210 OnCalendar = instance.onCalendar;
211 AccuracySec = "10min";
212 Persistent = true;
213 };
214 };
215 }
216 )
217 cfg.instances;
218 };
219
220}