at 18.03-beta 13 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4with types; 5 6let 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 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 304in 305{ 306 options = { 307 services.znapzend = { 308 enable = mkEnableOption "ZnapZend ZFS backup daemon"; 309 310 logLevel = mkOption { 311 default = "debug"; 312 example = "warning"; 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 ''; 318 }; 319 320 logTo = mkOption { 321 type = str; 322 default = "syslog::daemon"; 323 example = "/var/log/znapzend.log"; 324 description = '' 325 Where to log to (syslog::&lt;facility&gt; or &lt;filepath&gt;). 326 ''; 327 }; 328 329 noDestroy = mkOption { 330 type = bool; 331 default = false; 332 description = "Does all changes to the filesystem except destroy."; 333 }; 334 335 autoCreation = mkOption { 336 type = bool; 337 default = false; 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; 370 }; 371 }; 372 }; 373 374 config = mkIf cfg.enable { 375 environment.systemPackages = [ pkgs.znapzend ]; 376 377 systemd.services = { 378 "znapzend" = { 379 description = "ZnapZend - ZFS Backup System"; 380 wantedBy = [ "zfs.target" ]; 381 after = [ "zfs.target" ]; 382 383 path = with pkgs; [ zfs mbuffer openssh ]; 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 395 serviceConfig = { 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}"; 403 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; 404 Restart = "on-failure"; 405 }; 406 }; 407 }; 408 }; 409 410 meta.maintainers = with maintainers; [ infinisil ]; 411}