at 22.05-pre 8.1 kB view raw
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}