This repository has no description
4.8 kB
147 lines
1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.lass.backup;
9 states = lib.attrValues cfg.state;
10 key = config.clan.core.vars.generators.borgbackup.files."borgbackup.ssh.id25519".path;
11
12 # Runs every state's hook in its own script, keeps going when one fails
13 # and fails at the end if any did.
14 runHooks =
15 hook:
16 pkgs.writeShellScript "backup-${hook}" ''
17 failed=()
18 ${lib.concatMapStrings (state: ''
19 echo "backup: ${hook} ${state.name}"
20 ${pkgs.writeShellScript "backup-${hook}-${state.name}" ''
21 set -xeo pipefail
22 ${state.${hook}}
23 ''} || failed+=(${lib.escapeShellArg state.name})
24 '') (lib.filter (state: state.${hook} != null) states)}
25 if [ ''${#failed[@]} -gt 0 ]; then
26 echo "backup: ${hook} failed for: ''${failed[*]}" >&2
27 exit 1
28 fi
29 '';
30in
31{
32 options.lass.backup = {
33 repo = lib.mkOption {
34 type = lib.types.nullOr lib.types.str;
35 default = null;
36 example = "user@backup.example.com:/./machine";
37 description = ''
38 Borg repository the machine backs up to. Without it no backup job is
39 generated, so modules can declare their state everywhere.
40 '';
41 };
42
43 sshPort = lib.mkOption {
44 type = lib.types.port;
45 default = 22;
46 };
47
48 knownHosts = lib.mkOption {
49 type = lib.types.lines;
50 description = "known_hosts lines pinning the host key of the repo's server.";
51 };
52
53 state = lib.mkOption {
54 default = { };
55 description = ''
56 Backup needs of a service, declared next to it. Dumps go to
57 /var/backup, which is always backed up.
58 '';
59 type = lib.types.attrsOf (
60 lib.types.submodule (
61 { name, ... }:
62 {
63 options = {
64 name = lib.mkOption {
65 type = lib.types.str;
66 default = name;
67 };
68 folders = lib.mkOption {
69 type = lib.types.listOf lib.types.str;
70 default = [ ];
71 };
72 exclude = lib.mkOption {
73 type = lib.types.listOf lib.types.str;
74 default = [ ];
75 description = "borg exclude patterns.";
76 };
77 preBackup = lib.mkOption {
78 type = lib.types.nullOr lib.types.lines;
79 default = null;
80 description = ''
81 Runs before the backup inside the read-only borg unit (as
82 root), e.g. to dump a database to /var/backup. If it fails,
83 no backup is made and the job fails.
84 '';
85 };
86 postBackup = lib.mkOption {
87 type = lib.types.nullOr lib.types.lines;
88 default = null;
89 description = ''
90 Runs as root after the backup, also when it failed, e.g. to
91 start a service stopped by preBackup.
92 '';
93 };
94 readWritePaths = lib.mkOption {
95 type = lib.types.listOf lib.types.str;
96 default = [ ];
97 description = "Paths preBackup needs to write to.";
98 };
99 };
100 }
101 )
102 );
103 };
104 };
105
106 config = lib.mkIf (cfg.repo != null) {
107 clan.core.vars.generators.borgbackup = {
108 files."borgbackup.ssh.id25519" = { };
109 files."borgbackup.ssh.id25519.pub".secret = false;
110 runtimeInputs = with pkgs; [
111 coreutils
112 openssh
113 ];
114 script = ''
115 ssh-keygen -t ed25519 -N "" -f $out/borgbackup.ssh.id25519
116 '';
117 };
118
119 systemd.tmpfiles.rules = [ "d /var/backup 0700 root root -" ];
120
121 # Archives are named and pruned by <hostname>-<job>; another job name
122 # would leave the existing archives unpruned.
123 services.borgbackup.jobs.hetzner = {
124 inherit (cfg) repo;
125 paths = lib.unique ([ "/var/backup" ] ++ lib.concatMap (state: state.folders) states);
126 exclude = lib.unique ([ "*.pyc" ] ++ lib.concatMap (state: state.exclude) states);
127 readWritePaths = lib.unique (
128 [ "/var/backup" ] ++ lib.concatMap (state: state.readWritePaths) states
129 );
130 preHook = "${runHooks "preBackup"}";
131 encryption.mode = "none";
132 compression = "auto,zstd";
133 startAt = "*-*-* 03:00:00";
134 environment.BORG_RSH = "ssh -oPort=${toString cfg.sshPort} -oUserKnownHostsFile=${pkgs.writeText "backup-known-hosts" cfg.knownHosts} -i ${key}";
135 prune.keep = {
136 within = "1d";
137 daily = 7;
138 weekly = 4;
139 monthly = 0;
140 };
141 };
142
143 systemd.services.borgbackup-job-hetzner.serviceConfig.ExecStopPost = lib.mkIf (lib.any (
144 state: state.postBackup != null
145 ) states) [ "+${runHooks "postBackup"}" ];
146 };
147}