1import ./make-test-python.nix (
2 { pkgs, ... }:
3
4 let
5 passphrase = "supersecret";
6 dataDir = "/ran:dom/data";
7 subDir = "not_anything_here";
8 excludedSubDirFile = "not_this_file_either";
9 excludeFile = "not_this_file";
10 keepFile = "important_file";
11 keepFileData = "important_data";
12 localRepo = "/root/back:up";
13 # a repository on a file system which is not mounted automatically
14 localRepoMount = "/noAutoMount";
15 archiveName = "my_archive";
16 remoteRepo = "borg@server:."; # No need to specify path
17 privateKey = pkgs.writeText "id_ed25519" ''
18 -----BEGIN OPENSSH PRIVATE KEY-----
19 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
20 QyNTUxOQAAACBx8UB04Q6Q/fwDFjakHq904PYFzG9pU2TJ9KXpaPMcrwAAAJB+cF5HfnBe
21 RwAAAAtzc2gtZWQyNTUxOQAAACBx8UB04Q6Q/fwDFjakHq904PYFzG9pU2TJ9KXpaPMcrw
22 AAAEBN75NsJZSpt63faCuaD75Unko0JjlSDxMhYHAPJk2/xXHxQHThDpD9/AMWNqQer3Tg
23 9gXMb2lTZMn0pelo8xyvAAAADXJzY2h1ZXR6QGt1cnQ=
24 -----END OPENSSH PRIVATE KEY-----
25 '';
26 publicKey = ''
27 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHHxQHThDpD9/AMWNqQer3Tg9gXMb2lTZMn0pelo8xyv root@client
28 '';
29 privateKeyAppendOnly = pkgs.writeText "id_ed25519" ''
30 -----BEGIN OPENSSH PRIVATE KEY-----
31 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
32 QyNTUxOQAAACBacZuz1ELGQdhI7PF6dGFafCDlvh8pSEc4cHjkW0QjLwAAAJC9YTxxvWE8
33 cQAAAAtzc2gtZWQyNTUxOQAAACBacZuz1ELGQdhI7PF6dGFafCDlvh8pSEc4cHjkW0QjLw
34 AAAEAAhV7wTl5dL/lz+PF/d4PnZXuG1Id6L/mFEiGT1tZsuFpxm7PUQsZB2Ejs8Xp0YVp8
35 IOW+HylIRzhweORbRCMvAAAADXJzY2h1ZXR6QGt1cnQ=
36 -----END OPENSSH PRIVATE KEY-----
37 '';
38 publicKeyAppendOnly = ''
39 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFpxm7PUQsZB2Ejs8Xp0YVp8IOW+HylIRzhweORbRCMv root@client
40 '';
41
42 in
43 {
44 name = "borgbackup";
45 meta = with pkgs.lib; {
46 maintainers = with maintainers; [ dotlambda ];
47 };
48
49 nodes = {
50 client =
51 { ... }:
52 {
53 virtualisation.fileSystems.${localRepoMount} = {
54 device = "tmpfs";
55 fsType = "tmpfs";
56 options = [ "noauto" ];
57 };
58
59 services.borgbackup.jobs = {
60
61 local = {
62 paths = dataDir;
63 repo = localRepo;
64 preHook = ''
65 # Don't append a timestamp
66 archiveName="${archiveName}"
67 '';
68 encryption = {
69 mode = "repokey";
70 inherit passphrase;
71 };
72 compression = "auto,zlib,9";
73 prune.keep = {
74 within = "1y";
75 yearly = 5;
76 };
77 exclude = [ "*/${excludeFile}" ];
78 extraCreateArgs = [
79 "--exclude-caches"
80 "--exclude-if-present"
81 ".dont backup"
82 ];
83 postHook = "echo post";
84 startAt = [ ]; # Do not run automatically
85 };
86
87 localMount = {
88 paths = dataDir;
89 repo = localRepoMount;
90 encryption.mode = "none";
91 startAt = [ ];
92 };
93
94 remote = {
95 paths = dataDir;
96 repo = remoteRepo;
97 encryption.mode = "none";
98 startAt = [ ];
99 environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519";
100 };
101
102 remoteAppendOnly = {
103 paths = dataDir;
104 repo = remoteRepo;
105 encryption.mode = "none";
106 startAt = [ ];
107 environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519.appendOnly";
108 };
109
110 commandSuccess = {
111 dumpCommand = pkgs.writeScript "commandSuccess" ''
112 echo -n test
113 '';
114 repo = remoteRepo;
115 encryption.mode = "none";
116 startAt = [ ];
117 environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519";
118 };
119
120 commandFail = {
121 dumpCommand = "${pkgs.coreutils}/bin/false";
122 repo = remoteRepo;
123 encryption.mode = "none";
124 startAt = [ ];
125 environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519";
126 };
127
128 sleepInhibited = {
129 inhibitsSleep = true;
130 # Blocks indefinitely while "backing up" so that we can try to suspend the local system while it's hung
131 dumpCommand = pkgs.writeScript "sleepInhibited" ''
132 cat /dev/zero
133 '';
134 repo = remoteRepo;
135 encryption.mode = "none";
136 startAt = [ ];
137 environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519";
138 };
139
140 };
141 };
142
143 server =
144 { ... }:
145 {
146 services.openssh = {
147 enable = true;
148 settings = {
149 PasswordAuthentication = false;
150 KbdInteractiveAuthentication = false;
151 };
152 };
153
154 services.borgbackup.repos.repo1 = {
155 authorizedKeys = [ publicKey ];
156 path = "/data/borgbackup";
157 };
158
159 # Second repo to make sure the authorizedKeys options are merged correctly
160 services.borgbackup.repos.repo2 = {
161 authorizedKeysAppendOnly = [ publicKeyAppendOnly ];
162 path = "/data/borgbackup";
163 quota = ".5G";
164 };
165 };
166 };
167
168 testScript = ''
169 start_all()
170
171 client.fail('test -d "${remoteRepo}"')
172
173 client.succeed(
174 "cp ${privateKey} /root/id_ed25519"
175 )
176 client.succeed("chmod 0600 /root/id_ed25519")
177 client.succeed(
178 "cp ${privateKeyAppendOnly} /root/id_ed25519.appendOnly"
179 )
180 client.succeed("chmod 0600 /root/id_ed25519.appendOnly")
181
182 client.succeed("mkdir -p ${dataDir}/${subDir}")
183 client.succeed("touch ${dataDir}/${excludeFile}")
184 client.succeed("touch '${dataDir}/${subDir}/.dont backup'")
185 client.succeed("touch ${dataDir}/${subDir}/${excludedSubDirFile}")
186 client.succeed("echo '${keepFileData}' > ${dataDir}/${keepFile}")
187
188 with subtest("local"):
189 borg = "BORG_PASSPHRASE='${passphrase}' borg"
190 client.systemctl("start --wait borgbackup-job-local")
191 client.fail("systemctl is-failed borgbackup-job-local")
192 # Make sure exactly one archive has been created
193 assert int(client.succeed("{} list '${localRepo}' | wc -l".format(borg))) > 0
194 # Make sure excludeFile has been excluded
195 client.fail(
196 "{} list '${localRepo}::${archiveName}' | grep -qF '${excludeFile}'".format(borg)
197 )
198 # Make sure excludedSubDirFile has been excluded
199 client.fail(
200 "{} list '${localRepo}::${archiveName}' | grep -qF '${subDir}/${excludedSubDirFile}".format(borg)
201 )
202 # Make sure keepFile has the correct content
203 client.succeed("{} extract '${localRepo}::${archiveName}'".format(borg))
204 assert "${keepFileData}" in client.succeed("cat ${dataDir}/${keepFile}")
205 # Make sure the same is true when using `borg mount`
206 client.succeed(
207 "mkdir -p /mnt/borg && {} mount '${localRepo}::${archiveName}' /mnt/borg".format(
208 borg
209 )
210 )
211 assert "${keepFileData}" in client.succeed(
212 "cat /mnt/borg/${dataDir}/${keepFile}"
213 )
214
215 with subtest("localMount"):
216 # the file system for the repo should not be already mounted
217 client.fail("mount | grep ${localRepoMount}")
218 # ensure trying to write to the mountpoint before the fs is mounted fails
219 client.succeed("chattr +i ${localRepoMount}")
220 borg = "borg"
221 client.systemctl("start --wait borgbackup-job-localMount")
222 client.fail("systemctl is-failed borgbackup-job-localMount")
223 # Make sure exactly one archive has been created
224 assert int(client.succeed("{} list '${localRepoMount}' | wc -l".format(borg))) > 0
225
226 with subtest("remote"):
227 borg = "BORG_RSH='ssh -oStrictHostKeyChecking=no -i /root/id_ed25519' borg"
228 server.wait_for_unit("sshd.service")
229 client.wait_for_unit("network.target")
230 client.systemctl("start --wait borgbackup-job-remote")
231 client.fail("systemctl is-failed borgbackup-job-remote")
232
233 # Make sure we can't access repos other than the specified one
234 client.fail("{} list borg\@server:wrong".format(borg))
235
236 # TODO: Make sure that data is actually deleted
237
238 with subtest("remoteAppendOnly"):
239 borg = (
240 "BORG_RSH='ssh -oStrictHostKeyChecking=no -i /root/id_ed25519.appendOnly' borg"
241 )
242 server.wait_for_unit("sshd.service")
243 client.wait_for_unit("network.target")
244 client.systemctl("start --wait borgbackup-job-remoteAppendOnly")
245 client.fail("systemctl is-failed borgbackup-job-remoteAppendOnly")
246
247 # Make sure we can't access repos other than the specified one
248 client.fail("{} list borg\@server:wrong".format(borg))
249
250 # TODO: Make sure that data is not actually deleted
251
252 with subtest("commandSuccess"):
253 server.wait_for_unit("sshd.service")
254 client.wait_for_unit("network.target")
255 client.systemctl("start --wait borgbackup-job-commandSuccess")
256 client.fail("systemctl is-failed borgbackup-job-commandSuccess")
257 id = client.succeed("borg-job-commandSuccess list | tail -n1 | cut -d' ' -f1").strip()
258 client.succeed(f"borg-job-commandSuccess extract ::{id} stdin")
259 assert "test" == client.succeed("cat stdin")
260
261 with subtest("commandFail"):
262 server.wait_for_unit("sshd.service")
263 client.wait_for_unit("network.target")
264 client.systemctl("start --wait borgbackup-job-commandFail")
265 client.succeed("systemctl is-failed borgbackup-job-commandFail")
266
267 with subtest("sleepInhibited"):
268 server.wait_for_unit("sshd.service")
269 client.wait_for_unit("network.target")
270 client.fail("systemd-inhibit --list | grep -q borgbackup")
271 client.systemctl("start borgbackup-job-sleepInhibited")
272 client.wait_until_succeeds("systemd-inhibit --list | grep -q borgbackup")
273 client.systemctl("stop borgbackup-job-sleepInhibited")
274 '';
275 }
276)