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 postHook = "echo post";
83 startAt = [ ]; # Do not run automatically
84 };
85
86 localMount = {
87 paths = dataDir;
88 repo = localRepoMount;
89 encryption.mode = "none";
90 startAt = [ ];
91 };
92
93 remote = {
94 paths = dataDir;
95 repo = remoteRepo;
96 encryption.mode = "none";
97 startAt = [ ];
98 environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519";
99 };
100
101 remoteAppendOnly = {
102 paths = dataDir;
103 repo = remoteRepo;
104 encryption.mode = "none";
105 startAt = [ ];
106 environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519.appendOnly";
107 };
108
109 commandSuccess = {
110 dumpCommand = pkgs.writeScript "commandSuccess" ''
111 echo -n test
112 '';
113 repo = remoteRepo;
114 encryption.mode = "none";
115 startAt = [ ];
116 environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519";
117 };
118
119 commandFail = {
120 dumpCommand = "${pkgs.coreutils}/bin/false";
121 repo = remoteRepo;
122 encryption.mode = "none";
123 startAt = [ ];
124 environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519";
125 };
126
127 sleepInhibited = {
128 inhibitsSleep = true;
129 # Blocks indefinitely while "backing up" so that we can try to suspend the local system while it's hung
130 dumpCommand = pkgs.writeScript "sleepInhibited" ''
131 cat /dev/zero
132 '';
133 repo = remoteRepo;
134 encryption.mode = "none";
135 startAt = [ ];
136 environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519";
137 };
138
139 };
140 };
141
142 server =
143 { ... }:
144 {
145 services.openssh = {
146 enable = true;
147 settings = {
148 PasswordAuthentication = false;
149 KbdInteractiveAuthentication = false;
150 };
151 };
152
153 services.borgbackup.repos.repo1 = {
154 authorizedKeys = [ publicKey ];
155 path = "/data/borgbackup";
156 };
157
158 # Second repo to make sure the authorizedKeys options are merged correctly
159 services.borgbackup.repos.repo2 = {
160 authorizedKeysAppendOnly = [ publicKeyAppendOnly ];
161 path = "/data/borgbackup";
162 quota = ".5G";
163 };
164 };
165 };
166
167 testScript = ''
168 start_all()
169
170 client.fail('test -d "${remoteRepo}"')
171
172 client.succeed(
173 "cp ${privateKey} /root/id_ed25519"
174 )
175 client.succeed("chmod 0600 /root/id_ed25519")
176 client.succeed(
177 "cp ${privateKeyAppendOnly} /root/id_ed25519.appendOnly"
178 )
179 client.succeed("chmod 0600 /root/id_ed25519.appendOnly")
180
181 client.succeed("mkdir -p ${dataDir}/${subDir}")
182 client.succeed("touch ${dataDir}/${excludeFile}")
183 client.succeed("touch '${dataDir}/${subDir}/.dont backup'")
184 client.succeed("touch ${dataDir}/${subDir}/${excludedSubDirFile}")
185 client.succeed("echo '${keepFileData}' > ${dataDir}/${keepFile}")
186
187 with subtest("local"):
188 borg = "BORG_PASSPHRASE='${passphrase}' borg"
189 client.systemctl("start --wait borgbackup-job-local")
190 client.fail("systemctl is-failed borgbackup-job-local")
191 # Make sure exactly one archive has been created
192 assert int(client.succeed("{} list '${localRepo}' | wc -l".format(borg))) > 0
193 # Make sure excludeFile has been excluded
194 client.fail(
195 "{} list '${localRepo}::${archiveName}' | grep -qF '${excludeFile}'".format(borg)
196 )
197 # Make sure excludedSubDirFile has been excluded
198 client.fail(
199 "{} list '${localRepo}::${archiveName}' | grep -qF '${subDir}/${excludedSubDirFile}".format(borg)
200 )
201 # Make sure keepFile has the correct content
202 client.succeed("{} extract '${localRepo}::${archiveName}'".format(borg))
203 assert "${keepFileData}" in client.succeed("cat ${dataDir}/${keepFile}")
204 # Make sure the same is true when using `borg mount`
205 client.succeed(
206 "mkdir -p /mnt/borg && {} mount '${localRepo}::${archiveName}' /mnt/borg".format(
207 borg
208 )
209 )
210 assert "${keepFileData}" in client.succeed(
211 "cat /mnt/borg/${dataDir}/${keepFile}"
212 )
213
214 with subtest("localMount"):
215 # the file system for the repo should not be already mounted
216 client.fail("mount | grep ${localRepoMount}")
217 # ensure trying to write to the mountpoint before the fs is mounted fails
218 client.succeed("chattr +i ${localRepoMount}")
219 borg = "borg"
220 client.systemctl("start --wait borgbackup-job-localMount")
221 client.fail("systemctl is-failed borgbackup-job-localMount")
222 # Make sure exactly one archive has been created
223 assert int(client.succeed("{} list '${localRepoMount}' | wc -l".format(borg))) > 0
224
225 with subtest("remote"):
226 borg = "BORG_RSH='ssh -oStrictHostKeyChecking=no -i /root/id_ed25519' borg"
227 server.wait_for_unit("sshd.service")
228 client.wait_for_unit("network.target")
229 client.systemctl("start --wait borgbackup-job-remote")
230 client.fail("systemctl is-failed borgbackup-job-remote")
231
232 # Make sure we can't access repos other than the specified one
233 client.fail("{} list borg\@server:wrong".format(borg))
234
235 # TODO: Make sure that data is actually deleted
236
237 with subtest("remoteAppendOnly"):
238 borg = (
239 "BORG_RSH='ssh -oStrictHostKeyChecking=no -i /root/id_ed25519.appendOnly' borg"
240 )
241 server.wait_for_unit("sshd.service")
242 client.wait_for_unit("network.target")
243 client.systemctl("start --wait borgbackup-job-remoteAppendOnly")
244 client.fail("systemctl is-failed borgbackup-job-remoteAppendOnly")
245
246 # Make sure we can't access repos other than the specified one
247 client.fail("{} list borg\@server:wrong".format(borg))
248
249 # TODO: Make sure that data is not actually deleted
250
251 with subtest("commandSuccess"):
252 server.wait_for_unit("sshd.service")
253 client.wait_for_unit("network.target")
254 client.systemctl("start --wait borgbackup-job-commandSuccess")
255 client.fail("systemctl is-failed borgbackup-job-commandSuccess")
256 id = client.succeed("borg-job-commandSuccess list | tail -n1 | cut -d' ' -f1").strip()
257 client.succeed(f"borg-job-commandSuccess extract ::{id} stdin")
258 assert "test" == client.succeed("cat stdin")
259
260 with subtest("commandFail"):
261 server.wait_for_unit("sshd.service")
262 client.wait_for_unit("network.target")
263 client.systemctl("start --wait borgbackup-job-commandFail")
264 client.succeed("systemctl is-failed borgbackup-job-commandFail")
265
266 with subtest("sleepInhibited"):
267 server.wait_for_unit("sshd.service")
268 client.wait_for_unit("network.target")
269 client.fail("systemd-inhibit --list | grep -q borgbackup")
270 client.systemctl("start borgbackup-job-sleepInhibited")
271 client.wait_until_succeeds("systemd-inhibit --list | grep -q borgbackup")
272 client.systemctl("stop borgbackup-job-sleepInhibited")
273 '';
274}