diff options
Diffstat (limited to 'nixpkgs/nixos/modules/services/backup/tarsnap.nix')
-rw-r--r-- | nixpkgs/nixos/modules/services/backup/tarsnap.nix | 402 |
1 files changed, 402 insertions, 0 deletions
diff --git a/nixpkgs/nixos/modules/services/backup/tarsnap.nix b/nixpkgs/nixos/modules/services/backup/tarsnap.nix new file mode 100644 index 000000000000..4fc7c24813a5 --- /dev/null +++ b/nixpkgs/nixos/modules/services/backup/tarsnap.nix @@ -0,0 +1,402 @@ +{ config, lib, pkgs, utils, ... }: + +with lib; + +let + gcfg = config.services.tarsnap; + + configFile = name: cfg: '' + keyfile ${cfg.keyfile} + ${optionalString (cfg.cachedir != null) "cachedir ${cfg.cachedir}"} + ${optionalString cfg.nodump "nodump"} + ${optionalString cfg.printStats "print-stats"} + ${optionalString cfg.printStats "humanize-numbers"} + ${optionalString (cfg.checkpointBytes != null) ("checkpoint-bytes "+cfg.checkpointBytes)} + ${optionalString cfg.aggressiveNetworking "aggressive-networking"} + ${concatStringsSep "\n" (map (v: "exclude ${v}") cfg.excludes)} + ${concatStringsSep "\n" (map (v: "include ${v}") cfg.includes)} + ${optionalString cfg.lowmem "lowmem"} + ${optionalString cfg.verylowmem "verylowmem"} + ${optionalString (cfg.maxbw != null) "maxbw ${toString cfg.maxbw}"} + ${optionalString (cfg.maxbwRateUp != null) "maxbw-rate-up ${toString cfg.maxbwRateUp}"} + ${optionalString (cfg.maxbwRateDown != null) "maxbw-rate-down ${toString cfg.maxbwRateDown}"} + ''; +in +{ + options = { + services.tarsnap = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Enable periodic tarsnap backups. + ''; + }; + + keyfile = mkOption { + type = types.str; + default = "/root/tarsnap.key"; + description = '' + The keyfile which associates this machine with your tarsnap + account. + Create the keyfile with <command>tarsnap-keygen</command>. + + Note that each individual archive (specified below) may also have its + own individual keyfile specified. Tarsnap does not allow multiple + concurrent backups with the same cache directory and key (starting a + new backup will cause another one to fail). If you have multiple + archives specified, you should either spread out your backups to be + far apart, or specify a separate key for each archive. By default + every archive defaults to using + <literal>"/root/tarsnap.key"</literal>. + + It's recommended for backups that you generate a key for every archive + using <literal>tarsnap-keygen(1)</literal>, and then generate a + write-only tarsnap key using <literal>tarsnap-keymgmt(1)</literal>, + and keep your master key(s) for a particular machine off-site. + + The keyfile name should be given as a string and not a path, to + avoid the key being copied into the Nix store. + ''; + }; + + archives = mkOption { + type = types.attrsOf (types.submodule ({ config, ... }: + { + options = { + keyfile = mkOption { + type = types.str; + default = gcfg.keyfile; + description = '' + Set a specific keyfile for this archive. This defaults to + <literal>"/root/tarsnap.key"</literal> if left unspecified. + + Use this option if you want to run multiple backups + concurrently - each archive must have a unique key. You can + generate a write-only key derived from your master key (which + is recommended) using <literal>tarsnap-keymgmt(1)</literal>. + + Note: every archive must have an individual master key. You + must generate multiple keys with + <literal>tarsnap-keygen(1)</literal>, and then generate write + only keys from those. + + The keyfile name should be given as a string and not a path, to + avoid the key being copied into the Nix store. + ''; + }; + + cachedir = mkOption { + type = types.nullOr types.path; + default = "/var/cache/tarsnap/${utils.escapeSystemdPath config.keyfile}"; + description = '' + The cache allows tarsnap to identify previously stored data + blocks, reducing archival time and bandwidth usage. + + Should the cache become desynchronized or corrupted, tarsnap + will refuse to run until you manually rebuild the cache with + <command>tarsnap --fsck</command>. + + Set to <literal>null</literal> to disable caching. + ''; + }; + + nodump = mkOption { + type = types.bool; + default = true; + description = '' + Exclude files with the <literal>nodump</literal> flag. + ''; + }; + + printStats = mkOption { + type = types.bool; + default = true; + description = '' + Print global archive statistics upon completion. + The output is available via + <command>systemctl status tarsnap-archive-name</command>. + ''; + }; + + checkpointBytes = mkOption { + type = types.nullOr types.str; + default = "1GB"; + description = '' + Create a checkpoint every <literal>checkpointBytes</literal> + of uploaded data (optionally specified using an SI prefix). + + 1GB is the minimum value. A higher value is recommended, + as checkpointing is expensive. + + Set to <literal>null</literal> to disable checkpointing. + ''; + }; + + period = mkOption { + type = types.str; + default = "01:15"; + example = "hourly"; + description = '' + Create archive at this interval. + + The format is described in + <citerefentry><refentrytitle>systemd.time</refentrytitle> + <manvolnum>7</manvolnum></citerefentry>. + ''; + }; + + aggressiveNetworking = mkOption { + type = types.bool; + default = false; + description = '' + Upload data over multiple TCP connections, potentially + increasing tarsnap's bandwidth utilisation at the cost + of slowing down all other network traffic. Not + recommended unless TCP congestion is the dominant + limiting factor. + ''; + }; + + directories = mkOption { + type = types.listOf types.path; + default = []; + description = "List of filesystem paths to archive."; + }; + + excludes = mkOption { + type = types.listOf types.str; + default = []; + description = '' + Exclude files and directories matching these patterns. + ''; + }; + + includes = mkOption { + type = types.listOf types.str; + default = []; + description = '' + Include only files and directories matching these + patterns (the empty list includes everything). + + Exclusions have precedence over inclusions. + ''; + }; + + lowmem = mkOption { + type = types.bool; + default = false; + description = '' + Reduce memory consumption by not caching small files. + Possibly beneficial if the average file size is smaller + than 1 MB and the number of files is lower than the + total amount of RAM in KB. + ''; + }; + + verylowmem = mkOption { + type = types.bool; + default = false; + description = '' + Reduce memory consumption by a factor of 2 beyond what + <literal>lowmem</literal> does, at the cost of significantly + slowing down the archiving process. + ''; + }; + + maxbw = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + Abort archival if upstream bandwidth usage in bytes + exceeds this threshold. + ''; + }; + + maxbwRateUp = mkOption { + type = types.nullOr types.int; + default = null; + example = literalExample "25 * 1000"; + description = '' + Upload bandwidth rate limit in bytes. + ''; + }; + + maxbwRateDown = mkOption { + type = types.nullOr types.int; + default = null; + example = literalExample "50 * 1000"; + description = '' + Download bandwidth rate limit in bytes. + ''; + }; + + verbose = mkOption { + type = types.bool; + default = false; + description = '' + Whether to produce verbose logging output. + ''; + }; + explicitSymlinks = mkOption { + type = types.bool; + default = false; + description = '' + Whether to follow symlinks specified as archives. + ''; + }; + followSymlinks = mkOption { + type = types.bool; + default = false; + description = '' + Whether to follow all symlinks in archive trees. + ''; + }; + }; + } + )); + + default = {}; + + example = literalExample '' + { + nixos = + { directories = [ "/home" "/root/ssl" ]; + }; + + gamedata = + { directories = [ "/var/lib/minecraft" ]; + period = "*:30"; + }; + } + ''; + + description = '' + Tarsnap archive configurations. Each attribute names an archive + to be created at a given time interval, according to the options + associated with it. When uploading to the tarsnap server, + archive names are suffixed by a 1 second resolution timestamp. + + For each member of the set is created a timer which triggers the + instanced <literal>tarsnap-archive-name</literal> service unit. You may use + <command>systemctl start tarsnap-archive-name</command> to + manually trigger creation of <literal>archive-name</literal> at + any time. + ''; + }; + }; + }; + + config = mkIf gcfg.enable { + assertions = + (mapAttrsToList (name: cfg: + { assertion = cfg.directories != []; + message = "Must specify paths for tarsnap to back up"; + }) gcfg.archives) ++ + (mapAttrsToList (name: cfg: + { assertion = !(cfg.lowmem && cfg.verylowmem); + message = "You cannot set both lowmem and verylowmem"; + }) gcfg.archives); + + systemd.services = + (mapAttrs' (name: cfg: nameValuePair "tarsnap-${name}" { + description = "Tarsnap archive '${name}'"; + requires = [ "network-online.target" ]; + after = [ "network-online.target" ]; + + path = with pkgs; [ iputils tarsnap utillinux ]; + + # In order for the persistent tarsnap timer to work reliably, we have to + # make sure that the tarsnap server is reachable after systemd starts up + # the service - therefore we sleep in a loop until we can ping the + # endpoint. + preStart = '' + while ! ping -q -c 1 v1-0-0-server.tarsnap.com &> /dev/null; do sleep 3; done + ''; + + script = let + tarsnap = ''tarsnap --configfile "/etc/tarsnap/${name}.conf"''; + run = ''${tarsnap} -c -f "${name}-$(date +"%Y%m%d%H%M%S")" \ + ${optionalString cfg.verbose "-v"} \ + ${optionalString cfg.explicitSymlinks "-H"} \ + ${optionalString cfg.followSymlinks "-L"} \ + ${concatStringsSep " " cfg.directories}''; + in if (cfg.cachedir != null) then '' + mkdir -p ${cfg.cachedir} + chmod 0700 ${cfg.cachedir} + + ( flock 9 + if [ ! -e ${cfg.cachedir}/firstrun ]; then + ( flock 10 + flock -u 9 + ${tarsnap} --fsck + flock 9 + ) 10>${cfg.cachedir}/firstrun + fi + ) 9>${cfg.cachedir}/lockf + + exec flock ${cfg.cachedir}/firstrun ${run} + '' else "exec ${run}"; + + serviceConfig = { + Type = "oneshot"; + IOSchedulingClass = "idle"; + NoNewPrivileges = "true"; + CapabilityBoundingSet = [ "CAP_DAC_READ_SEARCH" ]; + PermissionsStartOnly = "true"; + }; + }) gcfg.archives) // + + (mapAttrs' (name: cfg: nameValuePair "tarsnap-restore-${name}"{ + description = "Tarsnap restore '${name}'"; + requires = [ "network-online.target" ]; + + path = with pkgs; [ iputils tarsnap utillinux ]; + + script = let + tarsnap = ''tarsnap --configfile "/etc/tarsnap/${name}.conf"''; + lastArchive = ''$(${tarsnap} --list-archives | sort | tail -1)''; + run = ''${tarsnap} -x -f "${lastArchive}" ${optionalString cfg.verbose "-v"}''; + + in if (cfg.cachedir != null) then '' + mkdir -p ${cfg.cachedir} + chmod 0700 ${cfg.cachedir} + + ( flock 9 + if [ ! -e ${cfg.cachedir}/firstrun ]; then + ( flock 10 + flock -u 9 + ${tarsnap} --fsck + flock 9 + ) 10>${cfg.cachedir}/firstrun + fi + ) 9>${cfg.cachedir}/lockf + + exec flock ${cfg.cachedir}/firstrun ${run} + '' else "exec ${run}"; + + serviceConfig = { + Type = "oneshot"; + IOSchedulingClass = "idle"; + NoNewPrivileges = "true"; + CapabilityBoundingSet = [ "CAP_DAC_READ_SEARCH" ]; + PermissionsStartOnly = "true"; + }; + }) gcfg.archives); + + # Note: the timer must be Persistent=true, so that systemd will start it even + # if e.g. your laptop was asleep while the latest interval occurred. + systemd.timers = mapAttrs' (name: cfg: nameValuePair "tarsnap-${name}" + { timerConfig.OnCalendar = cfg.period; + timerConfig.Persistent = "true"; + wantedBy = [ "timers.target" ]; + }) gcfg.archives; + + environment.etc = + mapAttrs' (name: cfg: nameValuePair "tarsnap/${name}.conf" + { text = configFile name cfg; + }) gcfg.archives; + + environment.systemPackages = [ pkgs.tarsnap ]; + }; +} |