]>
Commit | Line | Data |
---|---|---|
1 | { pkgs, config, lib, ... }: | |
2 | let | |
3 | cfg = config.myServices.databasesReplication.postgresql; | |
4 | in | |
5 | { | |
6 | options.myServices.databasesReplication.postgresql = { | |
7 | enable = lib.mkEnableOption "Enable postgresql replication"; | |
8 | base = lib.mkOption { | |
9 | type = lib.types.path; | |
10 | description = '' | |
11 | Base path to put the replications | |
12 | ''; | |
13 | }; | |
14 | mainPackage = lib.mkOption { | |
15 | type = lib.types.package; | |
16 | default = pkgs.postgresql; | |
17 | description = '' | |
18 | Postgresql package available in shell | |
19 | ''; | |
20 | }; | |
21 | hosts = lib.mkOption { | |
22 | default = {}; | |
23 | description = '' | |
24 | Hosts to backup | |
25 | ''; | |
26 | type = lib.types.attrsOf (lib.types.submodule { | |
27 | options = { | |
28 | package = lib.mkOption { | |
29 | type = lib.types.package; | |
30 | default = pkgs.postgresql; | |
31 | description = '' | |
32 | Postgresql package for this host | |
33 | ''; | |
34 | }; | |
35 | slot = lib.mkOption { | |
36 | type = lib.types.str; | |
37 | description = '' | |
38 | Slot to use for replication | |
39 | ''; | |
40 | }; | |
41 | connection = lib.mkOption { | |
42 | type = lib.types.str; | |
43 | description = '' | |
44 | Connection string to access the psql master | |
45 | ''; | |
46 | }; | |
47 | }; | |
48 | }); | |
49 | }; | |
50 | }; | |
51 | ||
52 | config = lib.mkIf cfg.enable { | |
53 | myServices.chatonsProperties.hostings.postgresql-replication = { | |
54 | file.datetime = "2022-08-27T15:00:00"; | |
55 | hosting = { | |
56 | name = "PostgreSQL replication"; | |
57 | description = "Replication of PostgreSQL database"; | |
58 | website = "db-1.immae.eu"; | |
59 | status.level = "OK"; | |
60 | status.description = "OK"; | |
61 | registration.load = "OPEN"; | |
62 | install.type = "PACKAGE"; | |
63 | }; | |
64 | software = { | |
65 | name = "PostgreSQL"; | |
66 | website = "https://www.postgresql.org/"; | |
67 | license.url = "https://www.postgresql.org/about/licence/"; | |
68 | license.name = "The PostgreSQL Licence"; | |
69 | version = pkgs.postgresql.version; | |
70 | source.url = "https://git.postgresql.org/gitweb/?p=postgresql.git;a=summary"; | |
71 | }; | |
72 | }; | |
73 | users.users.postgres = { | |
74 | name = "postgres"; | |
75 | uid = config.ids.uids.postgres; | |
76 | group = "postgres"; | |
77 | description = "PostgreSQL server user"; | |
78 | home = "/var/lib/postgresql"; | |
79 | useDefaultShell = true; | |
80 | extraGroups = [ "keys" ]; | |
81 | }; | |
82 | users.groups.postgres.gid = config.ids.gids.postgres; | |
83 | environment.systemPackages = [ cfg.mainPackage ]; | |
84 | ||
85 | secrets.keys = lib.listToAttrs (lib.flatten (lib.mapAttrsToList (name: hcfg: [ | |
86 | (lib.nameValuePair "postgresql_replication/${name}/recovery.conf" { | |
87 | user = "postgres"; | |
88 | group = "postgres"; | |
89 | permissions = "0400"; | |
90 | text = '' | |
91 | standby_mode = on | |
92 | primary_conninfo = '${hcfg.connection}?sslmode=require' | |
93 | primary_slot_name = '${hcfg.slot}' | |
94 | ''; | |
95 | }) | |
96 | (lib.nameValuePair "postgresql_replication/${name}/connection_string" { | |
97 | user = "postgres"; | |
98 | group = "postgres"; | |
99 | permissions = "0400"; | |
100 | text = hcfg.connection; | |
101 | }) | |
102 | (lib.nameValuePair "postgresql_replication/${name}/postgresql.conf" { | |
103 | user = "postgres"; | |
104 | group = "postgres"; | |
105 | permissions = "0400"; | |
106 | text = let | |
107 | dataDir = "${cfg.base}/${name}/postgresql"; | |
108 | in '' | |
109 | listen_addresses = ''' | |
110 | unix_socket_directories = '${dataDir}' | |
111 | data_directory = '${dataDir}' | |
112 | wal_level = logical | |
113 | max_connections = 300 | |
114 | ''; | |
115 | }) | |
116 | ]) cfg.hosts)); | |
117 | ||
118 | services.cron = { | |
119 | enable = true; | |
120 | systemCronJobs = lib.flatten (lib.mapAttrsToList (name: hcfg: | |
121 | let | |
122 | dataDir = "${cfg.base}/${name}/postgresql"; | |
123 | backupDir = "${cfg.base}/${name}/postgresql_backup"; | |
124 | backup_script = pkgs.writeScript "backup_psql_${name}" '' | |
125 | #!${pkgs.stdenv.shell} | |
126 | ||
127 | set -euo pipefail | |
128 | ||
129 | resume_replication() { | |
130 | ${hcfg.package}/bin/psql -h ${dataDir} -c "SELECT pg_wal_replay_resume();" >/dev/null || echo "impossible to resume replication" | |
131 | } | |
132 | ||
133 | trap resume_replication EXIT | |
134 | ||
135 | ${hcfg.package}/bin/psql -h ${dataDir} -c "SELECT pg_wal_replay_pause();" >/dev/null || (echo "impossible to pause replication" && false) | |
136 | ||
137 | ${hcfg.package}/bin/pg_dumpall -h ${dataDir} -f ${backupDir}/$(${pkgs.coreutils}/bin/date -Iminutes).sql | |
138 | ''; | |
139 | u = pkgs.callPackage ./utils.nix {}; | |
140 | cleanup_script = pkgs.writeScript "cleanup_postgresql_${name}" (u.keepLastNDumps "sql" backupDir 6); | |
141 | in [ | |
142 | "0 22,4,10,16 * * * postgres ${backup_script}" | |
143 | "0 3 * * * postgres ${cleanup_script}" | |
144 | ]) cfg.hosts); | |
145 | }; | |
146 | ||
147 | system.activationScripts = lib.attrsets.mapAttrs' (name: hcfg: | |
148 | lib.attrsets.nameValuePair "psql_replication_${name}" { | |
149 | deps = [ "users" ]; | |
150 | text = '' | |
151 | install -m 0700 -o postgres -g postgres -d ${cfg.base}/${name}/postgresql | |
152 | install -m 0700 -o postgres -g postgres -d ${cfg.base}/${name}/postgresql_backup | |
153 | ''; | |
154 | }) cfg.hosts; | |
155 | ||
156 | systemd.services = lib.attrsets.mapAttrs' (name: hcfg: | |
157 | let | |
158 | dataDir = "${cfg.base}/${name}/postgresql"; | |
159 | in | |
160 | lib.attrsets.nameValuePair "postgresql_backup_${name}" { | |
161 | description = "Postgresql replication for ${name}"; | |
162 | wantedBy = [ "multi-user.target" ]; | |
163 | after = [ "network.target" ]; | |
164 | ||
165 | environment.PGDATA = dataDir; | |
166 | path = [ hcfg.package ]; | |
167 | ||
168 | preStart = '' | |
169 | if ! test -e ${dataDir}/PG_VERSION; then | |
170 | mkdir -m 0700 -p ${dataDir} | |
171 | chown -R postgres:postgres ${dataDir} | |
172 | fi | |
173 | ''; | |
174 | script = let | |
175 | fp = n: config.secrets.fullPaths."postgresql_replication/${name}/${n}"; | |
176 | in '' | |
177 | if ! test -e ${dataDir}/PG_VERSION; then | |
178 | pg_basebackup -d $(cat ${fp "connection_string"}) -D ${dataDir} -S ${hcfg.slot} | |
179 | fi | |
180 | ln -sfn ${fp "recovery.conf"} ${dataDir}/recovery.conf | |
181 | ln -sfn ${fp "postgresql.conf"} ${dataDir}/postgresql.conf | |
182 | ||
183 | exec postgres | |
184 | ''; | |
185 | ||
186 | serviceConfig = { | |
187 | ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; | |
188 | User = "postgres"; | |
189 | Group = "postgres"; | |
190 | PermissionsStartOnly = true; | |
191 | RuntimeDirectory = "postgresql"; | |
192 | Type = "notify"; | |
193 | ||
194 | KillSignal = "SIGINT"; | |
195 | KillMode = "mixed"; | |
196 | # basebackup can take a long time | |
197 | TimeoutStartSec="infinity"; | |
198 | TimeoutStopSec = 120; | |
199 | }; | |
200 | unitConfig.RequiresMountsFor = dataDir; | |
201 | }) cfg.hosts; | |
202 | }; | |
203 | } |