1{ pkgs, ... }:
2let
3 inherit (import ./ssh-keys.nix pkgs)
4 snakeOilEd25519PrivateKey
5 snakeOilEd25519PublicKey
6 ;
7
8 remoteRepository = "/root/restic-backup";
9 remoteFromFileRepository = "/root/restic-backup-from-file";
10 remoteFromCommandRepository = "/root/restic-backup-from-command";
11 remoteInhibitTestRepository = "/root/restic-backup-inhibit-test";
12 remoteNoInitRepository = "/root/restic-backup-no-init";
13 rcloneRepository = "rclone:local:/root/restic-rclone-backup";
14 sftpRepository = "sftp:alice@sftp:backups/test";
15
16 backupPrepareCommand = ''
17 touch /root/backupPrepareCommand
18 test ! -e /root/backupCleanupCommand
19 '';
20
21 backupCleanupCommand = ''
22 rm /root/backupPrepareCommand
23 touch /root/backupCleanupCommand
24 '';
25
26 testDir = pkgs.stdenvNoCC.mkDerivation {
27 name = "test-files-to-backup";
28 unpackPhase = "true";
29 installPhase = ''
30 mkdir $out
31 echo some_file > $out/some_file
32 echo some_other_file > $out/some_other_file
33 mkdir $out/a_dir
34 echo a_file > $out/a_dir/a_file
35 echo a_file_2 > $out/a_dir/a_file_2
36 '';
37 };
38
39 passwordFile = "${pkgs.writeText "password" "correcthorsebatterystaple"}";
40 paths = [ "/opt" ];
41 exclude = [ "/opt/excluded_file_*" ];
42 pruneOpts = [
43 "--keep-daily 2"
44 "--keep-weekly 1"
45 "--keep-monthly 1"
46 "--keep-yearly 99"
47 ];
48 commandString = "testing";
49 command = [
50 "echo"
51 "-n"
52 commandString
53 ];
54in
55{
56 name = "restic";
57
58 meta = with pkgs.lib.maintainers; {
59 maintainers = [
60 bbigras
61 i077
62 ];
63 };
64
65 nodes = {
66 sftp =
67 # Copied from openssh.nix
68 { pkgs, ... }:
69 {
70 services.openssh = {
71 enable = true;
72 extraConfig = ''
73 Match Group sftponly
74 ChrootDirectory /srv/sftp
75 ForceCommand internal-sftp
76 '';
77 };
78
79 users.groups = {
80 sftponly = { };
81 };
82 users.users = {
83 alice = {
84 isNormalUser = true;
85 createHome = false;
86 group = "sftponly";
87 shell = "/run/current-system/sw/bin/nologin";
88 openssh.authorizedKeys.keys = [ snakeOilEd25519PublicKey ];
89 };
90 };
91 };
92
93 restic =
94 { pkgs, ... }:
95 {
96 services.restic.backups = {
97 remotebackup = {
98 inherit
99 passwordFile
100 paths
101 exclude
102 pruneOpts
103 backupPrepareCommand
104 backupCleanupCommand
105 ;
106 repository = remoteRepository;
107 initialize = true;
108 timerConfig = null; # has no effect here, just checking that it doesn't break the service
109 };
110 remote-sftp = {
111 inherit
112 passwordFile
113 paths
114 exclude
115 pruneOpts
116 ;
117 repository = sftpRepository;
118 initialize = true;
119 timerConfig = null; # has no effect here, just checking that it doesn't break the service
120 extraOptions = [
121 "sftp.command='ssh alice@sftp -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -s sftp'"
122 ];
123 };
124 remote-from-file-backup = {
125 inherit passwordFile exclude pruneOpts;
126 initialize = true;
127 repositoryFile = pkgs.writeText "repositoryFile" remoteFromFileRepository;
128 paths = [
129 "/opt/a_dir/a_file"
130 "/opt/a_dir/a_file_2"
131 ];
132 dynamicFilesFrom = ''
133 find /opt -mindepth 1 -maxdepth 1 ! -name a_dir # all files in /opt except for a_dir
134 '';
135 };
136 remote-from-command-backup = {
137 inherit
138 passwordFile
139 pruneOpts
140 command
141 ;
142 initialize = true;
143 repository = remoteFromCommandRepository;
144 };
145 inhibit-test = {
146 inherit
147 passwordFile
148 paths
149 exclude
150 pruneOpts
151 ;
152 repository = remoteInhibitTestRepository;
153 initialize = true;
154 inhibitsSleep = true;
155 };
156 remote-noinit-backup = {
157 inherit
158 passwordFile
159 exclude
160 pruneOpts
161 paths
162 ;
163 initialize = false;
164 repository = remoteNoInitRepository;
165 };
166 rclonebackup = {
167 inherit
168 passwordFile
169 paths
170 exclude
171 pruneOpts
172 ;
173 initialize = true;
174 repository = rcloneRepository;
175 rcloneConfig = {
176 type = "local";
177 one_file_system = true;
178 };
179
180 # This gets overridden by rcloneConfig.type
181 rcloneConfigFile = pkgs.writeText "rclone.conf" ''
182 [local]
183 type=ftp
184 '';
185 };
186 remoteprune = {
187 inherit passwordFile;
188 repository = remoteRepository;
189 pruneOpts = [ "--keep-last 1" ];
190 };
191 custompackage = {
192 inherit passwordFile paths;
193 repository = "some-fake-repository";
194 package = pkgs.writeShellScriptBin "restic" ''
195 echo "$@" >> /root/fake-restic.log;
196 '';
197
198 pruneOpts = [ "--keep-last 1" ];
199 checkOpts = [ "--some-check-option" ];
200 };
201 };
202
203 environment.sessionVariables.RCLONE_CONFIG_LOCAL_TYPE = "local";
204 };
205 };
206
207 testScript = ''
208 restic.start()
209 sftp.start()
210 restic.wait_for_unit("dbus.socket")
211 sftp.wait_for_unit("sshd.service")
212
213 restic.systemctl("start network-online.target")
214 restic.wait_for_unit("network-online.target")
215
216 sftp.succeed(
217 "mkdir -p /srv/sftp/backups",
218 "chown alice:sftponly /srv/sftp/backups",
219 "chmod 0755 /srv/sftp/backups",
220 )
221
222 restic.succeed(
223 "mkdir -p /root/.ssh/",
224 "cat ${snakeOilEd25519PrivateKey} > /root/.ssh/id_ed25519",
225 "chmod 0600 /root/.ssh/id_ed25519",
226 )
227
228 restic.fail(
229 "restic-remotebackup snapshots",
230 "restic-remote-sftp snapshots",
231 'restic-remote-from-file-backup snapshots"',
232 "restic-rclonebackup snapshots",
233 "grep 'backup.* /opt' /root/fake-restic.log",
234 )
235 restic.succeed(
236 # set up
237 "cp -rT ${testDir} /opt",
238 "touch /opt/excluded_file_1 /opt/excluded_file_2",
239 "mkdir -p /root/restic-rclone-backup",
240 )
241
242 restic.fail(
243 # test that noinit backup in fact does not initialize the repository
244 # and thus fails without a pre-initialized repository
245 "systemctl start restic-backups-remote-noinit-backup.service",
246 )
247
248 restic.succeed(
249 # test that remotebackup runs custom commands and produces a snapshot
250 "timedatectl set-time '2016-12-13 13:45'",
251 "systemctl start restic-backups-remotebackup.service",
252 "rm /root/backupCleanupCommand",
253 'restic-remotebackup snapshots --json | ${pkgs.jq}/bin/jq "length | . == 1"',
254 )
255
256 restic.succeed(
257 # test that remotebackup runs custom commands and produces a snapshot
258 "timedatectl set-time '2016-12-13 13:45'",
259 "systemctl start restic-backups-remotebackup.service",
260 "rm /root/backupCleanupCommand",
261 'restic-remotebackup snapshots --json | ${pkgs.jq}/bin/jq "length | . == 1"',
262
263 # test that restoring that snapshot produces the same directory
264 "mkdir /tmp/restore-1",
265 "restic-remotebackup restore latest -t /tmp/restore-1",
266 "diff -ru ${testDir} /tmp/restore-1/opt",
267
268 # test that remote-from-file-backup produces a snapshot
269 "systemctl start restic-backups-remote-from-file-backup.service",
270 'restic-remote-from-file-backup snapshots --json | ${pkgs.jq}/bin/jq "length | . == 1"',
271 "mkdir /tmp/restore-2",
272 "restic-remote-from-file-backup restore latest -t /tmp/restore-2",
273 "diff -ru ${testDir} /tmp/restore-2/opt",
274
275 # test that remote-noinit-backup produces a snapshot once initialized
276 "restic-remote-noinit-backup init",
277 "systemctl start restic-backups-remote-noinit-backup.service",
278 'restic-remote-noinit-backup snapshots --json | ${pkgs.jq}/bin/jq "length | . == 1"',
279
280 # test that restoring that snapshot produces the same directory
281 "mkdir /tmp/restore-3",
282 "${pkgs.restic}/bin/restic -r ${remoteRepository} -p ${passwordFile} restore latest -t /tmp/restore-3",
283 "diff -ru ${testDir} /tmp/restore-3/opt",
284
285 # test that remote-from-command-backup produces a snapshot, with the expected contents
286 "systemctl start restic-backups-remote-from-command-backup.service",
287 'restic-remote-from-command-backup snapshots --json | ${pkgs.jq}/bin/jq "length | . == 1"',
288 '[[ $(restic-remote-from-command-backup dump --path /stdin latest stdin) == ${commandString} ]]',
289
290 # test that rclonebackup produces a snapshot
291 "systemctl start restic-backups-rclonebackup.service",
292 'restic-rclonebackup snapshots --json | ${pkgs.jq}/bin/jq "length | . == 1"',
293
294 # test that custompackage runs both `restic backup` and `restic check` with reasonable commandlines
295 "systemctl start restic-backups-custompackage.service",
296 "grep 'backup' /root/fake-restic.log",
297 "grep 'check.* --some-check-option' /root/fake-restic.log",
298
299 # test that we can create four snapshots in remotebackup and rclonebackup
300 "timedatectl set-time '2017-12-13 13:45'",
301 "systemctl start restic-backups-remotebackup.service",
302 "rm /root/backupCleanupCommand",
303 "systemctl start restic-backups-rclonebackup.service",
304
305 "timedatectl set-time '2018-12-13 13:45'",
306 "systemctl start restic-backups-remotebackup.service",
307 "rm /root/backupCleanupCommand",
308 "systemctl start restic-backups-rclonebackup.service",
309
310 "timedatectl set-time '2018-12-14 13:45'",
311 "systemctl start restic-backups-remotebackup.service",
312 "rm /root/backupCleanupCommand",
313 "systemctl start restic-backups-rclonebackup.service",
314
315 "timedatectl set-time '2018-12-15 13:45'",
316 "systemctl start restic-backups-remotebackup.service",
317 "rm /root/backupCleanupCommand",
318 "systemctl start restic-backups-rclonebackup.service",
319
320 "timedatectl set-time '2018-12-16 13:45'",
321 "systemctl start restic-backups-remotebackup.service",
322 "rm /root/backupCleanupCommand",
323 "systemctl start restic-backups-rclonebackup.service",
324
325 'restic-remotebackup snapshots --json | ${pkgs.jq}/bin/jq "length | . == 4"',
326 'restic-rclonebackup snapshots --json | ${pkgs.jq}/bin/jq "length | . == 4"',
327
328 # test that SFTP backup works by copying from the remotebackup
329 'restic-remote-sftp init --from-repo ${remoteRepository} --from-password-file ${passwordFile} --copy-chunker-params',
330 'restic-remote-sftp copy --from-repo ${remoteRepository} --from-password-file ${passwordFile}',
331 'restic-remote-sftp snapshots --json | ${pkgs.jq}/bin/jq "length | . == 4"',
332
333 # test that remoteprune brings us back to 1 snapshot in remotebackup
334 "systemctl start restic-backups-remoteprune.service",
335 'restic-remotebackup snapshots --json | ${pkgs.jq}/bin/jq "length | . == 1"',
336
337 # test that remoteprune brings us back to 1 snapshot in remotebackup
338 "systemctl start restic-backups-remoteprune.service",
339 'restic-remotebackup snapshots --json | ${pkgs.jq}/bin/jq "length | . == 1"',
340 )
341
342 # test that the inhibit option is working
343 restic.systemctl("start --no-block restic-backups-inhibit-test.service")
344 restic.wait_until_succeeds(
345 "systemd-inhibit --no-legend --no-pager | grep -q restic",
346 5
347 )
348 # test that the inhibit option is working
349 restic.systemctl("start --no-block restic-backups-inhibit-test.service")
350 restic.wait_until_succeeds(
351 "systemd-inhibit --no-legend --no-pager | grep -q restic",
352 5
353 )
354 '';
355}