1{ lib, pkgs, ... }:
2let
3 inherit (import ../ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
4 backupPath = "/var/lib/pgbackrest";
5in
6{
7 name = "pgbackrest-posix";
8
9 meta = {
10 maintainers = with lib.maintainers; [ wolfgangwalther ];
11 };
12
13 nodes.primary =
14 {
15 pkgs,
16 ...
17 }:
18 {
19 services.openssh.enable = true;
20 users.users.postgres.openssh.authorizedKeys.keys = [
21 snakeOilPublicKey
22 ];
23
24 services.postgresql = {
25 enable = true;
26 initialScript = pkgs.writeText "init.sql" ''
27 CREATE TABLE t(c text);
28 INSERT INTO t VALUES ('hello world');
29 '';
30 # To make sure we're waiting for read-write after recovery.
31 ensureUsers = [
32 {
33 name = "app-user";
34 ensureClauses.login = true;
35 }
36 ];
37 };
38
39 services.pgbackrest = {
40 enable = true;
41 repos.backup = {
42 type = "posix";
43 path = backupPath;
44 host-user = "pgbackrest";
45 };
46 };
47 };
48
49 nodes.backup =
50 {
51 nodes,
52 ...
53 }:
54 {
55 services.openssh.enable = true;
56 users.users.pgbackrest.openssh.authorizedKeys.keys = [
57 snakeOilPublicKey
58 ];
59
60 services.pgbackrest = {
61 enable = true;
62 repos.localhost.path = backupPath;
63
64 stanzas.default = {
65 jobs.future = {
66 schedule = "3000-01-01";
67 type = "full";
68 };
69 instances.primary = {
70 path = nodes.primary.services.postgresql.dataDir;
71 user = "postgres";
72 };
73 };
74
75 # Examples from https://pgbackrest.org/configuration.html#introduction
76 # Not used for the test, except for dumping the config.
77 stanzas.config-format.settings = {
78 start-fast = true;
79 compress-level = 3;
80 buffer-size = "2MiB";
81 db-timeout = 600;
82 db-exclude = [
83 "db1"
84 "db2"
85 "db5"
86 ];
87 tablespace-map = {
88 ts_01 = "/db/ts_01";
89 ts_02 = "/db/ts_02";
90 };
91 };
92 };
93 };
94
95 testScript =
96 { nodes, ... }:
97 ''
98 start_all()
99
100 primary.wait_for_unit("multi-user.target")
101 backup.wait_for_unit("multi-user.target")
102
103 with subtest("config file is written correctly"):
104 from textwrap import dedent
105 have = backup.succeed("cat /etc/pgbackrest/pgbackrest.conf")
106 want = dedent("""\
107 [config-format]
108 buffer-size=2MiB
109 compress-level=3
110 db-exclude=db1
111 db-exclude=db2
112 db-exclude=db5
113 db-timeout=600
114 start-fast=y
115 tablespace-map=ts_01=/db/ts_01
116 tablespace-map=ts_02=/db/ts_02
117 """)
118 assert want in have, repr((want, have))
119
120 primary.log(primary.succeed("""
121 HOME="${nodes.primary.services.postgresql.dataDir}"
122 mkdir -m 700 -p ~/.ssh
123 cat ${snakeOilPrivateKey} > ~/.ssh/id_ecdsa
124 chmod 400 ~/.ssh/id_ecdsa
125 ssh-keyscan backup >> ~/.ssh/known_hosts
126 chown -R postgres:postgres ~/.ssh
127 """))
128
129 backup.log(backup.succeed("""
130 HOME="${backupPath}"
131 mkdir -m 700 -p ~/.ssh
132 cat ${snakeOilPrivateKey} > ~/.ssh/id_ecdsa
133 chmod 400 ~/.ssh/id_ecdsa
134 ssh-keyscan primary >> ~/.ssh/known_hosts
135 chown -R pgbackrest:pgbackrest ~
136 """))
137
138 with subtest("backup/restore works with remote instance/local repo (SSH)"):
139 backup.succeed("sudo -u pgbackrest pgbackrest --stanza=default stanza-create")
140 backup.succeed("sudo -u pgbackrest pgbackrest --stanza=default check")
141
142 backup.systemctl("start pgbackrest-default-future")
143
144 # corrupt cluster
145 primary.systemctl("stop postgresql")
146 primary.execute("rm ${nodes.primary.services.postgresql.dataDir}/global/pg_control")
147
148 primary.succeed("sudo -u postgres pgbackrest --stanza=default restore --delta")
149
150 primary.systemctl("start postgresql")
151 primary.wait_for_unit("postgresql.target")
152 assert "hello world" in primary.succeed("sudo -u postgres psql -c 'TABLE t;'")
153 '';
154}