1 { lib, pkgs, config, myconfig, ... }:
3 partModule = lib.types.submodule {
5 remote_folder = lib.mkOption {
11 exclude_from = lib.mkOption {
12 type = lib.types.listOf lib.types.path;
15 Paths to exclude from the backup
18 files_from = lib.mkOption {
19 type = lib.types.listOf lib.types.path;
22 Paths to take for the backup
23 (if empty: whole folder minus exclude_from)
27 type = lib.types.nullOr lib.types.str;
30 additional arguments for rsync
35 profileModule = lib.types.submodule {
41 Number of backups to keep
44 login = lib.mkOption {
63 host_key = lib.mkOption {
66 Host key to use as known host
69 host_key_type = lib.mkOption {
75 parts = lib.mkOption {
76 type = lib.types.attrsOf partModule;
78 folders to backup in the host
83 cfg = config.services.rsyncBackup;
85 ssh_key = config.secrets.fullPaths."rsync_backup/identity";
87 backup_head = mailto: ''
88 #!${pkgs.stdenv.shell}
94 ${lib.optionalString (mailto != null) ''
96 if [ -s "$TMP_STDERR" ]; then
97 cat "$TMP_STDERR" | ${pkgs.mailutils}/bin/mail -s "save_distant rsync error" "$MAILTO"
100 rm -f $TMP_STDERR $EXCL_FROM $FILES_FROM
105 exec 2> "$TMP_STDERR"
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)]);
116 backup_profile_head = name: profile: ''
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}
127 -o PreferredAuthentications=publickey \
128 -o StrictHostKeyChecking=yes \
129 -o ClearAllForwardings=yes \
133 echo "Fichier de verrouillage backup sur $DEST ou impossible de se connecter" >&2
137 rm -rf ''${OLD_BAK_BASE}''${NBR}
138 for j in `seq -w $(($NBR-1)) -1 0`; do
139 [ ! -d ''${OLD_BAK_BASE}$j ] && continue
140 mv ''${OLD_BAK_BASE}$j ''${OLD_BAK_BASE}$(($j+1))
143 mv $RSYNC_OUTPUT $BAK_BASE
146 if [ "$skip" != "$DEST" ]; then
149 backup_profile_tail = name: profile: ''
150 ssh -i ${ssh_key} -p $PORT $DEST sh -c "date > .cache/last_backup"
151 fi # [ "$skip" != "$DEST" ]
152 ##### End ${name} #####
155 backup_part = profile_name: part_name: part: ''
156 ### ${profile_name} ${part_name} ###
158 REMOTE="${part.remote_folder}"
160 if [ ! -d "$BASE/$LOCAL" ]; then
164 cat > $EXCL_FROM <<EOF
165 ${builtins.concatStringsSep "\n" part.exclude_from}
167 cat > $FILES_FROM <<EOF
168 ${builtins.concatStringsSep "\n" part.files_from}
171 OUT=$RSYNC_OUTPUT/$LOCAL
172 ${pkgs.rsync}/bin/rsync -XAavbrz --fake-super -e "ssh -i ${ssh_key} -p $PORT" --numeric-ids --delete \
173 --backup-dir=$BAK_BASE/$LOCAL \${
174 lib.optionalString (part.args != null) "\n ${part.args} \\"}${
175 lib.optionalString (builtins.length part.exclude_from > 0) "\n --exclude-from=$EXCL_FROM \\"}${
176 lib.optionalString (builtins.length part.files_from > 0) "\n --files-from=$FILES_FROM \\"}
177 $DEST:$REMOTE . > $OUT || true
178 ### End ${profile_name} ${part_name} ###
182 options.services.rsyncBackup = {
183 mountpoint = lib.mkOption {
184 type = lib.types.path;
185 description = "Path to the base folder for backups";
187 mailto = lib.mkOption {
188 type = lib.types.nullOr lib.types.str;
190 description = "E-mail to send the report to";
192 profiles = lib.mkOption {
193 type = lib.types.attrsOf profileModule;
199 ssh_key_public = lib.mkOption {
200 type = lib.types.str;
201 description = "Public key for the backup";
203 ssh_key_private = lib.mkOption {
204 type = lib.types.str;
205 description = "Private key for the backup";
209 config = lib.mkIf (builtins.length (builtins.attrNames cfg.profiles) > 0) {
210 # FIXME: monitoring to check that backup is less than 14h old
211 users.users.backup = {
213 uid = config.ids.uids.backup;
215 extraGroups = [ "keys" ];
218 users.groups.backup = {
219 gid = config.ids.gids.backup;
222 services.cron.systemCronJobs = let
223 backup = pkgs.writeScript "backup.sh" (builtins.concatStringsSep "\n" ([
224 (backup_head cfg.mailto)
225 ] ++ lib.mapAttrsToList backup_profile cfg.profiles));
228 25 3,15 * * * backup ${backup}
232 programs.ssh.knownHosts = lib.attrsets.mapAttrs' (name: profile: lib.attrsets.nameValuePair name {
233 hostNames = [ profile.host ];
234 publicKey = "${profile.host_key_type} ${profile.host_key}";
237 system.activationScripts.rsyncBackup = {
239 text = builtins.concatStringsSep "\n" (map (v: ''
240 install -m 0700 -o backup -g backup -d ${cfg.mountpoint}/${v} ${cfg.mountpoint}/${v}/older ${cfg.mountpoint}/${v}/rsync_output
241 '') (builtins.attrNames cfg.profiles)
247 dest = "rsync_backup/identity";
250 permissions = "0400";
251 text = cfg.ssh_key_private;
254 dest = "rsync_backup/identity.pub";
257 permissions = "0444";
258 text = cfg.ssh_key_public;