Clone of https://github.com/NixOS/nixpkgs.git (to stress-test knotserver)

Merge pull request #32914 from Infinisil/znapzendzetup

znapzend service: stateless setup

authored by Orivej Desh and committed by GitHub 54d01b0e ff555bda

+360 -9
+360 -9
nixos/modules/services/backup/znapzend.nix
··· 1 1 { config, lib, pkgs, ... }: 2 2 3 3 with lib; 4 + with types; 4 5 5 6 let 7 + 8 + # Converts a plan like 9 + # { "1d" = "1h"; "1w" = "1d"; } 10 + # into 11 + # "1d=>1h,1w=>1d" 12 + attrToPlan = attrs: concatStringsSep "," (builtins.attrValues ( 13 + mapAttrs (n: v: "${n}=>${v}") attrs)); 14 + 15 + planDescription = '' 16 + The znapzend backup plan to use for the source. 17 + </para> 18 + <para> 19 + The plan specifies how often to backup and for how long to keep the 20 + backups. It consists of a series of retention periodes to interval 21 + associations: 22 + </para> 23 + <para> 24 + <literal> 25 + retA=>intA,retB=>intB,... 26 + </literal> 27 + </para> 28 + <para> 29 + Both intervals and retention periods are expressed in standard units 30 + of time or multiples of them. You can use both the full name or a 31 + shortcut according to the following listing: 32 + </para> 33 + <para> 34 + <literal> 35 + second|sec|s, minute|min, hour|h, day|d, week|w, month|mon|m, year|y 36 + </literal> 37 + </para> 38 + <para> 39 + See <citerefentry><refentrytitle>znapzendzetup</refentrytitle><manvolnum>1</manvolnum></citerefentry> for more info. 40 + ''; 41 + planExample = "1h=>10min,1d=>1h,1w=>1d,1m=>1w,1y=>1m"; 42 + 43 + # A type for a string of the form number{b|k|M|G} 44 + mbufferSizeType = str // { 45 + check = x: str.check x && builtins.isList (builtins.match "^[0-9]+[bkMG]$" x); 46 + description = "string of the form number{b|k|M|G}"; 47 + }; 48 + 49 + # Type for a string that must contain certain other strings (the list parameter). 50 + # Note that these would need regex escaping. 51 + stringContainingStrings = list: let 52 + matching = s: map (str: builtins.match ".*${str}.*" s) list; 53 + in str // { 54 + check = x: str.check x && all isList (matching x); 55 + description = "string containing all of the characters ${concatStringsSep ", " list}"; 56 + }; 57 + 58 + timestampType = stringContainingStrings [ "%Y" "%m" "%d" "%H" "%M" "%S" ]; 59 + 60 + destType = srcConfig: submodule ({ name, ... }: { 61 + options = { 62 + 63 + label = mkOption { 64 + type = str; 65 + description = "Label for this destination. Defaults to the attribute name."; 66 + }; 67 + 68 + plan = mkOption { 69 + type = str; 70 + description = planDescription; 71 + example = planExample; 72 + }; 73 + 74 + dataset = mkOption { 75 + type = str; 76 + description = "Dataset name to send snapshots to."; 77 + example = "tank/main"; 78 + }; 79 + 80 + host = mkOption { 81 + type = nullOr str; 82 + description = '' 83 + Host to use for the destination dataset. Can be prefixed with 84 + <literal>user@</literal> to specify the ssh user. 85 + ''; 86 + default = null; 87 + example = "john@example.com"; 88 + }; 89 + 90 + presend = mkOption { 91 + type = nullOr str; 92 + description = '' 93 + Command to run before sending the snapshot to the destination. 94 + Intended to run a remote script via <command>ssh</command> on the 95 + destination, e.g. to bring up a backup disk or server or to put a 96 + zpool online/offline. See also <option>postsend</option>. 97 + ''; 98 + default = null; 99 + example = "ssh root@bserv zpool import -Nf tank"; 100 + }; 101 + 102 + postsend = mkOption { 103 + type = nullOr str; 104 + description = '' 105 + Command to run after sending the snapshot to the destination. 106 + Intended to run a remote script via <command>ssh</command> on the 107 + destination, e.g. to bring up a backup disk or server or to put a 108 + zpool online/offline. See also <option>presend</option>. 109 + ''; 110 + default = null; 111 + example = "ssh root@bserv zpool export tank"; 112 + }; 113 + }; 114 + 115 + config = { 116 + label = mkDefault name; 117 + plan = mkDefault srcConfig.plan; 118 + }; 119 + }); 120 + 121 + 122 + 123 + srcType = submodule ({ name, config, ... }: { 124 + options = { 125 + 126 + enable = mkOption { 127 + type = bool; 128 + description = "Whether to enable this source."; 129 + default = true; 130 + }; 131 + 132 + recursive = mkOption { 133 + type = bool; 134 + description = "Whether to do recursive snapshots."; 135 + default = false; 136 + }; 137 + 138 + mbuffer = { 139 + enable = mkOption { 140 + type = bool; 141 + description = "Whether to use <command>mbuffer</command>."; 142 + default = false; 143 + }; 144 + 145 + port = mkOption { 146 + type = nullOr ints.u16; 147 + description = '' 148 + Port to use for <command>mbuffer</command>. 149 + </para> 150 + <para> 151 + If this is null, it will run <command>mbuffer</command> through 152 + ssh. 153 + </para> 154 + <para> 155 + If this is not null, it will run <command>mbuffer</command> 156 + directly through TCP, which is not encrypted but faster. In that 157 + case the given port needs to be open on the destination host. 158 + ''; 159 + default = null; 160 + }; 161 + 162 + size = mkOption { 163 + type = mbufferSizeType; 164 + description = '' 165 + The size for <command>mbuffer</command>. 166 + Supports the units b, k, M, G. 167 + ''; 168 + default = "1G"; 169 + example = "128M"; 170 + }; 171 + }; 172 + 173 + presnap = mkOption { 174 + type = nullOr str; 175 + description = '' 176 + Command to run before snapshots are taken on the source dataset, 177 + e.g. for database locking/flushing. See also 178 + <option>postsnap</option>. 179 + ''; 180 + default = null; 181 + example = literalExample '' 182 + ''${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 183 + ''; 184 + }; 185 + 186 + postsnap = mkOption { 187 + type = nullOr str; 188 + description = '' 189 + Command to run after snapshots are taken on the source dataset, 190 + e.g. for database unlocking. See also <option>presnap</option>. 191 + ''; 192 + default = null; 193 + example = literalExample '' 194 + ''${pkgs.coreutils}/bin/kill `''${pkgs.coreutils}/bin/cat /tmp/mariadblock.pid`;''${pkgs.coreutils}/bin/rm /tmp/mariadblock.pid 195 + ''; 196 + }; 197 + 198 + timestampFormat = mkOption { 199 + type = timestampType; 200 + description = '' 201 + The timestamp format to use for constructing snapshot names. 202 + The syntax is <literal>strftime</literal>-like. The string must 203 + consist of the mandatory <literal>%Y %m %d %H %M %S</literal>. 204 + Optionally <literal>- _ . :</literal> characters as well as any 205 + alphanumeric character are allowed. If suffixed by a 206 + <literal>Z</literal>, times will be in UTC. 207 + ''; 208 + default = "%Y-%m-%d-%H%M%S"; 209 + example = "znapzend-%m.%d.%Y-%H%M%SZ"; 210 + }; 211 + 212 + sendDelay = mkOption { 213 + type = int; 214 + description = '' 215 + Specify delay (in seconds) before sending snaps to the destination. 216 + May be useful if you want to control sending time. 217 + ''; 218 + default = 0; 219 + example = 60; 220 + }; 221 + 222 + plan = mkOption { 223 + type = str; 224 + description = planDescription; 225 + example = planExample; 226 + }; 227 + 228 + dataset = mkOption { 229 + type = str; 230 + description = "The dataset to use for this source."; 231 + example = "tank/home"; 232 + }; 233 + 234 + destinations = mkOption { 235 + type = loaOf (destType config); 236 + description = "Additional destinations."; 237 + default = {}; 238 + example = literalExample '' 239 + { 240 + local = { 241 + dataset = "btank/backup"; 242 + presend = "zpool import -N btank"; 243 + postsend = "zpool export btank"; 244 + }; 245 + remote = { 246 + host = "john@example.com"; 247 + dataset = "tank/john"; 248 + }; 249 + }; 250 + ''; 251 + }; 252 + }; 253 + 254 + config = { 255 + dataset = mkDefault name; 256 + }; 257 + 258 + }); 259 + 260 + ### Generating the configuration from here 261 + 6 262 cfg = config.services.znapzend; 263 + 264 + onOff = b: if b then "on" else "off"; 265 + nullOff = b: if isNull b then "off" else toString b; 266 + stripSlashes = replaceStrings [ "/" ] [ "." ]; 267 + 268 + attrsToFile = config: concatStringsSep "\n" (builtins.attrValues ( 269 + mapAttrs (n: v: "${n}=${v}") config)); 270 + 271 + mkDestAttrs = dst: with dst; 272 + mapAttrs' (n: v: nameValuePair "dst_${label}${n}" v) ({ 273 + "" = optionalString (! isNull host) "${host}:" + dataset; 274 + _plan = plan; 275 + } // optionalAttrs (presend != null) { 276 + _precmd = presend; 277 + } // optionalAttrs (postsend != null) { 278 + _pstcmd = postsend; 279 + }); 280 + 281 + mkSrcAttrs = srcCfg: with srcCfg; { 282 + enabled = onOff enable; 283 + mbuffer = with mbuffer; if enable then "${pkgs.mbuffer}/bin/mbuffer" 284 + + optionalString (port != null) ":${toString port}" else "off"; 285 + mbuffer_size = mbuffer.size; 286 + post_znap_cmd = nullOff postsnap; 287 + pre_znap_cmd = nullOff presnap; 288 + recursive = onOff recursive; 289 + src = dataset; 290 + src_plan = plan; 291 + tsformat = timestampFormat; 292 + zend_delay = toString sendDelay; 293 + } // fold (a: b: a // b) {} ( 294 + map mkDestAttrs (builtins.attrValues destinations) 295 + ); 296 + 297 + files = mapAttrs' (n: srcCfg: let 298 + fileText = attrsToFile (mkSrcAttrs srcCfg); 299 + in { 300 + name = srcCfg.dataset; 301 + value = pkgs.writeText (stripSlashes srcCfg.dataset) fileText; 302 + }) cfg.zetup; 303 + 7 304 in 8 305 { 9 306 options = { 10 307 services.znapzend = { 11 - enable = mkEnableOption "ZnapZend daemon"; 308 + enable = mkEnableOption "ZnapZend ZFS backup daemon"; 12 309 13 310 logLevel = mkOption { 14 311 default = "debug"; 15 312 example = "warning"; 16 - type = lib.types.enum ["debug" "info" "warning" "err" "alert"]; 17 - description = "The log level when logging to file. Any of debug, info, warning, err, alert. Default in daemonized form is debug."; 313 + type = enum ["debug" "info" "warning" "err" "alert"]; 314 + description = '' 315 + The log level when logging to file. Any of debug, info, warning, err, 316 + alert. Default in daemonized form is debug. 317 + ''; 18 318 }; 19 319 20 320 logTo = mkOption { 21 - type = types.str; 321 + type = str; 22 322 default = "syslog::daemon"; 23 323 example = "/var/log/znapzend.log"; 24 - description = "Where to log to (syslog::&lt;facility&gt; or &lt;filepath&gt;)."; 324 + description = '' 325 + Where to log to (syslog::&lt;facility&gt; or &lt;filepath&gt;). 326 + ''; 25 327 }; 26 328 27 329 noDestroy = mkOption { 28 - type = types.bool; 330 + type = bool; 29 331 default = false; 30 332 description = "Does all changes to the filesystem except destroy."; 31 333 }; 32 334 33 335 autoCreation = mkOption { 34 - type = types.bool; 336 + type = bool; 35 337 default = false; 36 - description = "Automatically create the dataset on dest if it does not exists."; 338 + description = "Automatically create the destination dataset if it does not exists."; 339 + }; 340 + 341 + zetup = mkOption { 342 + type = loaOf srcType; 343 + description = "Znapzend configuration."; 344 + default = {}; 345 + example = literalExample '' 346 + { 347 + "tank/home" = { 348 + # Make snapshots of tank/home every hour, keep those for 1 day, 349 + # keep every days snapshot for 1 month, etc. 350 + plan = "1d=>1h,1m=>1d,1y=>1m"; 351 + recursive = true; 352 + # Send all those snapshots to john@example.com:rtank/john as well 353 + destinations.remote = { 354 + host = "john@example.com"; 355 + dataset = "rtank/john"; 356 + }; 357 + }; 358 + }; 359 + ''; 360 + }; 361 + 362 + pure = mkOption { 363 + type = bool; 364 + description = '' 365 + Do not persist any stateful znapzend setups. If this option is 366 + enabled, your previously set znapzend setups will be cleared and only 367 + the ones defined with this module will be applied. 368 + ''; 369 + default = false; 37 370 }; 38 371 }; 39 372 }; ··· 49 382 50 383 path = with pkgs; [ zfs mbuffer openssh ]; 51 384 385 + preStart = optionalString cfg.pure '' 386 + echo Resetting znapzend zetups 387 + ${pkgs.znapzend}/bin/znapzendzetup list \ 388 + | grep -oP '(?<=\*\*\* backup plan: ).*(?= \*\*\*)' \ 389 + | xargs ${pkgs.znapzend}/bin/znapzendzetup delete 390 + '' + concatStringsSep "\n" (mapAttrsToList (dataset: config: '' 391 + echo Importing znapzend zetup ${config} for dataset ${dataset} 392 + ${pkgs.znapzend}/bin/znapzendzetup import --write ${dataset} ${config} 393 + '') files); 394 + 52 395 serviceConfig = { 53 - ExecStart = "${pkgs.znapzend}/bin/znapzend --logto=${cfg.logTo} --loglevel=${cfg.logLevel} ${optionalString cfg.noDestroy "--nodestroy"} ${optionalString cfg.autoCreation "--autoCreation"}"; 396 + ExecStart = let 397 + args = concatStringsSep " " [ 398 + "--logto=${cfg.logTo}" 399 + "--loglevel=${cfg.logLevel}" 400 + (optionalString cfg.noDestroy "--nodestroy") 401 + (optionalString cfg.autoCreation "--autoCreation") 402 + ]; in "${pkgs.znapzend}/bin/znapzend ${args}"; 54 403 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; 55 404 Restart = "on-failure"; 56 405 }; 57 406 }; 58 407 }; 59 408 }; 409 + 410 + meta.maintainers = with maintainers; [ infinisil ]; 60 411 }