]> git.immae.eu Git - perso/Immae/Config/Nix.git/blame - systems/eldiron/borg_backup.nix
Migrate to borg backup
[perso/Immae/Config/Nix.git] / systems / eldiron / borg_backup.nix
CommitLineData
1c90c0dd
IB
1{ lib, pkgs, config, name, ... }:
2
3let
4 cfg = config.myEnv.borg_backup;
5 varDir = "/var/lib/borgbackup";
6 borg_args = "--encryption repokey --make-parent-dirs init create prune compact check";
7 borg_backup_full_with_ignored = pkgs.writeScriptBin "borg_full_with_ignored" ''
8 #!${pkgs.stdenv.shell}
9
10 if [ -z "$1" -o "$1" = "-h" -o "$1" = "--help" ]; then
11 echo "borg_full_with_ignored /path/to/borgmatic.yaml"
12 echo "Does a full backup including directories with .duplicity-ignore"
13 exit 1
14 fi
15 ${pkgs.borgmatic}/bin/borgmatic -c "$1" --override 'storage.archive_name_format="{hostname}-with-ignored-{now:%Y-%m-%dT%H:%M:%S.%f}"' --override 'location.exclude_if_present=[]' ${borg_args}
16 '';
17 borg_backup = pkgs.writeScriptBin "borg_backup" ''
18 #!${pkgs.stdenv.shell}
19
20 declare -a profiles
21 profiles=()
22 ${builtins.concatStringsSep "\n" (lib.flatten (lib.mapAttrsToList (k: v: map (remote: [
23 ''profiles+=("${remote}_${k}")''
24 ]) v.remotes) config.services.borgBackup.profiles))}
25
26 if [ -f "${varDir}/last_backup_profile" ]; then
27 last_backup=$(cat ${varDir}/last_backup_profile)
28 for i in "''${!profiles[@]}"; do
29 if [[ "''${profiles[$i]}" = "$last_backup" ]]; then
30 break
31 fi
32 done
33 ((i+=1))
34 profiles=("''${profiles[@]:$i}" "''${profiles[@]:0:$i}")
35 fi
36
37 # timeout in minutes
38 timeout="''${1:-180}"
39 timeout_timestamp=$(date +%s -d "$timeout minutes")
40 for profile in "''${profiles[@]}"; do
41 if [ $(date +%s -d "now") -ge "$timeout_timestamp" ]; then
42 break
43 fi
44
45 touch "${varDir}/$profile.log"
46 ${pkgs.borgmatic}/bin/borgmatic -c "${config.secrets.location}/borg_backup/$profile/borgmatic.yaml" ${borg_args} >> ${varDir}/$profile.log
47 [[ $? = 0 ]] || echo -e "Error when doing backup for $profile, see above or logs in ${varDir}/$profile.log\n---------------------------------------" >&2
48 echo "$profile" > ${varDir}/last_backup_profile
49 done
50 '';
51
52 check_backups = pkgs.writeScriptBin "borg_list_not_backuped" ''
53 #!${pkgs.stdenv.shell}
54
55 do_check() {
56 local dir="$1" path ignored_path
57 find "$dir" -mindepth 1 -maxdepth 1 | while IFS= read -r path; do
58 if ${pkgs.gnugrep}/bin/grep -qFx "$path" ${config.secrets.fullPaths."borg_backup/backuped_list"}; then
59 continue
60 elif ${pkgs.gnugrep}/bin/grep -q "^$path/" ${config.secrets.fullPaths."borg_backup/backuped_list"}; then
61 do_check "$path"
62 else
63 while IFS= read -r ignored_path; do
64 if [[ "$path" =~ ^$ignored_path$ ]]; then
65 continue 2
66 fi
67 done < ${config.secrets.fullPaths."borg_backup/ignored_list"}
68 printf '%s\n' "$path"
69 fi
70 done
71 }
72
73 do_check /var/lib
74 '';
75 borgProfile = profile: remote: bucket: builtins.toJSON {
76 location = {
77 source_directories = map (p: "${profile.rootDir}/${p}") profile.includedPaths;
78 repositories = [
79 { path = cfg.remotes.${remote}.remote bucket; label = "backupserver"; }
80 ];
81 one_file_system = false;
82 exclude_if_present = [".duplicity-ignore"];
83 source_directories_must_exist = profile.directoriesMustExist;
84 borgmatic_source_directory = "${varDir}/${profile.bucket}/.borgmatic";
85 };
86 storage = {
87 encryption_passphrase = profile.password;
88 ssh_command = "ssh -i ${config.secrets.fullPaths."borg_backup/identity"}";
89 compression = "zlib";
90 borg_base_directory = "${varDir}/${profile.bucket}";
91 };
92 retention = {
93 keep_within = "10d";
94 keep_daily = 30;
95 };
96 };
97in
98{
99 options = {
100 services.borgBackup.enable = lib.mkOption {
101 type = lib.types.bool;
102 default = false;
103 description = ''
104 Whether to enable remote backups.
105 '';
106 };
107 services.borgBackup.profiles = lib.mkOption {
108 type = lib.types.attrsOf (lib.types.submodule {
109 options = {
110 hash = lib.mkOption {
111 type = lib.types.bool;
112 default = true;
113 description = ''
114 Hash bucket and directory names
115 '';
116 };
117 rootDir = lib.mkOption {
118 type = lib.types.path;
119 default = "/var/lib";
120 description = ''
121 Path to backup
122 '';
123 };
124 password = lib.mkOption {
125 type = lib.types.str;
126 default = cfg.password;
127 description = ''
128 password to use to encrypt data
129 '';
130 };
131 directoriesMustExist = lib.mkOption {
132 type = lib.types.bool;
133 default = true;
134 description = ''
135 Raise error if backuped directory doesn't exist
136 '';
137 };
138 bucket = lib.mkOption {
139 type = lib.types.str;
140 description = ''
141 Bucket to use
142 '';
143 };
144 remotes = lib.mkOption {
145 type = lib.types.listOf lib.types.str;
146 description = ''
147 Remotes to use for backup
148 '';
149 };
150 includedPaths = lib.mkOption {
151 type = lib.types.listOf lib.types.str;
152 default = [];
153 description = ''
154 Included paths (subdirs of rootDir)
155 '';
156 };
157 excludeFile = lib.mkOption {
158 type = lib.types.lines;
159 default = "";
160 description = ''
161 Content to put in exclude file
162 '';
163 };
164 ignoredPaths = lib.mkOption {
165 type = lib.types.listOf lib.types.str;
166 default = [];
167 description = ''
168 List of paths to ignore when checking non-backed-up directories
169 Can use (POSIX extended) regex
170 '';
171 };
172 };
173 });
174 };
175 };
176
177 config = lib.mkIf config.services.borgBackup.enable {
178 system.activationScripts.borg_backup = ''
179 install -m 0700 -o root -g root -d ${varDir}
180 '';
181 secrets.keys = lib.listToAttrs (lib.flatten (lib.mapAttrsToList (k: v:
182 let
183 bucket = if v.hash or true then builtins.hashString "sha256" v.bucket else v.bucket;
184 in map (remote: [
185 (lib.nameValuePair "borg_backup/${remote}_${k}/borgmatic.yaml" {
186 permissions = "0400";
187 text = borgProfile v remote bucket;
188 })
189 (lib.nameValuePair "borg_backup/${remote}_${k}" {
190 permissions = "0700";
191 isDir = true;
192 })
193 ]) v.remotes) config.services.borgBackup.profiles)) // {
194 "borg_backup/identity" = {
195 permissions = "0400";
196 text = "{{ .ssl_keys.borg_backup }}";
197 };
198 "borg_backup/ignored_list" = {
199 permissions = "0400";
200 text = let
201 ignored = map
202 (v: map (p: "${v.rootDir}/${p}") v.ignoredPaths)
203 (builtins.attrValues config.services.borgBackup.profiles);
204 in builtins.concatStringsSep "\n" (lib.flatten ignored);
205 };
206 "borg_backup/backuped_list" = {
207 permissions = "0400";
208 text = let
209 included = map
210 (v: map (p: "${v.rootDir}/${p}") v.includedPaths)
211 (builtins.attrValues config.services.borgBackup.profiles);
212 in builtins.concatStringsSep "\n" (lib.flatten included);
213 };
214 };
215
216 programs.ssh.knownHostsFiles = [
217 (pkgs.writeText
218 "borg_backup_known_hosts"
219 (builtins.concatStringsSep
220 "\n"
221 (builtins.filter
222 (v: v != null)
223 (builtins.map
224 (v: v.sshKnownHosts)
225 (builtins.attrValues cfg.remotes)
226 )
227 )
228 )
229 )
230 ];
231 environment.systemPackages = [ pkgs.borgbackup pkgs.borgmatic borg_backup_full_with_ignored borg_backup check_backups ];
232 services.cron = {
233 enable = true;
234 systemCronJobs = [
235 "0 0 * * * root ${borg_backup}/bin/borg_backup 300"
236 ];
237
238 };
239
240 };
241}