at 23.11-pre 16 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4with types; 5 6let 7 8 planDescription = '' 9 The znapzend backup plan to use for the source. 10 11 The plan specifies how often to backup and for how long to keep the 12 backups. It consists of a series of retention periods to interval 13 associations: 14 15 ``` 16 retA=>intA,retB=>intB,... 17 ``` 18 19 Both intervals and retention periods are expressed in standard units 20 of time or multiples of them. You can use both the full name or a 21 shortcut according to the following listing: 22 23 ``` 24 second|sec|s, minute|min, hour|h, day|d, week|w, month|mon|m, year|y 25 ``` 26 27 See {manpage}`znapzendzetup(1)` for more info. 28 ''; 29 planExample = "1h=>10min,1d=>1h,1w=>1d,1m=>1w,1y=>1m"; 30 31 # A type for a string of the form number{b|k|M|G} 32 mbufferSizeType = str // { 33 check = x: str.check x && builtins.isList (builtins.match "^[0-9]+[bkMG]$" x); 34 description = "string of the form number{b|k|M|G}"; 35 }; 36 37 enabledFeatures = concatLists (mapAttrsToList (name: enabled: optional enabled name) cfg.features); 38 39 # Type for a string that must contain certain other strings (the list parameter). 40 # Note that these would need regex escaping. 41 stringContainingStrings = list: let 42 matching = s: map (str: builtins.match ".*${str}.*" s) list; 43 in str // { 44 check = x: str.check x && all isList (matching x); 45 description = "string containing all of the characters ${concatStringsSep ", " list}"; 46 }; 47 48 timestampType = stringContainingStrings [ "%Y" "%m" "%d" "%H" "%M" "%S" ]; 49 50 destType = srcConfig: submodule ({ name, ... }: { 51 options = { 52 53 label = mkOption { 54 type = str; 55 description = lib.mdDoc "Label for this destination. Defaults to the attribute name."; 56 }; 57 58 plan = mkOption { 59 type = str; 60 description = lib.mdDoc planDescription; 61 example = planExample; 62 }; 63 64 dataset = mkOption { 65 type = str; 66 description = lib.mdDoc "Dataset name to send snapshots to."; 67 example = "tank/main"; 68 }; 69 70 host = mkOption { 71 type = nullOr str; 72 description = lib.mdDoc '' 73 Host to use for the destination dataset. Can be prefixed with 74 `user@` to specify the ssh user. 75 ''; 76 default = null; 77 example = "john@example.com"; 78 }; 79 80 presend = mkOption { 81 type = nullOr str; 82 description = lib.mdDoc '' 83 Command to run before sending the snapshot to the destination. 84 Intended to run a remote script via {command}`ssh` on the 85 destination, e.g. to bring up a backup disk or server or to put a 86 zpool online/offline. See also {option}`postsend`. 87 ''; 88 default = null; 89 example = "ssh root@bserv zpool import -Nf tank"; 90 }; 91 92 postsend = mkOption { 93 type = nullOr str; 94 description = lib.mdDoc '' 95 Command to run after sending the snapshot to the destination. 96 Intended to run a remote script via {command}`ssh` on the 97 destination, e.g. to bring up a backup disk or server or to put a 98 zpool online/offline. See also {option}`presend`. 99 ''; 100 default = null; 101 example = "ssh root@bserv zpool export tank"; 102 }; 103 }; 104 105 config = { 106 label = mkDefault name; 107 plan = mkDefault srcConfig.plan; 108 }; 109 }); 110 111 112 113 srcType = submodule ({ name, config, ... }: { 114 options = { 115 116 enable = mkOption { 117 type = bool; 118 description = lib.mdDoc "Whether to enable this source."; 119 default = true; 120 }; 121 122 recursive = mkOption { 123 type = bool; 124 description = lib.mdDoc "Whether to do recursive snapshots."; 125 default = false; 126 }; 127 128 mbuffer = { 129 enable = mkOption { 130 type = bool; 131 description = lib.mdDoc "Whether to use {command}`mbuffer`."; 132 default = false; 133 }; 134 135 port = mkOption { 136 type = nullOr ints.u16; 137 description = lib.mdDoc '' 138 Port to use for {command}`mbuffer`. 139 140 If this is null, it will run {command}`mbuffer` through 141 ssh. 142 143 If this is not null, it will run {command}`mbuffer` 144 directly through TCP, which is not encrypted but faster. In that 145 case the given port needs to be open on the destination host. 146 ''; 147 default = null; 148 }; 149 150 size = mkOption { 151 type = mbufferSizeType; 152 description = lib.mdDoc '' 153 The size for {command}`mbuffer`. 154 Supports the units b, k, M, G. 155 ''; 156 default = "1G"; 157 example = "128M"; 158 }; 159 }; 160 161 presnap = mkOption { 162 type = nullOr str; 163 description = lib.mdDoc '' 164 Command to run before snapshots are taken on the source dataset, 165 e.g. for database locking/flushing. See also 166 {option}`postsnap`. 167 ''; 168 default = null; 169 example = literalExpression '' 170 '''''${pkgs.mariadb}/bin/mysql -e "set autocommit=0;flush tables with read lock;\\! ''${pkgs.coreutils}/bin/sleep 600" & ''${pkgs.coreutils}/bin/echo $! > /tmp/mariadblock.pid ; sleep 10''' 171 ''; 172 }; 173 174 postsnap = mkOption { 175 type = nullOr str; 176 description = lib.mdDoc '' 177 Command to run after snapshots are taken on the source dataset, 178 e.g. for database unlocking. See also {option}`presnap`. 179 ''; 180 default = null; 181 example = literalExpression '' 182 "''${pkgs.coreutils}/bin/kill `''${pkgs.coreutils}/bin/cat /tmp/mariadblock.pid`;''${pkgs.coreutils}/bin/rm /tmp/mariadblock.pid" 183 ''; 184 }; 185 186 timestampFormat = mkOption { 187 type = timestampType; 188 description = lib.mdDoc '' 189 The timestamp format to use for constructing snapshot names. 190 The syntax is `strftime`-like. The string must 191 consist of the mandatory `%Y %m %d %H %M %S`. 192 Optionally `- _ . :` characters as well as any 193 alphanumeric character are allowed. If suffixed by a 194 `Z`, times will be in UTC. 195 ''; 196 default = "%Y-%m-%d-%H%M%S"; 197 example = "znapzend-%m.%d.%Y-%H%M%SZ"; 198 }; 199 200 sendDelay = mkOption { 201 type = int; 202 description = lib.mdDoc '' 203 Specify delay (in seconds) before sending snaps to the destination. 204 May be useful if you want to control sending time. 205 ''; 206 default = 0; 207 example = 60; 208 }; 209 210 plan = mkOption { 211 type = str; 212 description = lib.mdDoc planDescription; 213 example = planExample; 214 }; 215 216 dataset = mkOption { 217 type = str; 218 description = lib.mdDoc "The dataset to use for this source."; 219 example = "tank/home"; 220 }; 221 222 destinations = mkOption { 223 type = attrsOf (destType config); 224 description = lib.mdDoc "Additional destinations."; 225 default = {}; 226 example = literalExpression '' 227 { 228 local = { 229 dataset = "btank/backup"; 230 presend = "zpool import -N btank"; 231 postsend = "zpool export btank"; 232 }; 233 remote = { 234 host = "john@example.com"; 235 dataset = "tank/john"; 236 }; 237 }; 238 ''; 239 }; 240 }; 241 242 config = { 243 dataset = mkDefault name; 244 }; 245 246 }); 247 248 ### Generating the configuration from here 249 250 cfg = config.services.znapzend; 251 252 onOff = b: if b then "on" else "off"; 253 nullOff = b: if b == null then "off" else toString b; 254 stripSlashes = replaceStrings [ "/" ] [ "." ]; 255 256 attrsToFile = config: concatStringsSep "\n" (builtins.attrValues ( 257 mapAttrs (n: v: "${n}=${v}") config)); 258 259 mkDestAttrs = dst: with dst; 260 mapAttrs' (n: v: nameValuePair "dst_${label}${n}" v) ({ 261 "" = optionalString (host != null) "${host}:" + dataset; 262 _plan = plan; 263 } // optionalAttrs (presend != null) { 264 _precmd = presend; 265 } // optionalAttrs (postsend != null) { 266 _pstcmd = postsend; 267 }); 268 269 mkSrcAttrs = srcCfg: with srcCfg; { 270 enabled = onOff enable; 271 # mbuffer is not referenced by its full path to accommodate non-NixOS systems or differing mbuffer versions between source and target 272 mbuffer = with mbuffer; if enable then "mbuffer" 273 + optionalString (port != null) ":${toString port}" else "off"; 274 mbuffer_size = mbuffer.size; 275 post_znap_cmd = nullOff postsnap; 276 pre_znap_cmd = nullOff presnap; 277 recursive = onOff recursive; 278 src = dataset; 279 src_plan = plan; 280 tsformat = timestampFormat; 281 zend_delay = toString sendDelay; 282 } // foldr (a: b: a // b) {} ( 283 map mkDestAttrs (builtins.attrValues destinations) 284 ); 285 286 files = mapAttrs' (n: srcCfg: let 287 fileText = attrsToFile (mkSrcAttrs srcCfg); 288 in { 289 name = srcCfg.dataset; 290 value = pkgs.writeText (stripSlashes srcCfg.dataset) fileText; 291 }) cfg.zetup; 292 293in 294{ 295 options = { 296 services.znapzend = { 297 enable = mkEnableOption (lib.mdDoc "ZnapZend ZFS backup daemon"); 298 299 logLevel = mkOption { 300 default = "debug"; 301 example = "warning"; 302 type = enum ["debug" "info" "warning" "err" "alert"]; 303 description = lib.mdDoc '' 304 The log level when logging to file. Any of debug, info, warning, err, 305 alert. Default in daemonized form is debug. 306 ''; 307 }; 308 309 logTo = mkOption { 310 type = str; 311 default = "syslog::daemon"; 312 example = "/var/log/znapzend.log"; 313 description = lib.mdDoc '' 314 Where to log to (syslog::\<facility\> or \<filepath\>). 315 ''; 316 }; 317 318 noDestroy = mkOption { 319 type = bool; 320 default = false; 321 description = lib.mdDoc "Does all changes to the filesystem except destroy."; 322 }; 323 324 autoCreation = mkOption { 325 type = bool; 326 default = false; 327 description = lib.mdDoc "Automatically create the destination dataset if it does not exist."; 328 }; 329 330 zetup = mkOption { 331 type = attrsOf srcType; 332 description = lib.mdDoc "Znapzend configuration."; 333 default = {}; 334 example = literalExpression '' 335 { 336 "tank/home" = { 337 # Make snapshots of tank/home every hour, keep those for 1 day, 338 # keep every days snapshot for 1 month, etc. 339 plan = "1d=>1h,1m=>1d,1y=>1m"; 340 recursive = true; 341 # Send all those snapshots to john@example.com:rtank/john as well 342 destinations.remote = { 343 host = "john@example.com"; 344 dataset = "rtank/john"; 345 }; 346 }; 347 }; 348 ''; 349 }; 350 351 pure = mkOption { 352 type = bool; 353 description = lib.mdDoc '' 354 Do not persist any stateful znapzend setups. If this option is 355 enabled, your previously set znapzend setups will be cleared and only 356 the ones defined with this module will be applied. 357 ''; 358 default = false; 359 }; 360 361 features.oracleMode = mkEnableOption (lib.mdDoc '' 362 Destroy snapshots one by one instead of using one long argument list. 363 If source and destination are out of sync for a long time, you may have 364 so many snapshots to destroy that the argument gets is too long and the 365 command fails. 366 ''); 367 features.recvu = mkEnableOption (lib.mdDoc '' 368 recvu feature which uses `-u` on the receiving end to keep the destination 369 filesystem unmounted. 370 ''); 371 features.compressed = mkEnableOption (lib.mdDoc '' 372 compressed feature which adds the options `-Lce` to 373 the {command}`zfs send` command. When this is enabled, make 374 sure that both the sending and receiving pool have the same relevant 375 features enabled. Using `-c` will skip unnecessary 376 decompress-compress stages, `-L` is for large block 377 support and -e is for embedded data support. see 378 {manpage}`znapzend(1)` 379 and {manpage}`zfs(8)` 380 for more info. 381 ''); 382 features.sendRaw = mkEnableOption (lib.mdDoc '' 383 sendRaw feature which adds the options `-w` to the 384 {command}`zfs send` command. For encrypted source datasets this 385 instructs zfs not to decrypt before sending which results in a remote 386 backup that can't be read without the encryption key/passphrase, useful 387 when the remote isn't fully trusted or not physically secure. This 388 option must be used consistently, raw incrementals cannot be based on 389 non-raw snapshots and vice versa. 390 ''); 391 features.skipIntermediates = mkEnableOption (lib.mdDoc '' 392 Enable the skipIntermediates feature to send a single increment 393 between latest common snapshot and the newly made one. It may skip 394 several source snaps if the destination was offline for some time, and 395 it should skip snapshots not managed by znapzend. Normally for online 396 destinations, the new snapshot is sent as soon as it is created on the 397 source, so there are no automatic increments to skip. 398 ''); 399 features.lowmemRecurse = mkEnableOption (lib.mdDoc '' 400 use lowmemRecurse on systems where you have too many datasets, so a 401 recursive listing of attributes to find backup plans exhausts the 402 memory available to {command}`znapzend`: instead, go the slower 403 way to first list all impacted dataset names, and then query their 404 configs one by one. 405 ''); 406 features.zfsGetType = mkEnableOption (lib.mdDoc '' 407 use zfsGetType if your {command}`zfs get` supports a 408 `-t` argument for filtering by dataset type at all AND 409 lists properties for snapshots by default when recursing, so that there 410 is too much data to process while searching for backup plans. 411 If these two conditions apply to your system, the time needed for a 412 `--recursive` search for backup plans can literally 413 differ by hundreds of times (depending on the amount of snapshots in 414 that dataset tree... and a decent backup plan will ensure you have a lot 415 of those), so you would benefit from requesting this feature. 416 ''); 417 }; 418 }; 419 420 config = mkIf cfg.enable { 421 environment.systemPackages = [ pkgs.znapzend ]; 422 423 systemd.services = { 424 znapzend = { 425 description = "ZnapZend - ZFS Backup System"; 426 wantedBy = [ "zfs.target" ]; 427 after = [ "zfs.target" ]; 428 429 path = with pkgs; [ zfs mbuffer openssh ]; 430 431 preStart = optionalString cfg.pure '' 432 echo Resetting znapzend zetups 433 ${pkgs.znapzend}/bin/znapzendzetup list \ 434 | grep -oP '(?<=\*\*\* backup plan: ).*(?= \*\*\*)' \ 435 | xargs -I{} ${pkgs.znapzend}/bin/znapzendzetup delete "{}" 436 '' + concatStringsSep "\n" (mapAttrsToList (dataset: config: '' 437 echo Importing znapzend zetup ${config} for dataset ${dataset} 438 ${pkgs.znapzend}/bin/znapzendzetup import --write ${dataset} ${config} & 439 '') files) + '' 440 wait 441 ''; 442 443 serviceConfig = { 444 # znapzendzetup --import apparently tries to connect to the backup 445 # host 3 times with a timeout of 30 seconds, leading to a startup 446 # delay of >90s when the host is down, which is just above the default 447 # service timeout of 90 seconds. Increase the timeout so it doesn't 448 # make the service fail in that case. 449 TimeoutStartSec = 180; 450 # Needs to have write access to ZFS 451 User = "root"; 452 ExecStart = let 453 args = concatStringsSep " " [ 454 "--logto=${cfg.logTo}" 455 "--loglevel=${cfg.logLevel}" 456 (optionalString cfg.noDestroy "--nodestroy") 457 (optionalString cfg.autoCreation "--autoCreation") 458 (optionalString (enabledFeatures != []) 459 "--features=${concatStringsSep "," enabledFeatures}") 460 ]; in "${pkgs.znapzend}/bin/znapzend ${args}"; 461 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; 462 Restart = "on-failure"; 463 }; 464 }; 465 }; 466 }; 467 468 meta.maintainers = with maintainers; [ infinisil SlothOfAnarchy ]; 469}