]> git.immae.eu Git - perso/Immae/Config/Nix.git/blob - modules/rsync_backup/default.nix
Fix verbose ssh rsync backup
[perso/Immae/Config/Nix.git] / modules / rsync_backup / default.nix
1 { lib, pkgs, config, myconfig, ... }:
2 let
3 partModule = lib.types.submodule {
4 options = {
5 remote_folder = lib.mkOption {
6 type = lib.types.path;
7 description = ''
8 Path to backup
9 '';
10 };
11 exclude_from = lib.mkOption {
12 type = lib.types.listOf lib.types.path;
13 default = [];
14 description = ''
15 Paths to exclude from the backup
16 '';
17 };
18 files_from = lib.mkOption {
19 type = lib.types.listOf lib.types.path;
20 default = [];
21 description = ''
22 Paths to take for the backup
23 (if empty: whole folder minus exclude_from)
24 '';
25 };
26 args = lib.mkOption {
27 type = lib.types.nullOr lib.types.str;
28 default = null;
29 description = ''
30 additional arguments for rsync
31 '';
32 };
33 };
34 };
35 profileModule = lib.types.submodule {
36 options = {
37 keep = lib.mkOption {
38 type = lib.types.int;
39 default = 7;
40 description = ''
41 Number of backups to keep
42 '';
43 };
44 login = lib.mkOption {
45 type = lib.types.str;
46 description = ''
47 login to connect to
48 '';
49 };
50 host = lib.mkOption {
51 type = lib.types.str;
52 description = ''
53 host to connect to
54 '';
55 };
56 port = lib.mkOption {
57 type = lib.types.str;
58 default = "22";
59 description = ''
60 port to connect to
61 '';
62 };
63 host_key = lib.mkOption {
64 type = lib.types.str;
65 description = ''
66 Host key to use as known host
67 '';
68 };
69 host_key_type = lib.mkOption {
70 type = lib.types.str;
71 description = ''
72 Host key type
73 '';
74 };
75 parts = lib.mkOption {
76 type = lib.types.attrsOf partModule;
77 description = ''
78 folders to backup in the host
79 '';
80 };
81 };
82 };
83 cfg = config.services.rsyncBackup;
84
85 ssh_key = config.secrets.fullPaths."rsync_backup/identity";
86
87 backup_head = mailto: ''
88 #!${pkgs.stdenv.shell}
89 EXCL_FROM=`mktemp`
90 FILES_FROM=`mktemp`
91 TMP_STDERR=`mktemp`
92
93 on_exit() {
94 ${lib.optionalString (mailto != null) ''
95 MAILTO="${mailto}"
96 if [ -s "$TMP_STDERR" ]; then
97 cat "$TMP_STDERR" | ${pkgs.mailutils}/bin/mail -s "save_distant rsync error" "$MAILTO"
98 fi
99 ''}
100 rm -f $TMP_STDERR $EXCL_FROM $FILES_FROM
101 }
102
103 trap "on_exit" EXIT
104
105 exec 2> "$TMP_STDERR"
106 exec < /dev/null
107
108 set -e
109 '';
110
111 backup_profile = name: profile: builtins.concatStringsSep "\n" (
112 [(backup_profile_head name profile)]
113 ++ lib.mapAttrsToList (backup_part name) profile.parts
114 ++ [(backup_profile_tail name profile)]);
115
116 backup_profile_head = name: profile: ''
117 ##### ${name} #####
118 PORT="${profile.port}"
119 DEST="${profile.login}@${profile.host}"
120 BASE="${cfg.mountpoint}/${name}"
121 OLD_BAK_BASE=$BASE/older/j
122 BAK_BASE=''${OLD_BAK_BASE}0
123 RSYNC_OUTPUT=$BASE/rsync_output
124 NBR=${builtins.toString profile.keep}
125
126 if ! ssh \
127 -o PreferredAuthentications=publickey \
128 -o StrictHostKeyChecking=yes \
129 -o ClearAllForwardings=yes \
130 -o UserKnownHostsFile=/dev/null \
131 -o CheckHostIP=no \
132 -p $PORT \
133 -i ${ssh_key} \
134 $DEST backup; then
135 echo "Fichier de verrouillage backup sur $DEST ou impossible de se connecter" >&2
136 skip=$DEST
137 fi
138
139 rm -rf ''${OLD_BAK_BASE}''${NBR}
140 for j in `seq -w $(($NBR-1)) -1 0`; do
141 [ ! -d ''${OLD_BAK_BASE}$j ] && continue
142 mv ''${OLD_BAK_BASE}$j ''${OLD_BAK_BASE}$(($j+1))
143 done
144 mkdir $BAK_BASE
145 mv $RSYNC_OUTPUT $BAK_BASE
146 mkdir $RSYNC_OUTPUT
147
148 if [ "$skip" != "$DEST" ]; then
149 '';
150
151 backup_profile_tail = name: profile: ''
152 ssh -o UserKnownHostsFile=/dev/null -o CheckHostIP=no -i ${ssh_key} -p $PORT $DEST sh -c "date > .cache/last_backup"
153 fi # [ "$skip" != "$DEST" ]
154 ##### End ${name} #####
155 '';
156
157 backup_part = profile_name: part_name: part: ''
158 ### ${profile_name} ${part_name} ###
159 LOCAL="${part_name}"
160 REMOTE="${part.remote_folder}"
161
162 if [ ! -d "$BASE/$LOCAL" ]; then
163 mkdir $BASE/$LOCAL
164 fi
165 cd $BASE/$LOCAL
166 cat > $EXCL_FROM <<EOF
167 ${builtins.concatStringsSep "\n" part.exclude_from}
168 EOF
169 cat > $FILES_FROM <<EOF
170 ${builtins.concatStringsSep "\n" part.files_from}
171 EOF
172
173 OUT=$RSYNC_OUTPUT/$LOCAL
174 ${pkgs.rsync}/bin/rsync -XAavbrz --fake-super -e "ssh -o UserKnownHostsFile=/dev/null -o CheckHostIP=no -i ${ssh_key} -p $PORT" --numeric-ids --delete \
175 --backup-dir=$BAK_BASE/$LOCAL \${
176 lib.optionalString (part.args != null) "\n ${part.args} \\"}${
177 lib.optionalString (builtins.length part.exclude_from > 0) "\n --exclude-from=$EXCL_FROM \\"}${
178 lib.optionalString (builtins.length part.files_from > 0) "\n --files-from=$FILES_FROM \\"}
179 $DEST:$REMOTE . > $OUT || true
180 ### End ${profile_name} ${part_name} ###
181 '';
182 in
183 {
184 options.services.rsyncBackup = {
185 mountpoint = lib.mkOption {
186 type = lib.types.path;
187 description = "Path to the base folder for backups";
188 };
189 mailto = lib.mkOption {
190 type = lib.types.nullOr lib.types.str;
191 default = null;
192 description = "E-mail to send the report to";
193 };
194 profiles = lib.mkOption {
195 type = lib.types.attrsOf profileModule;
196 default = {};
197 description = ''
198 Profiles to backup
199 '';
200 };
201 ssh_key_public = lib.mkOption {
202 type = lib.types.str;
203 description = "Public key for the backup";
204 };
205 ssh_key_private = lib.mkOption {
206 type = lib.types.str;
207 description = "Private key for the backup";
208 };
209 };
210
211 config = lib.mkIf (builtins.length (builtins.attrNames cfg.profiles) > 0) {
212 # FIXME: monitoring to check that backup is less than 14h old
213 users.users.backup = {
214 isSystemUser = true;
215 uid = config.ids.uids.backup;
216 group = "backup";
217 extraGroups = [ "keys" ];
218 };
219
220 users.groups.backup = {
221 gid = config.ids.gids.backup;
222 };
223
224 services.cron.systemCronJobs = let
225 backup = pkgs.writeScript "backup.sh" (builtins.concatStringsSep "\n" ([
226 (backup_head cfg.mailto)
227 ] ++ lib.mapAttrsToList backup_profile cfg.profiles));
228 in [
229 ''
230 25 3,15 * * * backup ${backup}
231 ''
232 ];
233
234 programs.ssh.knownHosts = lib.attrsets.mapAttrs' (name: profile: lib.attrsets.nameValuePair name {
235 hostNames = [ profile.host ];
236 publicKey = "${profile.host_key_type} ${profile.host_key}";
237 }) cfg.profiles;
238
239 system.activationScripts.rsyncBackup = {
240 deps = [ "users" ];
241 text = builtins.concatStringsSep "\n" (map (v: ''
242 install -m 0700 -o backup -g backup -d ${cfg.mountpoint}/${v} ${cfg.mountpoint}/${v}/older ${cfg.mountpoint}/${v}/rsync_output
243 '') (builtins.attrNames cfg.profiles)
244 );
245 };
246
247 secrets.keys = [
248 {
249 dest = "rsync_backup/identity";
250 user = "backup";
251 group = "backup";
252 permissions = "0400";
253 text = cfg.ssh_key_private;
254 }
255 {
256 dest = "rsync_backup/identity.pub";
257 user = "backup";
258 group = "backup";
259 permissions = "0444";
260 text = cfg.ssh_key_public;
261 }
262 ];
263 };
264 }