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}