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