{ options, config, pkgs, lib, ... }: with lib; let backups = config.restic-backups; backupOpts = { ... }: { options = { user = mkOption { type = types.str; default = "root"; }; passwordFile = mkOption { type = types.str; }; runBeforeBackup = mkOption { type = types.str; default = ""; }; paths = mkOption { type = with types; listOf str; default = []; }; postgresDatabases = mkOption { type = with types; listOf str; default = []; }; sqliteDatabases = mkOption { type = with types; listOf str; default = []; }; influxBuckets = mkOption { type = with types; listOf str; default = []; }; targets = mkOption { type = with types; listOf str; default = [ "wanderduene.ctu.cx" "briefkasten.ctu.cx" ]; }; timerConfig = mkOption { type = types.attrs; default = { OnCalendar = "daily"; RandomizedDelaySec = 1200; }; }; }; }; in { options.restic-backups = mkOption { type = with types; attrsOf (submodule backupOpts); default = {}; }; config = mkIf (backups != {}) { systemd.services = mapAttrs' ( name: backup: nameValuePair "restic-backup-${name}" { restartIfChanged = false; requires = [ "network.target" "local-fs.target" ]; onFailure = [ "email-notify@%i.service" ]; path = [ pkgs.restic ] ++ optionals (backup.postgresDatabases != []) [ config.services.postgresql.package pkgs.zstd ]; serviceConfig = { Type = "oneshot"; User = backup.user; RuntimeDirectory = "restic-backup-${name}"; CacheDirectory = "restic-backup-${name}"; CacheDirectoryMode = "0700"; # ReadWritePaths = backup.paths; PrivateTmp = true; ProtectHome = true; ProtectSystem = "strict"; Environment = "RESTIC_PASSWORD_FILE=/tmp/passwordFile"; ExecStartPre = [ ( "!" + (pkgs.writeScript "privileged-pre-start" ('' #!${pkgs.runtimeShell} set -eu pipefail cp ${backup.passwordFile} /tmp/passwordFile; ${if builtins.elem "briefkasten.ctu.cx" backup.targets then '' cp /run/agenix/restic-server-briefkasten /tmp/briefkasten.ctu.cx; '' else "" } ${if builtins.elem "wanderduene.ctu.cx" backup.targets then '' cp /run/agenix/restic-server-wanderduene /tmp/wanderduene.ctu.cx; '' else "" } chown -R ${backup.user} /tmp '')) ) ( pkgs.writeScript "pre-start" ('' #!${pkgs.runtimeShell} set -eu pipefail ${backup.runBeforeBackup} '' + concatMapStringsSep "\n" (db: '' echo "Dumping Postgres-database: ${db}" mkdir -p /tmp/postgresDatabases pg_dump ${db} | zstd --rsyncable > /tmp/postgresDatabases/${db}.sql.zst [ $(du -b /tmp/postgresDatabases/${db}.sql.zst | cut -f1) -gt "50" ] || exit 1 '') backup.postgresDatabases + concatMapStringsSep "\n" (db: '' echo "Dumping sqlite-database: ${db}" mkdir -p /tmp/sqliteDatabases ${pkgs.sqlite}/bin/sqlite3 ${db} ".backup '/tmp/sqliteDatabases/${builtins.baseNameOf db}.sqlite-backup'" [ $(du -b /tmp/sqliteDatabases/${builtins.baseNameOf db}.sqlite-backup | cut -f1) -gt "50" ] || exit 1 '') backup.sqliteDatabases + concatMapStringsSep "\n" (db: '' echo "Dumping influx-bucket: ${db}" mkdir -p /tmp/influxBuckets ${pkgs.influxdb2}/bin/influx backup --compression=none --bucket=${db} /tmp/influxBuckets/${db} [ $(du -b /tmp/influxBuckets/${db} | cut -f1) -gt "50" ] || exit 1 '') backup.influxBuckets) ) ]; }; script = '' set -eu pipefail export XDG_CACHE_HOME=/var/cache/restic-backup-${name} '' + concatMapStringsSep "\n\n" (server: '' echo "Backing up to: ${server}" export RESTIC_REPOSITORY="rest:https://restic:$(cat /tmp/${server})@restic.${server}/${config.networking.hostName}-${name}" #create repo if it not exists restic snapshots || restic init #backup files restic backup ${escapeShellArgs (((backup.paths ++ optional (backup.postgresDatabases != []) "/tmp/postgresDatabases") ++ optional (backup.sqliteDatabases != []) "/tmp/sqliteDatabases") ++ optional (backup.influxBuckets != []) "/tmp/influxBuckets") } restic check '') backup.targets; } ) backups; systemd.timers = mapAttrs' ( name: backup: nameValuePair "restic-backup-${name}" { wantedBy = [ "timers.target" ]; timerConfig = backup.timerConfig; } ) backups; }; }