active_postgres 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +58 -3
- data/lib/active_postgres/cli.rb +7 -0
- data/lib/active_postgres/components/core.rb +5 -5
- data/lib/active_postgres/components/monitoring.rb +165 -0
- data/lib/active_postgres/components/pgbackrest.rb +59 -9
- data/lib/active_postgres/components/pgbouncer.rb +17 -5
- data/lib/active_postgres/components/repmgr.rb +103 -7
- data/lib/active_postgres/configuration.rb +19 -2
- data/lib/active_postgres/generators/active_postgres/install_generator.rb +1 -0
- data/lib/active_postgres/generators/active_postgres/templates/postgres.yml.erb +10 -0
- data/lib/active_postgres/health_checker.rb +29 -7
- data/lib/active_postgres/installer.rb +9 -0
- data/lib/active_postgres/overview.rb +351 -0
- data/lib/active_postgres/secrets.rb +18 -0
- data/lib/active_postgres/ssh_executor.rb +9 -5
- data/lib/active_postgres/version.rb +1 -1
- data/lib/active_postgres.rb +1 -0
- data/lib/tasks/postgres.rake +67 -0
- data/templates/repmgr.conf.erb +10 -0
- data/templates/repmgr_dns_failover.sh.erb +25 -11
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e51d5f1214cedf497a9b060e5c667c43711f9ff5e36024bfd8dc926b50fe272f
|
|
4
|
+
data.tar.gz: 618fa70d39a43e1c74ba2ed449b75138670bca92cb6bf418606f772de34c6a7d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 254b6fb4342d4e510076d05ebfbeaa0e7990b463df13ad6ddb7d6eb5a70562c23cc4f502ce6c435c6372fc3dd5f695285ee3fdfed522cc188f1b1cc21c6cc90d
|
|
7
|
+
data.tar.gz: d9147a4679529344ccb0ffd3d5c2ffacc3072464b9cf4e1c6e11150a66c142ef0d0b45f9b9cd9d34eceba6f4d0e50baa7c2f227a5b2b1719502972e2b4b39ee4
|
data/README.md
CHANGED
|
@@ -69,6 +69,8 @@ postgres:
|
|
|
69
69
|
```bash
|
|
70
70
|
rake postgres:setup # Deploy cluster
|
|
71
71
|
rake postgres:status # Check health
|
|
72
|
+
rake postgres:overview # Control tower overview
|
|
73
|
+
rake postgres:help # Task summary
|
|
72
74
|
```
|
|
73
75
|
|
|
74
76
|
## Common Operations
|
|
@@ -83,8 +85,10 @@ rake postgres:promote[host] # Promote standby to primary
|
|
|
83
85
|
|
|
84
86
|
# Backups (requires pgBackRest)
|
|
85
87
|
rake postgres:backup:full
|
|
88
|
+
rake postgres:backup:incremental
|
|
86
89
|
rake postgres:backup:list
|
|
87
90
|
rake postgres:backup:restore[backup_id]
|
|
91
|
+
rake postgres:backup:restore_at["2026-01-29 01:15:00",promote]
|
|
88
92
|
|
|
89
93
|
# Credential rotation (zero downtime)
|
|
90
94
|
rake postgres:credentials:rotate_random
|
|
@@ -101,6 +105,7 @@ rake postgres:update:patch # Security patches
|
|
|
101
105
|
active_postgres setup --environment=production
|
|
102
106
|
active_postgres setup-standby HOST
|
|
103
107
|
active_postgres status
|
|
108
|
+
active_postgres overview
|
|
104
109
|
active_postgres promote HOST
|
|
105
110
|
active_postgres backup --type=full
|
|
106
111
|
active_postgres cache-secrets
|
|
@@ -115,21 +120,37 @@ active_postgres cache-secrets
|
|
|
115
120
|
| **repmgr** | HA & automatic failover | `repmgr: {enabled: true}` |
|
|
116
121
|
| **PgBouncer** | Connection pooling | `pgbouncer: {enabled: true}` |
|
|
117
122
|
| **pgBackRest** | Backup & restore | `pgbackrest: {enabled: true}` |
|
|
118
|
-
| **Monitoring** | postgres_exporter
|
|
123
|
+
| **Monitoring** | postgres_exporter + optional node_exporter/Grafana | `monitoring: {enabled: true}` |
|
|
119
124
|
| **SSL** | Encrypted connections | `ssl: {enabled: true}` |
|
|
120
125
|
| **Extensions** | pgvector, PostGIS, etc. | `extensions: {enabled: true, list: [pgvector]}` |
|
|
121
126
|
|
|
122
127
|
### Monitoring credentials
|
|
123
128
|
|
|
124
|
-
`postgres_exporter` uses a dedicated `pg_monitor` user. Provide a password in secrets and optionally set the username
|
|
129
|
+
`postgres_exporter` uses a dedicated `pg_monitor` user. Provide a password in secrets and optionally set the username.
|
|
130
|
+
Enable node_exporter for host-level metrics, and Grafana if you want a local UI:
|
|
125
131
|
|
|
126
132
|
```yaml
|
|
127
133
|
components:
|
|
128
|
-
monitoring:
|
|
134
|
+
monitoring:
|
|
135
|
+
enabled: true
|
|
136
|
+
user: postgres_exporter
|
|
137
|
+
node_exporter: true
|
|
138
|
+
node_exporter_port: 9100
|
|
139
|
+
# node_exporter_listen_address: 10.8.0.10
|
|
140
|
+
# grafana:
|
|
141
|
+
# enabled: true
|
|
142
|
+
# host: grafana.example.com
|
|
143
|
+
# listen_address: 10.8.0.10
|
|
144
|
+
# port: 3000
|
|
145
|
+
# prometheus_url: http://prometheus.example.com:9090
|
|
129
146
|
secrets:
|
|
130
147
|
monitoring_password: $POSTGRES_MONITORING_PASSWORD
|
|
148
|
+
# grafana_admin_password: $GRAFANA_ADMIN_PASSWORD
|
|
131
149
|
```
|
|
132
150
|
|
|
151
|
+
When Grafana is enabled, `grafana_admin_password` is required and Grafana will be installed
|
|
152
|
+
on the configured host. If `prometheus_url` is provided, a Prometheus datasource is provisioned.
|
|
153
|
+
|
|
133
154
|
### pgBackRest S3-compatible endpoints
|
|
134
155
|
|
|
135
156
|
For Tigris/MinIO/Wasabi/DO Spaces, set a custom endpoint and use path-style URLs:
|
|
@@ -145,6 +166,35 @@ components:
|
|
|
145
166
|
s3_uri_style: path
|
|
146
167
|
```
|
|
147
168
|
|
|
169
|
+
### Point-in-time recovery (PITR)
|
|
170
|
+
|
|
171
|
+
Ensure WAL retention is long enough for your recovery window:
|
|
172
|
+
|
|
173
|
+
```yaml
|
|
174
|
+
components:
|
|
175
|
+
pgbackrest:
|
|
176
|
+
retention_full: 7
|
|
177
|
+
retention_archive: 14
|
|
178
|
+
# You can schedule full + incremental separately:
|
|
179
|
+
# schedule_full: "0 2 * * *"
|
|
180
|
+
# schedule_incremental: "0 * * * *"
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Run a PITR restore:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
rake postgres:backup:restore_at["2026-01-29 01:15:00",promote]
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
If you want the old primary to rejoin without a full re-clone after failover,
|
|
190
|
+
enable pg_rewind support in repmgr:
|
|
191
|
+
|
|
192
|
+
```yaml
|
|
193
|
+
components:
|
|
194
|
+
repmgr:
|
|
195
|
+
use_rewind: true
|
|
196
|
+
```
|
|
197
|
+
|
|
148
198
|
### Stable app endpoint with PgBouncer
|
|
149
199
|
|
|
150
200
|
If you run PgBouncer on each PostgreSQL node and want a fixed app URL, enable `follow_primary`.
|
|
@@ -175,6 +225,8 @@ components:
|
|
|
175
225
|
enabled: true
|
|
176
226
|
provider: dnsmasq
|
|
177
227
|
domain: mesh.internal
|
|
228
|
+
# Or use multiple domains during cutover:
|
|
229
|
+
# domains: [mesh.internal, mesh.v2.internal]
|
|
178
230
|
dns_servers:
|
|
179
231
|
- host: 18.170.173.14
|
|
180
232
|
private_ip: 10.8.0.10
|
|
@@ -182,6 +234,9 @@ components:
|
|
|
182
234
|
private_ip: 10.8.0.110
|
|
183
235
|
primary_record: db-primary.mesh.internal
|
|
184
236
|
replica_record: db-replica.mesh.internal
|
|
237
|
+
# Or multiple records explicitly:
|
|
238
|
+
# primary_records: [db-primary.mesh.internal, db-primary.mesh.v2.internal]
|
|
239
|
+
# replica_records: [db-replica.mesh.internal, db-replica.mesh.v2.internal]
|
|
185
240
|
```
|
|
186
241
|
|
|
187
242
|
This installs an event hook so repmgr updates `/etc/dnsmasq.d/active_postgres.conf`
|
data/lib/active_postgres/cli.rb
CHANGED
|
@@ -49,6 +49,13 @@ module ActivePostgres
|
|
|
49
49
|
health_checker.show_status
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
desc 'overview', 'Show control tower overview'
|
|
53
|
+
def overview
|
|
54
|
+
config = load_config
|
|
55
|
+
overview = Overview.new(config)
|
|
56
|
+
overview.show
|
|
57
|
+
end
|
|
58
|
+
|
|
52
59
|
desc 'health', 'Run health checks'
|
|
53
60
|
def health
|
|
54
61
|
config = load_config
|
|
@@ -49,6 +49,11 @@ module ActivePostgres
|
|
|
49
49
|
create_app_user_and_database(config.primary_host)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
def install_packages_only(host)
|
|
53
|
+
puts " Installing packages on #{host} (cluster will be created by repmgr)..."
|
|
54
|
+
ssh_executor.install_postgres(host, config.version)
|
|
55
|
+
end
|
|
56
|
+
|
|
52
57
|
private
|
|
53
58
|
|
|
54
59
|
def install_on_host(host, is_primary:)
|
|
@@ -96,11 +101,6 @@ module ActivePostgres
|
|
|
96
101
|
optimal_settings.merge(user_postgresql)
|
|
97
102
|
end
|
|
98
103
|
|
|
99
|
-
def install_packages_only(host)
|
|
100
|
-
puts " Installing packages on #{host} (cluster will be created by repmgr)..."
|
|
101
|
-
ssh_executor.install_postgres(host, config.version)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
104
|
def cluster_exists?(host)
|
|
105
105
|
exists = false
|
|
106
106
|
version = config.version
|
|
@@ -11,6 +11,8 @@ module ActivePostgres
|
|
|
11
11
|
config.all_hosts.each do |host|
|
|
12
12
|
install_on_host(host)
|
|
13
13
|
end
|
|
14
|
+
|
|
15
|
+
install_grafana_if_enabled
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
def uninstall
|
|
@@ -21,8 +23,16 @@ module ActivePostgres
|
|
|
21
23
|
execute :sudo, 'systemctl', 'stop', 'prometheus-postgres-exporter'
|
|
22
24
|
execute :sudo, 'systemctl', 'disable', 'prometheus-postgres-exporter'
|
|
23
25
|
execute :sudo, 'apt-get', 'remove', '-y', 'prometheus-postgres-exporter'
|
|
26
|
+
|
|
27
|
+
if node_exporter_enabled?
|
|
28
|
+
execute :sudo, 'systemctl', 'stop', 'prometheus-node-exporter'
|
|
29
|
+
execute :sudo, 'systemctl', 'disable', 'prometheus-node-exporter'
|
|
30
|
+
execute :sudo, 'apt-get', 'remove', '-y', 'prometheus-node-exporter'
|
|
31
|
+
end
|
|
24
32
|
end
|
|
25
33
|
end
|
|
34
|
+
|
|
35
|
+
uninstall_grafana_if_enabled
|
|
26
36
|
end
|
|
27
37
|
|
|
28
38
|
def restart
|
|
@@ -31,6 +41,7 @@ module ActivePostgres
|
|
|
31
41
|
config.all_hosts.each do |host|
|
|
32
42
|
ssh_executor.execute_on_host(host) do
|
|
33
43
|
execute :sudo, 'systemctl', 'restart', 'prometheus-postgres-exporter'
|
|
44
|
+
execute :sudo, 'systemctl', 'restart', 'prometheus-node-exporter' if node_exporter_enabled?
|
|
34
45
|
end
|
|
35
46
|
end
|
|
36
47
|
end
|
|
@@ -63,6 +74,8 @@ module ActivePostgres
|
|
|
63
74
|
end
|
|
64
75
|
|
|
65
76
|
puts " Metrics available at: http://#{host}:#{exporter_port}/metrics"
|
|
77
|
+
|
|
78
|
+
install_node_exporter(host, monitoring_config) if node_exporter_enabled?
|
|
66
79
|
end
|
|
67
80
|
|
|
68
81
|
def ensure_monitoring_user
|
|
@@ -134,6 +147,158 @@ module ActivePostgres
|
|
|
134
147
|
password
|
|
135
148
|
end
|
|
136
149
|
|
|
150
|
+
def node_exporter_enabled?
|
|
151
|
+
monitoring_config = config.component_config(:monitoring)
|
|
152
|
+
monitoring_config.fetch(:node_exporter, false) == true
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def install_node_exporter(host, monitoring_config)
|
|
156
|
+
port = monitoring_config[:node_exporter_port] || 9100
|
|
157
|
+
listen_address = monitoring_config[:node_exporter_listen_address].to_s.strip
|
|
158
|
+
puts " Installing node_exporter on #{host}..."
|
|
159
|
+
|
|
160
|
+
ssh_executor.execute_on_host(host) do
|
|
161
|
+
execute :sudo, 'apt-get', 'install', '-y', '-qq', 'prometheus-node-exporter'
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
configure_node_exporter_service(host, port, listen_address)
|
|
165
|
+
|
|
166
|
+
ssh_executor.execute_on_host(host) do
|
|
167
|
+
execute :sudo, 'systemctl', 'enable', 'prometheus-node-exporter'
|
|
168
|
+
execute :sudo, 'systemctl', 'restart', 'prometheus-node-exporter'
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
puts " Node metrics available at: http://#{host}:#{port}/metrics"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def configure_node_exporter_service(host, port, listen_address)
|
|
175
|
+
listen = if listen_address.empty?
|
|
176
|
+
":#{port}"
|
|
177
|
+
else
|
|
178
|
+
"#{listen_address}:#{port}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
return if listen == ':9100'
|
|
182
|
+
|
|
183
|
+
override = <<~CONF
|
|
184
|
+
[Service]
|
|
185
|
+
ExecStart=
|
|
186
|
+
ExecStart=/usr/bin/prometheus-node-exporter --web.listen-address=#{listen}
|
|
187
|
+
CONF
|
|
188
|
+
|
|
189
|
+
ssh_executor.execute_on_host(host) do
|
|
190
|
+
execute :sudo, 'mkdir', '-p', '/etc/systemd/system/prometheus-node-exporter.service.d'
|
|
191
|
+
upload! StringIO.new(override), '/tmp/prometheus-node-exporter.override'
|
|
192
|
+
execute :sudo, 'mv', '/tmp/prometheus-node-exporter.override',
|
|
193
|
+
'/etc/systemd/system/prometheus-node-exporter.service.d/override.conf'
|
|
194
|
+
execute :sudo, 'chown', 'root:root', '/etc/systemd/system/prometheus-node-exporter.service.d/override.conf'
|
|
195
|
+
execute :sudo, 'chmod', '644', '/etc/systemd/system/prometheus-node-exporter.service.d/override.conf'
|
|
196
|
+
execute :sudo, 'systemctl', 'daemon-reload'
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def install_grafana_if_enabled
|
|
201
|
+
monitoring_config = config.component_config(:monitoring)
|
|
202
|
+
grafana_config = monitoring_config[:grafana] || {}
|
|
203
|
+
return unless grafana_config[:enabled]
|
|
204
|
+
|
|
205
|
+
host = grafana_config[:host].to_s.strip
|
|
206
|
+
raise Error, 'monitoring.grafana.host is required when grafana is enabled' if host.empty?
|
|
207
|
+
|
|
208
|
+
admin_password = normalize_grafana_password(secrets.resolve('grafana_admin_password'))
|
|
209
|
+
prometheus_url = grafana_config[:prometheus_url]
|
|
210
|
+
listen_address = grafana_config[:listen_address].to_s.strip
|
|
211
|
+
port = (grafana_config[:port] || 3000).to_i
|
|
212
|
+
|
|
213
|
+
puts "Installing Grafana on #{host}..."
|
|
214
|
+
|
|
215
|
+
ssh_executor.execute_on_host(host) do
|
|
216
|
+
execute :sudo, 'apt-get', 'install', '-y', '-qq', 'apt-transport-https', 'software-properties-common', 'wget'
|
|
217
|
+
execute :sudo, 'wget', '-q', '-O', '/usr/share/keyrings/grafana.gpg', 'https://apt.grafana.com/gpg.key'
|
|
218
|
+
execute :sudo, 'sh', '-c',
|
|
219
|
+
'echo "deb [signed-by=/usr/share/keyrings/grafana.gpg] https://apt.grafana.com stable main" > /etc/apt/sources.list.d/grafana.list'
|
|
220
|
+
execute :sudo, 'apt-get', 'update', '-qq'
|
|
221
|
+
execute :sudo, 'apt-get', 'install', '-y', '-qq', 'grafana'
|
|
222
|
+
execute :sudo, 'systemctl', 'enable', '--now', 'grafana-server'
|
|
223
|
+
execute :sudo, 'grafana-cli', 'admin', 'reset-admin-password', admin_password
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
configure_grafana_service(host, listen_address, port)
|
|
227
|
+
configure_grafana_datasource(host, prometheus_url) if prometheus_url
|
|
228
|
+
|
|
229
|
+
puts " Grafana available at: http://#{host}:#{port}"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def configure_grafana_service(host, listen_address, port)
|
|
233
|
+
env_lines = []
|
|
234
|
+
env_lines << "Environment=\"GF_SERVER_HTTP_ADDR=#{listen_address}\"" unless listen_address.empty?
|
|
235
|
+
env_lines << "Environment=\"GF_SERVER_HTTP_PORT=#{port}\"" if port && port != 3000
|
|
236
|
+
return if env_lines.empty?
|
|
237
|
+
|
|
238
|
+
override = <<~CONF
|
|
239
|
+
[Service]
|
|
240
|
+
#{env_lines.join("\n ")}
|
|
241
|
+
CONF
|
|
242
|
+
|
|
243
|
+
ssh_executor.execute_on_host(host) do
|
|
244
|
+
execute :sudo, 'mkdir', '-p', '/etc/systemd/system/grafana-server.service.d'
|
|
245
|
+
upload! StringIO.new(override), '/tmp/active_postgres_grafana.override'
|
|
246
|
+
execute :sudo, 'mv', '/tmp/active_postgres_grafana.override',
|
|
247
|
+
'/etc/systemd/system/grafana-server.service.d/override.conf'
|
|
248
|
+
execute :sudo, 'chown', 'root:root', '/etc/systemd/system/grafana-server.service.d/override.conf'
|
|
249
|
+
execute :sudo, 'chmod', '644', '/etc/systemd/system/grafana-server.service.d/override.conf'
|
|
250
|
+
execute :sudo, 'systemctl', 'daemon-reload'
|
|
251
|
+
execute :sudo, 'systemctl', 'restart', 'grafana-server'
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def uninstall_grafana_if_enabled
|
|
256
|
+
monitoring_config = config.component_config(:monitoring)
|
|
257
|
+
grafana_config = monitoring_config[:grafana] || {}
|
|
258
|
+
return unless grafana_config[:enabled]
|
|
259
|
+
|
|
260
|
+
host = grafana_config[:host].to_s.strip
|
|
261
|
+
return if host.empty?
|
|
262
|
+
|
|
263
|
+
ssh_executor.execute_on_host(host) do
|
|
264
|
+
execute :sudo, 'systemctl', 'stop', 'grafana-server'
|
|
265
|
+
execute :sudo, 'systemctl', 'disable', 'grafana-server'
|
|
266
|
+
execute :sudo, 'apt-get', 'remove', '-y', 'grafana'
|
|
267
|
+
execute :sudo, 'rm', '-f', '/etc/apt/sources.list.d/grafana.list'
|
|
268
|
+
execute :sudo, 'rm', '-f', '/usr/share/keyrings/grafana.gpg'
|
|
269
|
+
execute :sudo, 'rm', '-rf', '/etc/grafana/provisioning/datasources/active_postgres.yml'
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def configure_grafana_datasource(host, prometheus_url)
|
|
274
|
+
datasource = <<~YAML
|
|
275
|
+
apiVersion: 1
|
|
276
|
+
datasources:
|
|
277
|
+
- name: Prometheus
|
|
278
|
+
type: prometheus
|
|
279
|
+
access: proxy
|
|
280
|
+
url: #{prometheus_url}
|
|
281
|
+
isDefault: true
|
|
282
|
+
YAML
|
|
283
|
+
|
|
284
|
+
ssh_executor.execute_on_host(host) do
|
|
285
|
+
execute :sudo, 'mkdir', '-p', '/etc/grafana/provisioning/datasources'
|
|
286
|
+
upload! StringIO.new(datasource), '/tmp/active_postgres_grafana_ds.yml'
|
|
287
|
+
execute :sudo, 'mv', '/tmp/active_postgres_grafana_ds.yml', '/etc/grafana/provisioning/datasources/active_postgres.yml'
|
|
288
|
+
execute :sudo, 'chown', 'root:root', '/etc/grafana/provisioning/datasources/active_postgres.yml'
|
|
289
|
+
execute :sudo, 'chmod', '644', '/etc/grafana/provisioning/datasources/active_postgres.yml'
|
|
290
|
+
execute :sudo, 'systemctl', 'restart', 'grafana-server'
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def normalize_grafana_password(raw_password)
|
|
295
|
+
password = raw_password.to_s.rstrip
|
|
296
|
+
|
|
297
|
+
raise Error, 'grafana_admin_password secret is missing (required for grafana)' if password.empty?
|
|
298
|
+
|
|
299
|
+
password
|
|
300
|
+
end
|
|
301
|
+
|
|
137
302
|
def escape_systemd_env(value)
|
|
138
303
|
value.to_s.gsub('\\', '\\\\').gsub('"', '\\"')
|
|
139
304
|
end
|
|
@@ -20,6 +20,7 @@ module ActivePostgres
|
|
|
20
20
|
ssh_executor.execute_on_host(host) do
|
|
21
21
|
execute :sudo, 'apt-get', 'remove', '-y', 'pgbackrest'
|
|
22
22
|
execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup'
|
|
23
|
+
execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-incremental'
|
|
23
24
|
end
|
|
24
25
|
end
|
|
25
26
|
end
|
|
@@ -37,9 +38,12 @@ module ActivePostgres
|
|
|
37
38
|
puts "Running #{type} backup..."
|
|
38
39
|
postgres_user = config.postgres_user
|
|
39
40
|
|
|
41
|
+
output = nil
|
|
40
42
|
ssh_executor.execute_on_host(config.primary_host) do
|
|
41
|
-
|
|
43
|
+
output = capture(:sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', "--type=#{type}", 'backup')
|
|
42
44
|
end
|
|
45
|
+
|
|
46
|
+
puts output if output
|
|
43
47
|
end
|
|
44
48
|
|
|
45
49
|
def run_restore(backup_id)
|
|
@@ -51,20 +55,43 @@ module ActivePostgres
|
|
|
51
55
|
execute :sudo, 'systemctl', 'stop', 'postgresql'
|
|
52
56
|
|
|
53
57
|
# Restore
|
|
54
|
-
|
|
58
|
+
output = capture(:sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', "--set=#{backup_id}", 'restore')
|
|
59
|
+
puts output if output
|
|
55
60
|
|
|
56
61
|
# Start PostgreSQL
|
|
57
62
|
execute :sudo, 'systemctl', 'start', 'postgresql'
|
|
58
63
|
end
|
|
59
64
|
end
|
|
60
65
|
|
|
66
|
+
def run_restore_at(target_time, target_action: 'promote')
|
|
67
|
+
puts "Restoring to #{target_time} (PITR)..."
|
|
68
|
+
postgres_user = config.postgres_user
|
|
69
|
+
|
|
70
|
+
ssh_executor.execute_on_host(config.primary_host) do
|
|
71
|
+
execute :sudo, 'systemctl', 'stop', 'postgresql'
|
|
72
|
+
|
|
73
|
+
output = capture(:sudo, '-u', postgres_user, 'pgbackrest',
|
|
74
|
+
'--stanza=main',
|
|
75
|
+
'--type=time',
|
|
76
|
+
"--target=#{target_time}",
|
|
77
|
+
"--target-action=#{target_action}",
|
|
78
|
+
'restore')
|
|
79
|
+
puts output if output
|
|
80
|
+
|
|
81
|
+
execute :sudo, 'systemctl', 'start', 'postgresql'
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
61
85
|
def list_backups
|
|
62
86
|
puts 'Available backups:'
|
|
63
87
|
postgres_user = config.postgres_user
|
|
64
88
|
|
|
89
|
+
output = nil
|
|
65
90
|
ssh_executor.execute_on_host(config.primary_host) do
|
|
66
|
-
|
|
91
|
+
output = capture(:sudo, '-u', postgres_user, 'pgbackrest', 'info')
|
|
67
92
|
end
|
|
93
|
+
|
|
94
|
+
puts output if output
|
|
68
95
|
end
|
|
69
96
|
|
|
70
97
|
private
|
|
@@ -108,13 +135,35 @@ module ActivePostgres
|
|
|
108
135
|
end
|
|
109
136
|
|
|
110
137
|
# Set up scheduled backups on primary only
|
|
111
|
-
if create_stanza
|
|
112
|
-
|
|
138
|
+
if create_stanza
|
|
139
|
+
setup_backup_schedules(host, pgbackrest_config)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def setup_backup_schedules(host, pgbackrest_config)
|
|
144
|
+
schedules = backup_schedules(pgbackrest_config)
|
|
145
|
+
schedules.each do |entry|
|
|
146
|
+
setup_backup_schedule(host, entry[:schedule], entry[:type], entry[:file])
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def backup_schedules(pgbackrest_config)
|
|
151
|
+
schedule_full = pgbackrest_config[:schedule_full] || pgbackrest_config[:schedule]
|
|
152
|
+
schedule_incremental = pgbackrest_config[:schedule_incremental]
|
|
153
|
+
|
|
154
|
+
schedules = []
|
|
155
|
+
if schedule_full
|
|
156
|
+
schedules << { type: 'full', schedule: schedule_full, file: '/etc/cron.d/pgbackrest-backup' }
|
|
113
157
|
end
|
|
158
|
+
if schedule_incremental
|
|
159
|
+
schedules << { type: 'incremental', schedule: schedule_incremental, file: '/etc/cron.d/pgbackrest-backup-incremental' }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
schedules
|
|
114
163
|
end
|
|
115
164
|
|
|
116
|
-
def setup_backup_schedule(host, schedule)
|
|
117
|
-
puts " Setting up backup schedule: #{schedule}"
|
|
165
|
+
def setup_backup_schedule(host, schedule, backup_type = 'full', cron_file = '/etc/cron.d/pgbackrest-backup')
|
|
166
|
+
puts " Setting up #{backup_type} backup schedule: #{schedule}"
|
|
118
167
|
postgres_user = config.postgres_user
|
|
119
168
|
|
|
120
169
|
# Create cron job for scheduled backups
|
|
@@ -123,17 +172,18 @@ module ActivePostgres
|
|
|
123
172
|
# pgBackRest scheduled backups (managed by active_postgres)
|
|
124
173
|
SHELL=/bin/bash
|
|
125
174
|
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
|
|
126
|
-
#{schedule} #{postgres_user} pgbackrest --stanza=main --type
|
|
175
|
+
#{schedule} #{postgres_user} pgbackrest --stanza=main --type=#{backup_type} backup
|
|
127
176
|
CRON
|
|
128
177
|
|
|
129
178
|
# Install cron job in /etc/cron.d (system cron directory)
|
|
130
179
|
# Use a temp file + sudo mv to avoid shell redirection permission issues.
|
|
131
|
-
ssh_executor.upload_file(host, cron_content,
|
|
180
|
+
ssh_executor.upload_file(host, cron_content, cron_file, mode: '644', owner: 'root:root')
|
|
132
181
|
end
|
|
133
182
|
|
|
134
183
|
def remove_backup_schedule(host)
|
|
135
184
|
ssh_executor.execute_on_host(host) do
|
|
136
185
|
execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup'
|
|
186
|
+
execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-incremental'
|
|
137
187
|
end
|
|
138
188
|
end
|
|
139
189
|
end
|
|
@@ -5,7 +5,8 @@ module ActivePostgres
|
|
|
5
5
|
puts 'Installing PgBouncer for connection pooling...'
|
|
6
6
|
|
|
7
7
|
config.all_hosts.each do |host|
|
|
8
|
-
|
|
8
|
+
is_standby = config.standby_hosts.include?(host)
|
|
9
|
+
install_on_host(host, is_standby: is_standby)
|
|
9
10
|
end
|
|
10
11
|
end
|
|
11
12
|
|
|
@@ -49,16 +50,26 @@ module ActivePostgres
|
|
|
49
50
|
|
|
50
51
|
def install_on_standby(standby_host)
|
|
51
52
|
puts "Installing PgBouncer on standby #{standby_host}..."
|
|
52
|
-
install_on_host(standby_host)
|
|
53
|
+
install_on_host(standby_host, is_standby: true)
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
private
|
|
56
57
|
|
|
57
|
-
def install_on_host(host)
|
|
58
|
+
def install_on_host(host, is_standby: false)
|
|
58
59
|
puts " Installing PgBouncer on #{host}..."
|
|
59
60
|
|
|
60
61
|
user_config = config.component_config(:pgbouncer)
|
|
61
|
-
|
|
62
|
+
|
|
63
|
+
# Determine follow_primary behavior:
|
|
64
|
+
# - Primary: use global follow_primary setting
|
|
65
|
+
# - Standby: check per-standby pgbouncer_follow_primary, default false (use localhost for read replicas)
|
|
66
|
+
if is_standby
|
|
67
|
+
standby_config = config.standby_config_for(host) || {}
|
|
68
|
+
follow_primary = standby_config['pgbouncer_follow_primary'] == true
|
|
69
|
+
else
|
|
70
|
+
follow_primary = user_config[:follow_primary] == true
|
|
71
|
+
end
|
|
72
|
+
|
|
62
73
|
if follow_primary && !config.component_enabled?(:repmgr)
|
|
63
74
|
raise Error, 'PgBouncer follow_primary requires repmgr to be enabled'
|
|
64
75
|
end
|
|
@@ -67,7 +78,8 @@ module ActivePostgres
|
|
|
67
78
|
optimal_pool = ConnectionPooler.calculate_optimal_pool_sizes(max_connections)
|
|
68
79
|
|
|
69
80
|
pgbouncer_config = optimal_pool.merge(user_config)
|
|
70
|
-
|
|
81
|
+
# For standbys not following primary, use localhost; otherwise use primary host
|
|
82
|
+
pgbouncer_config[:database_host] = follow_primary ? config.primary_replication_host : '127.0.0.1'
|
|
71
83
|
ssl_enabled = config.component_enabled?(:ssl)
|
|
72
84
|
has_ca_cert = ssl_enabled && secrets.resolve('ssl_chain')
|
|
73
85
|
secrets_obj = secrets
|