2 description = "Rsync backups";
5 nixosModule = { lib, pkgs, config, ... }: {
6 options.services.rsyncBackup = {
7 mountpoint = lib.mkOption {
9 description = "Path to the base folder for backups";
11 profiles = lib.mkOption {
12 type = lib.types.attrsOf (lib.types.submodule {
18 Number of backups to keep
21 check_command = lib.mkOption {
25 command to check if backup needs to be done
28 login = lib.mkOption {
47 host_key = lib.mkOption {
50 Host key to use as known host
53 host_key_type = lib.mkOption {
59 parts = lib.mkOption {
60 type = lib.types.attrsOf (lib.types.submodule {
62 remote_folder = lib.mkOption {
63 type = lib.types.path;
68 exclude_from = lib.mkOption {
69 type = lib.types.listOf lib.types.path;
72 Paths to exclude from the backup
75 files_from = lib.mkOption {
76 type = lib.types.listOf lib.types.path;
79 Paths to take for the backup
80 (if empty: whole folder minus exclude_from)
84 type = lib.types.nullOr lib.types.str;
87 additional arguments for rsync
93 folders to backup in the host
103 ssh_key_public = lib.mkOption {
104 type = lib.types.str;
105 description = "Public key for the backup";
107 ssh_key_private = lib.mkOption {
108 type = lib.types.str;
109 description = "Private key for the backup";
115 cfg = config.services.rsyncBackup;
117 lib.mkIf (builtins.length (builtins.attrNames cfg.profiles) > 0) {
118 users.users.backup = {
120 uid = config.ids.uids.backup;
122 extraGroups = [ "keys" ];
125 users.groups.backup = {
126 gid = config.ids.gids.backup;
129 services.cron.systemCronJobs = let
130 ssh_key = cfg.ssh_key_private;
132 #!${pkgs.stdenv.shell}
138 if [ -s "$TMP_STDERR" ]; then
141 rm -f $TMP_STDERR $EXCL_FROM $FILES_FROM
146 exec 2> "$TMP_STDERR"
151 backup_profile_head = name: profile: ''
153 PORT="${profile.port}"
154 DEST="${profile.login}@${profile.host}"
155 BASE="${cfg.mountpoint}/${name}"
156 OLD_BAK_BASE=$BASE/older/j
157 BAK_BASE=''${OLD_BAK_BASE}0
158 RSYNC_OUTPUT=$BASE/rsync_output
159 NBR=${builtins.toString profile.keep}
162 -o PreferredAuthentications=publickey \
163 -o StrictHostKeyChecking=yes \
164 -o ClearAllForwardings=yes \
165 -o UserKnownHostsFile=/dev/null \
169 $DEST ${profile.check_command}; then
170 echo "Fichier de verrouillage backup sur $DEST ou impossible de se connecter" >&2
174 rm -rf ''${OLD_BAK_BASE}''${NBR}
175 for j in `seq -w $(($NBR-1)) -1 0`; do
176 [ ! -d ''${OLD_BAK_BASE}$j ] && continue
177 mv ''${OLD_BAK_BASE}$j ''${OLD_BAK_BASE}$(($j+1))
180 mv $RSYNC_OUTPUT $BAK_BASE
183 if [ "$skip" != "$DEST" ]; then
185 backup_profile_tail = name: profile: ''
186 ssh -o UserKnownHostsFile=/dev/null -o CheckHostIP=no -i ${ssh_key} -p $PORT $DEST sh -c "date > .cache/last_backup"
187 fi # [ "$skip" != "$DEST" ]
188 ##### End ${name} #####
191 backup_part = profile_name: part_name: part: ''
192 ### ${profile_name} ${part_name} ###
194 REMOTE="${part.remote_folder}"
196 if [ ! -d "$BASE/$LOCAL" ]; then
200 cat > $EXCL_FROM <<EOF
201 ${builtins.concatStringsSep "\n" part.exclude_from}
203 cat > $FILES_FROM <<EOF
204 ${builtins.concatStringsSep "\n" part.files_from}
207 OUT=$RSYNC_OUTPUT/$LOCAL
208 ${pkgs.rsync}/bin/rsync --new-compress -XAavbr --fake-super -e "ssh -o UserKnownHostsFile=/dev/null -o CheckHostIP=no -i ${ssh_key} -p $PORT" --numeric-ids --delete \
209 --backup-dir=$BAK_BASE/$LOCAL \${
210 lib.optionalString (part.args != null) "\n ${part.args} \\"}${
211 lib.optionalString (builtins.length part.exclude_from > 0) "\n --exclude-from=$EXCL_FROM \\"}${
212 lib.optionalString (builtins.length part.files_from > 0) "\n --files-from=$FILES_FROM \\"}
213 $DEST:$REMOTE . > $OUT || true
214 ### End ${profile_name} ${part_name} ###
216 backup_profile = name: profile: builtins.concatStringsSep "\n" (
217 [(backup_profile_head name profile)]
218 ++ lib.mapAttrsToList (backup_part name) profile.parts
219 ++ [(backup_profile_tail name profile)]);
221 backup = pkgs.writeScript "backup.sh" (builtins.concatStringsSep "\n" ([
223 ] ++ lib.mapAttrsToList backup_profile cfg.profiles));
226 25 3,15 * * * backup ${backup}
230 programs.ssh.knownHosts = lib.attrsets.mapAttrs' (name: profile: lib.attrsets.nameValuePair name {
231 extraHostNames = [ profile.host ];
232 publicKey = "${profile.host_key_type} ${profile.host_key}";
235 system.activationScripts.rsyncBackup = {
237 text = builtins.concatStringsSep "\n" (map (v: ''
238 install -m 0700 -o backup -g backup -d ${cfg.mountpoint}/${v} ${cfg.mountpoint}/${v}/older ${cfg.mountpoint}/${v}/rsync_output
239 '') (builtins.attrNames cfg.profiles)