active_postgres 0.9.6 → 0.9.8
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/CHANGELOG.md +24 -0
- data/README.md +33 -2
- data/lib/active_postgres/components/pgbackrest.rb +4 -0
- data/lib/active_postgres/components/repmgr.rb +100 -74
- data/lib/active_postgres/configuration/node_identity.rb +22 -0
- data/lib/active_postgres/configuration/pgbackrest_policy.rb +19 -0
- data/lib/active_postgres/configuration/standby_policy.rb +24 -0
- data/lib/active_postgres/configuration.rb +47 -16
- data/lib/active_postgres/deployment_flow.rb +5 -1
- data/lib/active_postgres/generators/active_postgres/templates/postgres.yml.erb +7 -0
- data/lib/active_postgres/installer.rb +2 -2
- data/lib/active_postgres/ssh_executor.rb +61 -4
- data/lib/active_postgres/standby_deployment_flow.rb +46 -8
- data/lib/active_postgres/version.rb +1 -1
- data/templates/pgbackrest.conf.erb +9 -1
- data/templates/repmgr.conf.erb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c3d6f986ee2ac96f5cdd8b69541a5acae87733f506b10efb94c1046082b990a9
|
|
4
|
+
data.tar.gz: f64bb049c6601cf853de16c6cfeff8293ebb06d622db9633330298f02ef82135
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b07c93095ab642d39d6d658e93ef82c7ca993a30cf06b3f320d02e20dc5f45a546ffcdb2196f5bae0d1ab2051fb3a453a0a445e3e3f49b89ec1e3be9ee19f042
|
|
7
|
+
data.tar.gz: aeae944d32c7ee5f661470eaee9f66c01ef47c1bb4f578bbae0fd8d3e50f81044d24ddf49f85304a5aa6437de207aa22600cd3cc6cf7167713c5f87204e3cee9
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ and this project follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.9.8 - 2026-10-02
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Support explicit node IDs so repmgr identities survive primary changes and standby reordering.
|
|
15
|
+
- Reject duplicate or invalid node IDs before deployment.
|
|
16
|
+
|
|
17
|
+
## 0.9.7 - 2026-09-01
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Support strict operator SSH through declared jump hosts.
|
|
22
|
+
- Allow a standby to override repmgr failover and priority policy.
|
|
23
|
+
- Allow a standby to be seeded from the latest valid pgBackRest backup.
|
|
24
|
+
- Make deployment-flow `--dry-run` stop before confirmation or mutation.
|
|
25
|
+
- Fail standby setup before mutation when required credentials do not resolve.
|
|
26
|
+
- Scope standby rollback to the requested host and keep existing cluster nodes intact.
|
|
27
|
+
- Preserve pgBackRest recovery options as one shell-safe argument during remote restore.
|
|
28
|
+
- Preserve the complete pgBackRest command as the single `bash -lc` command string.
|
|
29
|
+
- Run component rollback in local Ruby context instead of an SSHKit backend scope.
|
|
30
|
+
- Explicitly stop SysV-backed repmgrd before disabling it on manual-failover standbys.
|
|
31
|
+
- Support opt-in bundled block backups for large, frequently changed PostgreSQL relations.
|
|
32
|
+
- Support independent full, differential, and incremental pgBackRest schedules.
|
|
33
|
+
|
|
10
34
|
## 0.9.6 - 2026-08-20
|
|
11
35
|
|
|
12
36
|
### Changed
|
data/README.md
CHANGED
|
@@ -35,6 +35,11 @@ production:
|
|
|
35
35
|
version: 18
|
|
36
36
|
user: ubuntu
|
|
37
37
|
ssh_key: ~/.ssh/id_rsa
|
|
38
|
+
ssh_known_hosts_file: config/ssh_known_hosts
|
|
39
|
+
|
|
40
|
+
jump_hosts:
|
|
41
|
+
app-oci:
|
|
42
|
+
host: 129.213.194.40
|
|
38
43
|
|
|
39
44
|
primary:
|
|
40
45
|
host: 34.12.234.81 # Public IP for SSH
|
|
@@ -43,6 +48,13 @@ production:
|
|
|
43
48
|
standby:
|
|
44
49
|
- host: 52.23.45.67
|
|
45
50
|
private_ip: 10.8.0.11
|
|
51
|
+
- host: 100.63.4.227
|
|
52
|
+
private_ip: 10.8.0.12
|
|
53
|
+
jump_host: app-oci
|
|
54
|
+
seed_method: pgbackrest
|
|
55
|
+
repmgr:
|
|
56
|
+
auto_failover: false
|
|
57
|
+
priority: 0
|
|
46
58
|
|
|
47
59
|
components:
|
|
48
60
|
repmgr: {enabled: true}
|
|
@@ -56,6 +68,20 @@ production:
|
|
|
56
68
|
app_password: $POSTGRES_APP_PASSWORD
|
|
57
69
|
```
|
|
58
70
|
|
|
71
|
+
`jump_host` keeps the operator SSH key on the workstation and proxies the
|
|
72
|
+
connection through a declared host. A standby-level `repmgr` block overrides
|
|
73
|
+
the cluster defaults, so a cross-provider disaster-recovery replica can remain
|
|
74
|
+
manual-promotion while the ordinary standby retains automatic failover.
|
|
75
|
+
`seed_method: pgbackrest` restores the latest valid backup with standby recovery
|
|
76
|
+
settings before repmgr registration, avoiding a full base-backup stream from
|
|
77
|
+
the primary. The `pgbackrest` component must be enabled.
|
|
78
|
+
|
|
79
|
+
Set a unique positive integer `node_id` on every primary and standby entry
|
|
80
|
+
before permanently changing their roles or reordering standbys. Match the
|
|
81
|
+
existing IDs in `repmgr cluster show`, and keep each ID with its host after a
|
|
82
|
+
switchover. Without explicit IDs, the primary defaults to 1 and standbys use
|
|
83
|
+
their list position starting at 2. Duplicate or invalid IDs are rejected.
|
|
84
|
+
|
|
59
85
|
Add credentials (`rails credentials:edit`):
|
|
60
86
|
|
|
61
87
|
```yaml
|
|
@@ -179,8 +205,13 @@ components:
|
|
|
179
205
|
pgbackrest:
|
|
180
206
|
retention_full: 7
|
|
181
207
|
retention_archive: 14
|
|
182
|
-
#
|
|
183
|
-
#
|
|
208
|
+
# Reduce incremental backup churn for large, frequently changed relations.
|
|
209
|
+
# Block backups require repository bundling.
|
|
210
|
+
repo_bundle: true
|
|
211
|
+
repo_block: true
|
|
212
|
+
# You can schedule full, differential, and incremental backups separately:
|
|
213
|
+
# schedule_full: "0 2 * * 0"
|
|
214
|
+
# schedule_differential: "35 2 * * 1-6"
|
|
184
215
|
# schedule_incremental: "0 * * * *"
|
|
185
216
|
```
|
|
186
217
|
|
|
@@ -26,6 +26,7 @@ module ActivePostgres
|
|
|
26
26
|
ssh_executor.execute_on_host(host) do
|
|
27
27
|
execute :sudo, 'apt-get', 'remove', '-y', 'pgbackrest'
|
|
28
28
|
execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup'
|
|
29
|
+
execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-differential'
|
|
29
30
|
execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-incremental'
|
|
30
31
|
execute :sudo, 'rm', '-f', LOG_ARCHIVE_CRON_FILE
|
|
31
32
|
execute :sudo, 'rm', '-f', LOG_ARCHIVE_ENV_FILE
|
|
@@ -162,10 +163,12 @@ module ActivePostgres
|
|
|
162
163
|
|
|
163
164
|
def backup_schedules(pgbackrest_config)
|
|
164
165
|
schedule_full = pgbackrest_config[:schedule_full] || pgbackrest_config[:schedule]
|
|
166
|
+
schedule_differential = pgbackrest_config[:schedule_differential]
|
|
165
167
|
schedule_incremental = pgbackrest_config[:schedule_incremental]
|
|
166
168
|
|
|
167
169
|
schedules = []
|
|
168
170
|
schedules << { type: 'full', schedule: schedule_full, file: '/etc/cron.d/pgbackrest-backup' } if schedule_full
|
|
171
|
+
schedules << { type: 'diff', schedule: schedule_differential, file: '/etc/cron.d/pgbackrest-backup-differential' } if schedule_differential
|
|
169
172
|
schedules << { type: 'incr', schedule: schedule_incremental, file: '/etc/cron.d/pgbackrest-backup-incremental' } if schedule_incremental
|
|
170
173
|
|
|
171
174
|
schedules
|
|
@@ -192,6 +195,7 @@ module ActivePostgres
|
|
|
192
195
|
def remove_backup_schedule(host)
|
|
193
196
|
ssh_executor.execute_on_host(host) do
|
|
194
197
|
execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup'
|
|
198
|
+
execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-differential'
|
|
195
199
|
execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-incremental'
|
|
196
200
|
end
|
|
197
201
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'shellwords'
|
|
2
|
+
|
|
1
3
|
module ActivePostgres
|
|
2
4
|
module Components
|
|
3
5
|
class Repmgr < Base
|
|
@@ -22,31 +24,24 @@ module ActivePostgres
|
|
|
22
24
|
def uninstall
|
|
23
25
|
puts 'Uninstalling repmgr...'
|
|
24
26
|
|
|
25
|
-
config.all_hosts.each
|
|
26
|
-
|
|
27
|
-
node_id = if is_primary
|
|
28
|
-
1
|
|
29
|
-
else
|
|
30
|
-
begin
|
|
31
|
-
config.standby_hosts.index(host) + 2
|
|
32
|
-
rescue StandardError
|
|
33
|
-
999
|
|
34
|
-
end
|
|
35
|
-
end
|
|
27
|
+
config.all_hosts.each { |host| uninstall_from(host) }
|
|
28
|
+
end
|
|
36
29
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
capture(:sudo, '-u', 'postgres', 'repmgr', 'cluster', 'show')
|
|
40
|
-
rescue StandardError
|
|
41
|
-
''
|
|
42
|
-
end
|
|
30
|
+
def uninstall_from(host)
|
|
31
|
+
node_id = config.node_id_for(host)
|
|
43
32
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
33
|
+
ssh_executor.execute_on_host(host) do
|
|
34
|
+
cluster_output = begin
|
|
35
|
+
capture(:sudo, '-u', 'postgres', 'repmgr', 'cluster', 'show')
|
|
36
|
+
rescue StandardError
|
|
37
|
+
''
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if cluster_output.include?("| #{node_id}") && cluster_output.include?('running')
|
|
41
|
+
info "✓ Skipping repmgr removal from #{host} - node #{node_id} is running"
|
|
42
|
+
else
|
|
43
|
+
info "Removing repmgr from #{host}"
|
|
44
|
+
execute :sudo, 'apt-get', 'remove', '-y', '-q', 'postgresql-*-repmgr', '||', 'true'
|
|
50
45
|
end
|
|
51
46
|
end
|
|
52
47
|
end
|
|
@@ -62,7 +57,7 @@ module ActivePostgres
|
|
|
62
57
|
end
|
|
63
58
|
|
|
64
59
|
def setup_standby_only(standby_host)
|
|
65
|
-
puts "Setting up standby #{standby_host} (primary
|
|
60
|
+
puts "Setting up standby #{standby_host} (primary stays online)..."
|
|
66
61
|
|
|
67
62
|
version = config.version
|
|
68
63
|
install_apt_packages(standby_host, "postgresql-#{version}-repmgr")
|
|
@@ -103,7 +98,8 @@ module ActivePostgres
|
|
|
103
98
|
puts ' Setting up primary with repmgr...'
|
|
104
99
|
|
|
105
100
|
host = config.primary_host
|
|
106
|
-
|
|
101
|
+
primary_node_id = config.node_id_for(host)
|
|
102
|
+
repmgr_config = config.repmgr_config_for(host)
|
|
107
103
|
version = config.version
|
|
108
104
|
repmgr_password = normalize_repmgr_password(secrets.resolve('repmgr_password'))
|
|
109
105
|
replication_password = normalize_replication_password(secrets.resolve('replication_password'))
|
|
@@ -213,7 +209,7 @@ module ActivePostgres
|
|
|
213
209
|
unless cluster_show&.match?(/primary/i)
|
|
214
210
|
# Fallback: verify via repmgr metadata in the repmgr database
|
|
215
211
|
db_check = executor.run_sql_on_backend(self,
|
|
216
|
-
|
|
212
|
+
"SELECT type FROM repmgr.nodes WHERE node_id = #{primary_node_id};",
|
|
217
213
|
postgres_user: 'postgres',
|
|
218
214
|
database: repmgr_db,
|
|
219
215
|
tuples_only: true,
|
|
@@ -236,7 +232,7 @@ module ActivePostgres
|
|
|
236
232
|
puts " Setting up standby: #{standby_host}..."
|
|
237
233
|
|
|
238
234
|
host = standby_host
|
|
239
|
-
repmgr_config = config.
|
|
235
|
+
repmgr_config = config.repmgr_config_for(standby_host)
|
|
240
236
|
primary_replication_host = config.primary_replication_host
|
|
241
237
|
version = config.version
|
|
242
238
|
secrets_obj = secrets
|
|
@@ -244,13 +240,18 @@ module ActivePostgres
|
|
|
244
240
|
repmgr_db = config.repmgr_database
|
|
245
241
|
postgres_user = config.postgres_user
|
|
246
242
|
replication_user = config.replication_user
|
|
243
|
+
seed_method = config.standby_seed_method_for(standby_host)
|
|
244
|
+
restore_arguments = if seed_method == :pgbackrest
|
|
245
|
+
standby_label = config.node_label_for(standby_host) || "standby-#{standby_host.split('.').first}"
|
|
246
|
+
pgbackrest_restore_arguments(build_primary_conninfo(standby_label))
|
|
247
|
+
end
|
|
247
248
|
|
|
248
249
|
# Variables used in ERB templates via binding
|
|
249
250
|
_ = host
|
|
250
251
|
_ = repmgr_config
|
|
251
252
|
_ = secrets_obj
|
|
252
253
|
|
|
253
|
-
node_id = config.
|
|
254
|
+
node_id = config.node_id_for(standby_host)
|
|
254
255
|
repmgr_password = normalize_repmgr_password(secrets_obj.resolve('repmgr_password'))
|
|
255
256
|
replication_password = normalize_replication_password(secrets_obj.resolve('replication_password'))
|
|
256
257
|
if replication_user == repmgr_user && replication_password != repmgr_password
|
|
@@ -300,58 +301,61 @@ module ActivePostgres
|
|
|
300
301
|
nil
|
|
301
302
|
end
|
|
302
303
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
304
|
+
if seed_method == :repmgr
|
|
305
|
+
info 'Cloning from primary over private network...'
|
|
306
|
+
# Create a temporary repmgr config for cloning that points to the primary.
|
|
307
|
+
# The regular config points to localhost, which does not work during the initial clone.
|
|
308
|
+
temp_repmgr_conf = <<~CONF
|
|
309
|
+
node_id=#{node_id}
|
|
310
|
+
node_name='#{standby_host}'
|
|
311
|
+
conninfo='host=#{primary_replication_host} user=#{repmgr_user} dbname=#{repmgr_db} connect_timeout=10'
|
|
312
|
+
data_directory='/var/lib/postgresql/#{version}/main'
|
|
313
|
+
CONF
|
|
314
|
+
|
|
315
|
+
info 'Creating temporary repmgr config for cloning...'
|
|
316
|
+
upload! StringIO.new(temp_repmgr_conf), '/tmp/repmgr_clone.conf'
|
|
317
|
+
execute :sudo, 'mv', '/tmp/repmgr_clone.conf', '/etc/repmgr_clone.conf'
|
|
318
|
+
execute :sudo, 'chown', 'postgres:postgres', '/etc/repmgr_clone.conf'
|
|
319
|
+
execute :sudo, 'chmod', '600', '/etc/repmgr_clone.conf'
|
|
320
|
+
info 'Running: repmgr standby clone (this may take a few minutes to copy the database)...'
|
|
321
|
+
else
|
|
322
|
+
info 'Restoring the latest valid pgBackRest backup as a standby...'
|
|
323
|
+
end
|
|
320
324
|
|
|
321
325
|
clone_success = false
|
|
322
326
|
begin
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
327
|
+
if seed_method == :pgbackrest
|
|
328
|
+
execute(*restore_arguments)
|
|
329
|
+
info 'pgBackRest standby restore completed'
|
|
330
|
+
else
|
|
331
|
+
execute :sudo, '-u', postgres_user, 'env',
|
|
332
|
+
'HOME=/var/lib/postgresql',
|
|
333
|
+
'repmgr', 'standby', 'clone',
|
|
334
|
+
'-h', primary_replication_host,
|
|
335
|
+
'-U', repmgr_user,
|
|
336
|
+
'-d', repmgr_db,
|
|
337
|
+
'-f', '/etc/repmgr_clone.conf',
|
|
338
|
+
'--force',
|
|
339
|
+
'--verbose'
|
|
340
|
+
info 'Clone command completed'
|
|
341
|
+
end
|
|
334
342
|
|
|
335
|
-
# Check if clone actually succeeded
|
|
336
343
|
if test(:sudo, 'test', '-d', "/var/lib/postgresql/#{version}/main")
|
|
337
344
|
clone_success = true
|
|
338
|
-
info '
|
|
339
|
-
|
|
340
|
-
# Clean up temporary clone config
|
|
341
|
-
execute :sudo, 'rm', '-f', '/etc/repmgr_clone.conf'
|
|
345
|
+
info 'Standby data directory successfully seeded!'
|
|
346
|
+
execute :sudo, 'rm', '-f', '/etc/repmgr_clone.conf' if seed_method == :repmgr
|
|
342
347
|
else
|
|
343
|
-
error "Data directory /var/lib/postgresql/#{version}/main was not created even though
|
|
348
|
+
error "Data directory /var/lib/postgresql/#{version}/main was not created even though seeding succeeded!"
|
|
344
349
|
end
|
|
345
350
|
rescue SSHKit::Command::Failed => e
|
|
346
|
-
error "
|
|
351
|
+
error "Standby seed command failed with exit code #{e.message}"
|
|
347
352
|
rescue StandardError => e
|
|
348
|
-
error "
|
|
353
|
+
error "Standby seed command raised exception: #{e.class}: #{e.message}"
|
|
349
354
|
end
|
|
350
355
|
|
|
351
|
-
# If clone failed, raise error
|
|
352
356
|
unless clone_success
|
|
353
|
-
error '✗
|
|
354
|
-
raise '
|
|
357
|
+
error '✗ Standby data seeding failed'
|
|
358
|
+
raise 'standby data seeding failed to create data directory'
|
|
355
359
|
end
|
|
356
360
|
end
|
|
357
361
|
|
|
@@ -676,6 +680,7 @@ module ActivePostgres
|
|
|
676
680
|
puts 'Verifying PostgreSQL HA cluster health...'
|
|
677
681
|
|
|
678
682
|
primary_host = config.primary_host
|
|
683
|
+
primary_node_id = config.node_id_for(primary_host)
|
|
679
684
|
standby_hosts = config.standby_hosts
|
|
680
685
|
version = config.version
|
|
681
686
|
postgres_user = config.postgres_user
|
|
@@ -706,12 +711,12 @@ module ActivePostgres
|
|
|
706
711
|
cluster_output = capture(:sudo, '-u', 'postgres',
|
|
707
712
|
'repmgr', '-f', '/etc/repmgr.conf', 'cluster', 'show',
|
|
708
713
|
raise_on_non_zero_exit: false).to_s
|
|
709
|
-
#
|
|
710
|
-
if cluster_output.match?(/\s
|
|
714
|
+
# Check the configured node identity, which survives role changes.
|
|
715
|
+
if cluster_output.match?(/\s+#{primary_node_id}\s+\|.*primary.*\*\s+running/i)
|
|
711
716
|
info '✓ Primary is registered with repmgr'
|
|
712
717
|
else
|
|
713
718
|
db_check = executor.run_sql_on_backend(self,
|
|
714
|
-
|
|
719
|
+
"SELECT type FROM repmgr.nodes WHERE node_id = #{primary_node_id} AND active IS TRUE;",
|
|
715
720
|
postgres_user: postgres_user,
|
|
716
721
|
database: repmgr_db,
|
|
717
722
|
tuples_only: true,
|
|
@@ -907,6 +912,21 @@ module ActivePostgres
|
|
|
907
912
|
"host=#{primary_host} user=#{user} dbname=#{dbname} application_name=#{standby_label}"
|
|
908
913
|
end
|
|
909
914
|
|
|
915
|
+
def pgbackrest_restore_arguments(primary_conninfo)
|
|
916
|
+
command = Shellwords.join(
|
|
917
|
+
[
|
|
918
|
+
'pgbackrest', '--stanza=main', '--type=standby',
|
|
919
|
+
"--recovery-option=primary_conninfo=#{primary_conninfo}",
|
|
920
|
+
'restore'
|
|
921
|
+
]
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
[
|
|
925
|
+
:sudo, '-u', config.postgres_user,
|
|
926
|
+
:bash, '-lc', Shellwords.escape(command)
|
|
927
|
+
]
|
|
928
|
+
end
|
|
929
|
+
|
|
910
930
|
def normalize_repmgr_password(raw_password)
|
|
911
931
|
password = raw_password.to_s.rstrip
|
|
912
932
|
|
|
@@ -933,7 +953,7 @@ module ActivePostgres
|
|
|
933
953
|
|
|
934
954
|
def ensure_primary_registered
|
|
935
955
|
host = config.primary_host
|
|
936
|
-
config.
|
|
956
|
+
primary_node_id = config.node_id_for(host)
|
|
937
957
|
is_registered = false
|
|
938
958
|
|
|
939
959
|
ssh_executor.execute_on_host(host) do
|
|
@@ -947,8 +967,8 @@ module ActivePostgres
|
|
|
947
967
|
''
|
|
948
968
|
end
|
|
949
969
|
|
|
950
|
-
if cluster_output.
|
|
951
|
-
info
|
|
970
|
+
if cluster_output.match?(/\b#{primary_node_id}\s+\|.*primary/)
|
|
971
|
+
info "✓ Primary is registered (node_id=#{primary_node_id})"
|
|
952
972
|
else
|
|
953
973
|
warn "⚠ Primary not found in cluster, assuming it's registered"
|
|
954
974
|
end
|
|
@@ -1101,7 +1121,13 @@ module ActivePostgres
|
|
|
1101
1121
|
end
|
|
1102
1122
|
|
|
1103
1123
|
def enable_repmgrd_if_configured(host, repmgr_config)
|
|
1104
|
-
|
|
1124
|
+
if repmgr_config[:auto_failover] == false
|
|
1125
|
+
ssh_executor.execute_on_host(host) do
|
|
1126
|
+
execute :sudo, 'systemctl', 'stop', 'repmgrd', raise_on_non_zero_exit: false
|
|
1127
|
+
execute :sudo, 'systemctl', 'disable', 'repmgrd', raise_on_non_zero_exit: false
|
|
1128
|
+
end
|
|
1129
|
+
return
|
|
1130
|
+
end
|
|
1105
1131
|
|
|
1106
1132
|
default_config = <<~CONF
|
|
1107
1133
|
REPMGRD_ENABLED=yes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module ActivePostgres
|
|
2
|
+
class Configuration
|
|
3
|
+
module NodeIdentity
|
|
4
|
+
def node_id_for(host)
|
|
5
|
+
node = node_config_for(host)
|
|
6
|
+
raise Error, "Unknown PostgreSQL host: #{host}" unless node
|
|
7
|
+
|
|
8
|
+
value = node.fetch('node_id') { host == primary_host ? 1 : standby_hosts.index(host) + 2 }
|
|
9
|
+
raise Error, "node_id for #{host} must be a positive 32-bit integer" unless value.is_a?(Integer) && value.between?(1, 2_147_483_647)
|
|
10
|
+
|
|
11
|
+
value
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def validate_node_ids!
|
|
17
|
+
node_ids = all_hosts.map { |host| node_id_for(host) }
|
|
18
|
+
raise Error, 'PostgreSQL node_id values must be unique' unless node_ids.uniq.size == node_ids.size
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module ActivePostgres
|
|
2
|
+
class Configuration
|
|
3
|
+
module PgBackRestPolicy
|
|
4
|
+
private
|
|
5
|
+
|
|
6
|
+
def validate_pgbackrest!
|
|
7
|
+
return unless component_enabled?(:pgbackrest)
|
|
8
|
+
|
|
9
|
+
pg_config = component_config(:pgbackrest)
|
|
10
|
+
retention_full = pg_config[:retention_full]
|
|
11
|
+
retention_archive = pg_config[:retention_archive]
|
|
12
|
+
if retention_full && retention_archive && retention_archive.to_i < retention_full.to_i
|
|
13
|
+
raise Error, 'pgbackrest.retention_archive must be >= retention_full for PITR safety'
|
|
14
|
+
end
|
|
15
|
+
raise Error, 'pgbackrest.repo_block requires repo_bundle' if pg_config[:repo_block] && !pg_config[:repo_bundle]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module ActivePostgres
|
|
2
|
+
class Configuration
|
|
3
|
+
module StandbyPolicy
|
|
4
|
+
def standby_seed_method_for(host)
|
|
5
|
+
(standby_config_for(host)&.fetch('seed_method', nil) || 'repmgr').to_s.to_sym
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def validate_standby_seed_methods!
|
|
11
|
+
@standbys.each do |standby|
|
|
12
|
+
host = standby.fetch('host')
|
|
13
|
+
seed_method = standby_seed_method_for(host)
|
|
14
|
+
unless %i[repmgr pgbackrest].include?(seed_method)
|
|
15
|
+
raise Error, "Invalid seed_method '#{seed_method}' for standby #{host}. Use 'repmgr' or 'pgbackrest'."
|
|
16
|
+
end
|
|
17
|
+
if seed_method == :pgbackrest && !component_enabled?(:pgbackrest)
|
|
18
|
+
raise Error, "Standby #{host} requires the pgbackrest component for seed_method 'pgbackrest'"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
require 'yaml'
|
|
2
|
+
require_relative 'configuration/pgbackrest_policy'
|
|
3
|
+
require_relative 'configuration/standby_policy'
|
|
4
|
+
require_relative 'configuration/node_identity'
|
|
2
5
|
|
|
3
6
|
module ActivePostgres
|
|
4
7
|
class Configuration
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
include PgBackRestPolicy
|
|
9
|
+
include StandbyPolicy
|
|
10
|
+
include NodeIdentity
|
|
11
|
+
|
|
12
|
+
attr_reader :environment, :version, :user, :ssh_key, :ssh_known_hosts_file, :ssh_host_key_verification,
|
|
13
|
+
:primary, :standbys, :jump_hosts, :components, :secrets_config, :database_config
|
|
7
14
|
|
|
8
15
|
def initialize(config_hash, environment = 'development')
|
|
9
16
|
@environment = environment
|
|
@@ -14,6 +21,7 @@ module ActivePostgres
|
|
|
14
21
|
@version = env_config['version'] || 18
|
|
15
22
|
@user = env_config['user'] || 'ubuntu'
|
|
16
23
|
@ssh_key = File.expand_path(env_config['ssh_key'] || '~/.ssh/id_rsa')
|
|
24
|
+
@ssh_known_hosts_file = optional_path(env_config['ssh_known_hosts_file'])
|
|
17
25
|
@ssh_host_key_verification = normalize_ssh_host_key_verification(
|
|
18
26
|
env_config['ssh_host_key_verification'] || env_config['ssh_verify_host_key']
|
|
19
27
|
)
|
|
@@ -21,6 +29,7 @@ module ActivePostgres
|
|
|
21
29
|
@primary = env_config['primary'] || {}
|
|
22
30
|
@standbys = env_config['standby'] || []
|
|
23
31
|
@standbys = [@standbys] unless @standbys.is_a?(Array)
|
|
32
|
+
@jump_hosts = env_config['jump_hosts'] || {}
|
|
24
33
|
|
|
25
34
|
@components = parse_components(env_config['components'] || {})
|
|
26
35
|
@secrets_config = env_config['secrets'] || {}
|
|
@@ -111,6 +120,24 @@ module ActivePostgres
|
|
|
111
120
|
@standbys.find { |s| s['host'] == host }
|
|
112
121
|
end
|
|
113
122
|
|
|
123
|
+
def node_config_for(host)
|
|
124
|
+
return @primary if host == primary_host
|
|
125
|
+
|
|
126
|
+
standby_config_for(host)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def jump_host_config_for(host)
|
|
130
|
+
jump_host_name = node_config_for(host)&.dig('jump_host')
|
|
131
|
+
return unless jump_host_name
|
|
132
|
+
|
|
133
|
+
@jump_hosts[jump_host_name] || raise(Error, "Host #{host} jump_host not found: #{jump_host_name}")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def repmgr_config_for(host)
|
|
137
|
+
overrides = node_config_for(host)&.dig('repmgr') || {}
|
|
138
|
+
component_config(:repmgr).merge(symbolize_keys(overrides))
|
|
139
|
+
end
|
|
140
|
+
|
|
114
141
|
def node_label_for(host)
|
|
115
142
|
if host == primary_host
|
|
116
143
|
@primary['label']
|
|
@@ -122,6 +149,8 @@ module ActivePostgres
|
|
|
122
149
|
def validate!
|
|
123
150
|
raise Error, 'No primary host defined' unless primary_host
|
|
124
151
|
|
|
152
|
+
validate_node_ids!
|
|
153
|
+
|
|
125
154
|
# Validate required secrets if components are enabled
|
|
126
155
|
raise Error, 'Missing replication_password secret' if component_enabled?(:repmgr) && !secrets_config['replication_password']
|
|
127
156
|
raise Error, 'Missing monitoring_password secret' if component_enabled?(:monitoring) && !secrets_config['monitoring_password']
|
|
@@ -133,14 +162,7 @@ module ActivePostgres
|
|
|
133
162
|
raise Error, 'monitoring.grafana.host is required when grafana is enabled'
|
|
134
163
|
end
|
|
135
164
|
end
|
|
136
|
-
|
|
137
|
-
pg_config = component_config(:pgbackrest)
|
|
138
|
-
retention_full = pg_config[:retention_full]
|
|
139
|
-
retention_archive = pg_config[:retention_archive]
|
|
140
|
-
if retention_full && retention_archive && retention_archive.to_i < retention_full.to_i
|
|
141
|
-
raise Error, 'pgbackrest.retention_archive must be >= retention_full for PITR safety'
|
|
142
|
-
end
|
|
143
|
-
end
|
|
165
|
+
validate_pgbackrest!
|
|
144
166
|
|
|
145
167
|
if component_enabled?(:pgbouncer)
|
|
146
168
|
pgbouncer_app_hosts.each do |app_host|
|
|
@@ -149,6 +171,17 @@ module ActivePostgres
|
|
|
149
171
|
end
|
|
150
172
|
end
|
|
151
173
|
|
|
174
|
+
all_hosts.each do |host|
|
|
175
|
+
jump_host_name = node_config_for(host)&.dig('jump_host')
|
|
176
|
+
next unless jump_host_name
|
|
177
|
+
|
|
178
|
+
jump_host = @jump_hosts[jump_host_name]
|
|
179
|
+
raise Error, "Host #{host} jump_host not found: #{jump_host_name}" unless jump_host
|
|
180
|
+
raise Error, "Jump host #{jump_host_name} must include host" if jump_host['host'].to_s.strip.empty?
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
validate_standby_seed_methods!
|
|
184
|
+
|
|
152
185
|
if component_enabled?(:repmgr)
|
|
153
186
|
dns_failover = component_config(:repmgr)[:dns_failover]
|
|
154
187
|
if dns_failover && dns_failover[:enabled]
|
|
@@ -246,12 +279,6 @@ module ActivePostgres
|
|
|
246
279
|
result
|
|
247
280
|
end
|
|
248
281
|
|
|
249
|
-
def node_config_for(host)
|
|
250
|
-
return @primary if host == primary_host
|
|
251
|
-
|
|
252
|
-
standby_config_for(host)
|
|
253
|
-
end
|
|
254
|
-
|
|
255
282
|
def private_ip_for(node_config)
|
|
256
283
|
return unless node_config
|
|
257
284
|
|
|
@@ -295,5 +322,9 @@ module ActivePostgres
|
|
|
295
322
|
raise Error,
|
|
296
323
|
"Invalid ssh_host_key_verification '#{value}'. Use 'always' or 'accept_new'."
|
|
297
324
|
end
|
|
325
|
+
|
|
326
|
+
def optional_path(path)
|
|
327
|
+
File.expand_path(path) if path
|
|
328
|
+
end
|
|
298
329
|
end
|
|
299
330
|
end
|
|
@@ -13,12 +13,16 @@ module ActivePostgres
|
|
|
13
13
|
@skip_validation = skip_validation
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def execute
|
|
16
|
+
def execute(dry_run: false)
|
|
17
17
|
ErrorHandler.with_handling(context: { operation: operation_name }) do
|
|
18
18
|
print_header
|
|
19
19
|
validate_prerequisites
|
|
20
20
|
run_preflight_checks unless skip_validation
|
|
21
21
|
print_deployment_plan
|
|
22
|
+
if dry_run
|
|
23
|
+
logger.info "\n[DRY RUN] No changes made."
|
|
24
|
+
return
|
|
25
|
+
end
|
|
22
26
|
return unless confirm_deployment
|
|
23
27
|
|
|
24
28
|
rollback_manager.with_rollback(description: operation_name) do
|
|
@@ -57,6 +57,13 @@ shared: &shared
|
|
|
57
57
|
|
|
58
58
|
pgbackrest:
|
|
59
59
|
enabled: false
|
|
60
|
+
# For large, frequently changed relation files, bundle repository objects
|
|
61
|
+
# and back up changed blocks instead of copying each whole file.
|
|
62
|
+
# repo_bundle: true
|
|
63
|
+
# repo_block: true
|
|
64
|
+
# schedule_full: "0 2 * * 0"
|
|
65
|
+
# schedule_differential: "35 2 * * 1-6"
|
|
66
|
+
# schedule_incremental: "5 */4 * * *"
|
|
60
67
|
|
|
61
68
|
monitoring:
|
|
62
69
|
enabled: false
|
|
@@ -28,7 +28,7 @@ module ActivePostgres
|
|
|
28
28
|
rollback_manager: rollback_manager,
|
|
29
29
|
skip_validation: skip_validation
|
|
30
30
|
)
|
|
31
|
-
flow.execute
|
|
31
|
+
flow.execute(dry_run: dry_run)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def setup_component(component_name)
|
|
@@ -117,7 +117,7 @@ module ActivePostgres
|
|
|
117
117
|
rollback_manager: rollback_manager,
|
|
118
118
|
skip_validation: skip_validation
|
|
119
119
|
)
|
|
120
|
-
flow.execute
|
|
120
|
+
flow.execute(dry_run: dry_run)
|
|
121
121
|
end
|
|
122
122
|
end
|
|
123
123
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
require 'sshkit'
|
|
2
2
|
require 'sshkit/dsl'
|
|
3
|
+
require 'net/ssh/proxy/command'
|
|
3
4
|
require 'securerandom'
|
|
5
|
+
require 'shellwords'
|
|
4
6
|
require 'stringio'
|
|
5
7
|
|
|
6
8
|
module ActivePostgres
|
|
@@ -20,11 +22,11 @@ module ActivePostgres
|
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
def execute_on_host(host, &)
|
|
23
|
-
on(
|
|
25
|
+
on(ssh_host_for(host), &)
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def execute_on_host_as(host, user, &)
|
|
27
|
-
on(
|
|
29
|
+
on(ssh_host_for(host, user:), &)
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
def execute_on_primary(&)
|
|
@@ -32,12 +34,12 @@ module ActivePostgres
|
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
def execute_on_standbys(&)
|
|
35
|
-
hosts = config.standby_hosts.map { |
|
|
37
|
+
hosts = config.standby_hosts.map { |host| ssh_host_for(host) }
|
|
36
38
|
on(hosts, in: :parallel, &)
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
def execute_on_all_hosts(&)
|
|
40
|
-
hosts = config.all_hosts.map { |
|
|
42
|
+
hosts = config.all_hosts.map { |host| ssh_host_for(host) }
|
|
41
43
|
on(hosts, in: :parallel, &)
|
|
42
44
|
end
|
|
43
45
|
|
|
@@ -307,6 +309,8 @@ module ActivePostgres
|
|
|
307
309
|
number_of_password_prompts: 0
|
|
308
310
|
}
|
|
309
311
|
|
|
312
|
+
options[:user_known_hosts_file] = [config.ssh_known_hosts_file] if config.ssh_known_hosts_file
|
|
313
|
+
|
|
310
314
|
if config.ssh_key && File.exist?(config.ssh_key)
|
|
311
315
|
options[:keys] = [config.ssh_key]
|
|
312
316
|
options[:keys_only] = true
|
|
@@ -315,5 +319,58 @@ module ActivePostgres
|
|
|
315
319
|
ssh.ssh_options = options
|
|
316
320
|
end
|
|
317
321
|
end
|
|
322
|
+
|
|
323
|
+
def ssh_host_for(host, user: config.user)
|
|
324
|
+
ssh_host = SSHKit::Host.new(host)
|
|
325
|
+
ssh_host.user = user
|
|
326
|
+
|
|
327
|
+
node_config = config.node_config_for(host) || {}
|
|
328
|
+
ssh_host.port = node_config['ssh_port'] || node_config['port'] if node_config['ssh_port'] || node_config['port']
|
|
329
|
+
|
|
330
|
+
options = {
|
|
331
|
+
verify_host_key: config.ssh_host_key_verification || :always
|
|
332
|
+
}
|
|
333
|
+
options[:user_known_hosts_file] = [config.ssh_known_hosts_file] if config.ssh_known_hosts_file
|
|
334
|
+
|
|
335
|
+
if node_config['ssh_key']
|
|
336
|
+
options[:keys] = [File.expand_path(node_config['ssh_key'])]
|
|
337
|
+
options[:keys_only] = true
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
if (jump_host_config = config.jump_host_config_for(host))
|
|
341
|
+
options[:proxy] = jump_proxy(jump_host_config)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
ssh_host.ssh_options = options
|
|
345
|
+
ssh_host
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def jump_proxy(jump_host_config)
|
|
349
|
+
jump_user = jump_host_config['ssh_user'] || jump_host_config['user'] || config.user
|
|
350
|
+
jump_port = jump_host_config['ssh_port'] || jump_host_config['port']
|
|
351
|
+
jump_key = jump_host_config['ssh_key'] || config.ssh_key
|
|
352
|
+
command = [
|
|
353
|
+
'ssh',
|
|
354
|
+
'-F', '/dev/null',
|
|
355
|
+
'-o', 'BatchMode=yes',
|
|
356
|
+
'-o', 'ForwardAgent=no',
|
|
357
|
+
'-o', "StrictHostKeyChecking=#{open_ssh_host_key_mode}"
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
command.push('-o', "UserKnownHostsFile=#{config.ssh_known_hosts_file}") if config.ssh_known_hosts_file
|
|
361
|
+
|
|
362
|
+
command.push('-i', File.expand_path(jump_key), '-o', 'IdentitiesOnly=yes') if
|
|
363
|
+
jump_key && File.exist?(File.expand_path(jump_key))
|
|
364
|
+
|
|
365
|
+
command.push('-p', jump_port.to_s) if jump_port
|
|
366
|
+
command.push('-W', '%h:%p', "#{jump_user}@#{jump_host_config.fetch('host')}")
|
|
367
|
+
|
|
368
|
+
command_line = Shellwords.join(command).gsub('\\%h', '%h').gsub('\\%p', '%p')
|
|
369
|
+
Net::SSH::Proxy::Command.new(command_line)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def open_ssh_host_key_mode
|
|
373
|
+
config.ssh_host_key_verification == :accept_new ? 'accept-new' : 'yes'
|
|
374
|
+
end
|
|
318
375
|
end
|
|
319
376
|
end
|
|
@@ -21,9 +21,9 @@ module ActivePostgres
|
|
|
21
21
|
def validate_specific_requirements
|
|
22
22
|
abort "❌ #{standby_host} is not configured as a standby in config/postgres.yml" unless config.standby_hosts.include?(standby_host)
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
abort '❌ repmgr component must be enabled to setup standbys' unless config.component_enabled?(:repmgr)
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
validate_required_secrets!
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def run_preflight_checks
|
|
@@ -49,17 +49,22 @@ module ActivePostgres
|
|
|
49
49
|
|
|
50
50
|
def list_deployment_steps
|
|
51
51
|
logger.info " • Install PostgreSQL #{config.version} packages on #{standby_host}"
|
|
52
|
-
|
|
52
|
+
if config.standby_seed_method_for(standby_host) == :pgbackrest
|
|
53
|
+
logger.info ' • Restore the latest valid pgBackRest backup as a standby'
|
|
54
|
+
else
|
|
55
|
+
logger.info " • Clone data from primary: #{config.primary_host}"
|
|
56
|
+
end
|
|
53
57
|
logger.info ' • Register standby with repmgr cluster'
|
|
54
58
|
end
|
|
55
59
|
|
|
56
60
|
def list_warnings
|
|
57
|
-
logger.info "\n⚠️ Primary
|
|
61
|
+
logger.info "\n⚠️ Primary stays online; setup performs idempotent role and repmgr checks"
|
|
58
62
|
end
|
|
59
63
|
|
|
60
64
|
def deploy_components
|
|
61
65
|
deploy_ssl if config.component_enabled?(:ssl)
|
|
62
66
|
deploy_core
|
|
67
|
+
deploy_pgbackrest if config.component_enabled?(:pgbackrest)
|
|
63
68
|
deploy_repmgr
|
|
64
69
|
deploy_optional_components
|
|
65
70
|
end
|
|
@@ -79,8 +84,15 @@ module ActivePostgres
|
|
|
79
84
|
end
|
|
80
85
|
end
|
|
81
86
|
|
|
87
|
+
def deploy_pgbackrest
|
|
88
|
+
logger.task('Installing pgBackRest on standby') do
|
|
89
|
+
component = Components::PgBackRest.new(config, ssh_executor, secrets)
|
|
90
|
+
component.install_on_standby(standby_host)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
82
94
|
def deploy_repmgr
|
|
83
|
-
logger.task('Setting up repmgr and
|
|
95
|
+
logger.task('Setting up repmgr and seeding standby') do
|
|
84
96
|
component = Components::Repmgr.new(config, ssh_executor, secrets)
|
|
85
97
|
register_rollback('repmgr', component)
|
|
86
98
|
component.setup_standby_only(standby_host)
|
|
@@ -107,13 +119,39 @@ module ActivePostgres
|
|
|
107
119
|
end
|
|
108
120
|
|
|
109
121
|
def register_rollback(component_name, component)
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
target_host = standby_host
|
|
123
|
+
captured_logger = logger
|
|
124
|
+
|
|
125
|
+
rollback_manager.register("Uninstall #{component_name} on #{target_host}") do
|
|
126
|
+
if component.respond_to?(:uninstall_from)
|
|
127
|
+
component.uninstall_from(target_host)
|
|
128
|
+
else
|
|
129
|
+
component.uninstall
|
|
130
|
+
end
|
|
112
131
|
rescue StandardError => e
|
|
113
|
-
|
|
132
|
+
captured_logger.warn "Failed to uninstall #{component_name} on #{target_host}: #{e.message}"
|
|
114
133
|
end
|
|
115
134
|
end
|
|
116
135
|
|
|
136
|
+
def validate_required_secrets!
|
|
137
|
+
required = %w[repmgr_password replication_password]
|
|
138
|
+
required.push('ssl_cert', 'ssl_key') if config.component_enabled?(:ssl)
|
|
139
|
+
required << 'monitoring_password' if config.component_enabled?(:monitoring)
|
|
140
|
+
|
|
141
|
+
if config.component_enabled?(:pgbackrest)
|
|
142
|
+
pgbackrest = secrets.resolve_value(config.component_config(:pgbackrest))
|
|
143
|
+
if pgbackrest[:repo_type] == 's3'
|
|
144
|
+
required.push('s3_access_key', 's3_secret_key')
|
|
145
|
+
raise Error, 'pgbackrest.s3_bucket did not resolve' if pgbackrest[:s3_bucket].to_s.empty?
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
missing = required.uniq.select { |key| secrets.resolve(key).to_s.empty? }
|
|
150
|
+
return if missing.empty?
|
|
151
|
+
|
|
152
|
+
raise Error, "Required standby secrets did not resolve: #{missing.join(', ')}"
|
|
153
|
+
end
|
|
154
|
+
|
|
117
155
|
def list_next_steps
|
|
118
156
|
logger.info ' 1. Check cluster status: active_postgres status'
|
|
119
157
|
logger.info ' 2. Verify replication: Check repmgr cluster show'
|
|
@@ -40,6 +40,15 @@ repo1-path=<%= pgbackrest_config[:repo_path] || '/var/lib/pgbackrest' %>
|
|
|
40
40
|
repo1-retention-full=<%= pgbackrest_config[:retention_full] || 7 %>
|
|
41
41
|
repo1-retention-archive=<%= pgbackrest_config[:retention_archive] || 14 %>
|
|
42
42
|
|
|
43
|
+
<% if pgbackrest_config[:repo_bundle] %>
|
|
44
|
+
# Bundle small files and metadata into fewer repository objects.
|
|
45
|
+
repo1-bundle=y
|
|
46
|
+
<% end %>
|
|
47
|
+
<% if pgbackrest_config[:repo_block] %>
|
|
48
|
+
# Back up changed blocks instead of entire relation files.
|
|
49
|
+
repo1-block=y
|
|
50
|
+
<% end %>
|
|
51
|
+
|
|
43
52
|
# Compression
|
|
44
53
|
compress-type=lz4
|
|
45
54
|
compress-level=3
|
|
@@ -69,4 +78,3 @@ pg1-path=/var/lib/postgresql/<%= config.version %>/main
|
|
|
69
78
|
pg1-port=5432
|
|
70
79
|
pg1-socket-path=/var/run/postgresql
|
|
71
80
|
pg1-user=<%= config.postgres_user %>
|
|
72
|
-
|
data/templates/repmgr.conf.erb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_postgres
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.9.
|
|
4
|
+
version: 0.9.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- BoringCache
|
|
@@ -126,6 +126,9 @@ files:
|
|
|
126
126
|
- lib/active_postgres/components/repmgr.rb
|
|
127
127
|
- lib/active_postgres/components/ssl.rb
|
|
128
128
|
- lib/active_postgres/configuration.rb
|
|
129
|
+
- lib/active_postgres/configuration/node_identity.rb
|
|
130
|
+
- lib/active_postgres/configuration/pgbackrest_policy.rb
|
|
131
|
+
- lib/active_postgres/configuration/standby_policy.rb
|
|
129
132
|
- lib/active_postgres/connection_pooler.rb
|
|
130
133
|
- lib/active_postgres/credentials.rb
|
|
131
134
|
- lib/active_postgres/deployment_flow.rb
|