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