active_postgres 0.9.5 → 0.9.7
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 +40 -0
- data/README.md +47 -6
- data/SECURITY.md +21 -0
- data/lib/active_postgres/components/base.rb +2 -0
- data/lib/active_postgres/components/core.rb +11 -4
- data/lib/active_postgres/components/monitoring.rb +2 -2
- data/lib/active_postgres/components/pgbackrest.rb +13 -17
- data/lib/active_postgres/components/pgbouncer.rb +5 -9
- data/lib/active_postgres/components/repmgr.rb +165 -105
- 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 +64 -38
- data/lib/active_postgres/deployment_flow.rb +5 -1
- data/lib/active_postgres/direct_executor.rb +3 -9
- data/lib/active_postgres/error_handler.rb +18 -18
- data/lib/active_postgres/generators/active_postgres/templates/postgres.yml.erb +7 -0
- data/lib/active_postgres/health_checker.rb +10 -12
- data/lib/active_postgres/installer.rb +2 -2
- data/lib/active_postgres/overview.rb +10 -10
- data/lib/active_postgres/secrets.rb +6 -7
- data/lib/active_postgres/ssh_executor.rb +66 -9
- data/lib/active_postgres/standby_deployment_flow.rb +46 -8
- data/lib/active_postgres/version.rb +1 -1
- data/lib/tasks/postgres.rake +26 -26
- data/templates/pgbackrest.conf.erb +9 -1
- data/templates/postgresql.conf.erb +5 -0
- data/templates/repmgr_dns_failover.sh.erb +2 -2
- metadata +18 -56
|
@@ -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 = host == config.primary_host ? 1 : (config.standby_hosts.index(host) || 997) + 2
|
|
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,7 @@ module ActivePostgres
|
|
|
103
98
|
puts ' Setting up primary with repmgr...'
|
|
104
99
|
|
|
105
100
|
host = config.primary_host
|
|
106
|
-
repmgr_config = config.
|
|
101
|
+
repmgr_config = config.repmgr_config_for(host)
|
|
107
102
|
version = config.version
|
|
108
103
|
repmgr_password = normalize_repmgr_password(secrets.resolve('repmgr_password'))
|
|
109
104
|
replication_password = normalize_replication_password(secrets.resolve('replication_password'))
|
|
@@ -114,6 +109,7 @@ module ActivePostgres
|
|
|
114
109
|
if replication_user == repmgr_user && replication_password != repmgr_password
|
|
115
110
|
raise Error, 'replication_user matches repmgr user but passwords differ. Use a distinct replication_user or the same password.'
|
|
116
111
|
end
|
|
112
|
+
|
|
117
113
|
effective_replication_password = replication_user == repmgr_user ? repmgr_password : replication_password
|
|
118
114
|
|
|
119
115
|
# Variables used in ERB templates via binding
|
|
@@ -174,13 +170,13 @@ module ActivePostgres
|
|
|
174
170
|
info 'Creating repmgr database and user...'
|
|
175
171
|
repmgr_sql = repmgr_component.send(:build_repmgr_setup_sql, repmgr_user, repmgr_db, repmgr_password)
|
|
176
172
|
executor.run_sql_on_backend(self, repmgr_sql, postgres_user: 'postgres', port: 5432, tuples_only: false,
|
|
177
|
-
|
|
173
|
+
capture: false)
|
|
178
174
|
|
|
179
175
|
if replication_user != repmgr_user
|
|
180
176
|
info 'Ensuring replication user exists...'
|
|
181
177
|
repl_sql = repmgr_component.send(:build_replication_user_sql, replication_user, effective_replication_password)
|
|
182
178
|
executor.run_sql_on_backend(self, repl_sql, postgres_user: 'postgres', port: 5432, tuples_only: false,
|
|
183
|
-
|
|
179
|
+
capture: false)
|
|
184
180
|
end
|
|
185
181
|
|
|
186
182
|
info 'Reloading PostgreSQL configuration to apply pg_hba.conf changes...'
|
|
@@ -202,14 +198,14 @@ module ActivePostgres
|
|
|
202
198
|
cluster_show = nil
|
|
203
199
|
5.times do |attempt|
|
|
204
200
|
cluster_show = capture(:sudo, '-u', 'postgres', 'bash', '-lc',
|
|
205
|
-
|
|
201
|
+
'repmgr cluster show -f /etc/repmgr.conf 2>&1', raise_on_non_zero_exit: false).to_s
|
|
206
202
|
|
|
207
203
|
break if cluster_show.match?(/primary/i)
|
|
208
204
|
|
|
209
205
|
sleep 2 if attempt < 4
|
|
210
206
|
end
|
|
211
207
|
|
|
212
|
-
unless cluster_show
|
|
208
|
+
unless cluster_show&.match?(/primary/i)
|
|
213
209
|
# Fallback: verify via repmgr metadata in the repmgr database
|
|
214
210
|
db_check = executor.run_sql_on_backend(self,
|
|
215
211
|
'SELECT type FROM repmgr.nodes WHERE node_id = 1;',
|
|
@@ -235,7 +231,7 @@ module ActivePostgres
|
|
|
235
231
|
puts " Setting up standby: #{standby_host}..."
|
|
236
232
|
|
|
237
233
|
host = standby_host
|
|
238
|
-
repmgr_config = config.
|
|
234
|
+
repmgr_config = config.repmgr_config_for(standby_host)
|
|
239
235
|
primary_replication_host = config.primary_replication_host
|
|
240
236
|
version = config.version
|
|
241
237
|
secrets_obj = secrets
|
|
@@ -243,6 +239,11 @@ module ActivePostgres
|
|
|
243
239
|
repmgr_db = config.repmgr_database
|
|
244
240
|
postgres_user = config.postgres_user
|
|
245
241
|
replication_user = config.replication_user
|
|
242
|
+
seed_method = config.standby_seed_method_for(standby_host)
|
|
243
|
+
restore_arguments = if seed_method == :pgbackrest
|
|
244
|
+
standby_label = config.node_label_for(standby_host) || "standby-#{standby_host.split('.').first}"
|
|
245
|
+
pgbackrest_restore_arguments(build_primary_conninfo(standby_label))
|
|
246
|
+
end
|
|
246
247
|
|
|
247
248
|
# Variables used in ERB templates via binding
|
|
248
249
|
_ = host
|
|
@@ -255,13 +256,14 @@ module ActivePostgres
|
|
|
255
256
|
if replication_user == repmgr_user && replication_password != repmgr_password
|
|
256
257
|
raise Error, 'replication_user matches repmgr user but passwords differ. Use a distinct replication_user or the same password.'
|
|
257
258
|
end
|
|
259
|
+
|
|
258
260
|
effective_replication_password = replication_user == repmgr_user ? repmgr_password : replication_password
|
|
259
261
|
|
|
260
262
|
ensure_primary_registered
|
|
261
263
|
ensure_primary_replication_ready(repmgr_password, effective_replication_password)
|
|
262
264
|
|
|
263
265
|
setup_pgpass_file(standby_host, repmgr_password, replication_password: effective_replication_password,
|
|
264
|
-
|
|
266
|
+
primary_ip: primary_replication_host)
|
|
265
267
|
|
|
266
268
|
if standby_already_configured?(standby_host)
|
|
267
269
|
puts ' Standby already configured, updating configs...'
|
|
@@ -298,58 +300,61 @@ module ActivePostgres
|
|
|
298
300
|
nil
|
|
299
301
|
end
|
|
300
302
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
303
|
+
if seed_method == :repmgr
|
|
304
|
+
info 'Cloning from primary over private network...'
|
|
305
|
+
# Create a temporary repmgr config for cloning that points to the primary.
|
|
306
|
+
# The regular config points to localhost, which does not work during the initial clone.
|
|
307
|
+
temp_repmgr_conf = <<~CONF
|
|
308
|
+
node_id=#{node_id}
|
|
309
|
+
node_name='#{standby_host}'
|
|
310
|
+
conninfo='host=#{primary_replication_host} user=#{repmgr_user} dbname=#{repmgr_db} connect_timeout=10'
|
|
311
|
+
data_directory='/var/lib/postgresql/#{version}/main'
|
|
312
|
+
CONF
|
|
313
|
+
|
|
314
|
+
info 'Creating temporary repmgr config for cloning...'
|
|
315
|
+
upload! StringIO.new(temp_repmgr_conf), '/tmp/repmgr_clone.conf'
|
|
316
|
+
execute :sudo, 'mv', '/tmp/repmgr_clone.conf', '/etc/repmgr_clone.conf'
|
|
317
|
+
execute :sudo, 'chown', 'postgres:postgres', '/etc/repmgr_clone.conf'
|
|
318
|
+
execute :sudo, 'chmod', '600', '/etc/repmgr_clone.conf'
|
|
319
|
+
info 'Running: repmgr standby clone (this may take a few minutes to copy the database)...'
|
|
320
|
+
else
|
|
321
|
+
info 'Restoring the latest valid pgBackRest backup as a standby...'
|
|
322
|
+
end
|
|
318
323
|
|
|
319
324
|
clone_success = false
|
|
320
325
|
begin
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
326
|
+
if seed_method == :pgbackrest
|
|
327
|
+
execute(*restore_arguments)
|
|
328
|
+
info 'pgBackRest standby restore completed'
|
|
329
|
+
else
|
|
330
|
+
execute :sudo, '-u', postgres_user, 'env',
|
|
331
|
+
'HOME=/var/lib/postgresql',
|
|
332
|
+
'repmgr', 'standby', 'clone',
|
|
333
|
+
'-h', primary_replication_host,
|
|
334
|
+
'-U', repmgr_user,
|
|
335
|
+
'-d', repmgr_db,
|
|
336
|
+
'-f', '/etc/repmgr_clone.conf',
|
|
337
|
+
'--force',
|
|
338
|
+
'--verbose'
|
|
339
|
+
info 'Clone command completed'
|
|
340
|
+
end
|
|
332
341
|
|
|
333
|
-
# Check if clone actually succeeded
|
|
334
342
|
if test(:sudo, 'test', '-d', "/var/lib/postgresql/#{version}/main")
|
|
335
343
|
clone_success = true
|
|
336
|
-
info '
|
|
337
|
-
|
|
338
|
-
# Clean up temporary clone config
|
|
339
|
-
execute :sudo, 'rm', '-f', '/etc/repmgr_clone.conf'
|
|
344
|
+
info 'Standby data directory successfully seeded!'
|
|
345
|
+
execute :sudo, 'rm', '-f', '/etc/repmgr_clone.conf' if seed_method == :repmgr
|
|
340
346
|
else
|
|
341
|
-
error "Data directory /var/lib/postgresql/#{version}/main was not created even though
|
|
347
|
+
error "Data directory /var/lib/postgresql/#{version}/main was not created even though seeding succeeded!"
|
|
342
348
|
end
|
|
343
349
|
rescue SSHKit::Command::Failed => e
|
|
344
|
-
error "
|
|
350
|
+
error "Standby seed command failed with exit code #{e.message}"
|
|
345
351
|
rescue StandardError => e
|
|
346
|
-
error "
|
|
352
|
+
error "Standby seed command raised exception: #{e.class}: #{e.message}"
|
|
347
353
|
end
|
|
348
354
|
|
|
349
|
-
# If clone failed, raise error
|
|
350
355
|
unless clone_success
|
|
351
|
-
error '✗
|
|
352
|
-
raise '
|
|
356
|
+
error '✗ Standby data seeding failed'
|
|
357
|
+
raise 'standby data seeding failed to create data directory'
|
|
353
358
|
end
|
|
354
359
|
end
|
|
355
360
|
|
|
@@ -418,9 +423,10 @@ module ActivePostgres
|
|
|
418
423
|
|
|
419
424
|
def setup_inter_node_ssh
|
|
420
425
|
dns_config = dns_failover_config
|
|
421
|
-
key_path = dns_config && dns_config[:ssh_key_path] || '/var/lib/postgresql/.ssh/active_postgres_dns'
|
|
426
|
+
key_path = (dns_config && dns_config[:ssh_key_path]) || '/var/lib/postgresql/.ssh/active_postgres_dns'
|
|
422
427
|
postgres_user = config.postgres_user
|
|
423
428
|
all_hosts = config.all_hosts
|
|
429
|
+
replication_hosts = all_hosts.to_h { |host| [host, config.replication_host_for(host)] }
|
|
424
430
|
pub_keys = {}
|
|
425
431
|
|
|
426
432
|
all_hosts.each do |host|
|
|
@@ -430,18 +436,35 @@ module ActivePostgres
|
|
|
430
436
|
all_hosts.each do |host|
|
|
431
437
|
ssh_executor.execute_on_host(host) do
|
|
432
438
|
other_keys = pub_keys.reject { |h, _| h == host }.values
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
439
|
+
authorized_keys_path = '/var/lib/postgresql/.ssh/authorized_keys'
|
|
440
|
+
existing_keys = if test(:sudo, 'test', '-f', authorized_keys_path)
|
|
441
|
+
capture(:sudo, 'cat', authorized_keys_path).lines.map(&:strip)
|
|
442
|
+
else
|
|
443
|
+
[]
|
|
444
|
+
end
|
|
445
|
+
authorized_keys = (existing_keys + other_keys.map(&:to_s).map(&:strip)).reject(&:empty?).uniq.join("\n")
|
|
446
|
+
unless authorized_keys.empty?
|
|
447
|
+
upload! StringIO.new("#{authorized_keys}\n"), '/tmp/pg_authorized_keys'
|
|
448
|
+
execute :sudo, 'mv', '/tmp/pg_authorized_keys', authorized_keys_path
|
|
449
|
+
execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", authorized_keys_path
|
|
450
|
+
execute :sudo, 'chmod', '600', authorized_keys_path
|
|
440
451
|
end
|
|
441
452
|
|
|
442
|
-
peer_ips = all_hosts.reject { |h| h == host }.map { |h|
|
|
443
|
-
|
|
444
|
-
|
|
453
|
+
peer_ips = all_hosts.reject { |h| h == host }.map { |h| replication_hosts[h] }
|
|
454
|
+
known_hosts_path = '/var/lib/postgresql/.ssh/known_hosts'
|
|
455
|
+
existing_hosts = if test(:sudo, 'test', '-f', known_hosts_path)
|
|
456
|
+
capture(:sudo, 'cat', known_hosts_path).lines.map(&:strip)
|
|
457
|
+
else
|
|
458
|
+
[]
|
|
459
|
+
end
|
|
460
|
+
scanned_hosts = capture(:sudo, '-u', postgres_user, 'ssh-keyscan', *peer_ips, raise_on_non_zero_exit: false)
|
|
461
|
+
known_hosts = (existing_hosts + scanned_hosts.lines.map(&:strip)).reject(&:empty?).uniq.join("\n")
|
|
462
|
+
unless known_hosts.empty?
|
|
463
|
+
upload! StringIO.new("#{known_hosts}\n"), '/tmp/pg_known_hosts'
|
|
464
|
+
execute :sudo, 'mv', '/tmp/pg_known_hosts', known_hosts_path
|
|
465
|
+
execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", known_hosts_path
|
|
466
|
+
execute :sudo, 'chmod', '600', known_hosts_path
|
|
467
|
+
end
|
|
445
468
|
end
|
|
446
469
|
end
|
|
447
470
|
end
|
|
@@ -518,10 +541,21 @@ module ActivePostgres
|
|
|
518
541
|
public_key = capture(:sudo, '-u', postgres_user, 'cat', "#{key_path}.pub").strip
|
|
519
542
|
|
|
520
543
|
unless dns_servers.empty?
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
544
|
+
known_hosts_path = '/var/lib/postgresql/.ssh/known_hosts'
|
|
545
|
+
existing_hosts = if test(:sudo, 'test', '-f', known_hosts_path)
|
|
546
|
+
capture(:sudo, 'cat', known_hosts_path).lines.map(&:strip)
|
|
547
|
+
else
|
|
548
|
+
[]
|
|
549
|
+
end
|
|
550
|
+
scanned_hosts = capture(:sudo, '-u', postgres_user, 'ssh-keyscan', '-H', *dns_servers,
|
|
551
|
+
raise_on_non_zero_exit: false)
|
|
552
|
+
known_hosts = (existing_hosts + scanned_hosts.lines.map(&:strip)).reject(&:empty?).uniq.join("\n")
|
|
553
|
+
unless known_hosts.empty?
|
|
554
|
+
upload! StringIO.new("#{known_hosts}\n"), '/tmp/pg_known_hosts'
|
|
555
|
+
execute :sudo, 'mv', '/tmp/pg_known_hosts', known_hosts_path
|
|
556
|
+
execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", known_hosts_path
|
|
557
|
+
execute :sudo, 'chmod', '600', known_hosts_path
|
|
558
|
+
end
|
|
525
559
|
end
|
|
526
560
|
end
|
|
527
561
|
|
|
@@ -542,8 +576,8 @@ module ActivePostgres
|
|
|
542
576
|
|
|
543
577
|
upload! StringIO.new("#{key}\n"), '/tmp/active_postgres_dns_key.pub'
|
|
544
578
|
execute :bash, '-c',
|
|
545
|
-
|
|
546
|
-
|
|
579
|
+
'grep -qxF -f /tmp/active_postgres_dns_key.pub ~/.ssh/authorized_keys || ' \
|
|
580
|
+
'cat /tmp/active_postgres_dns_key.pub >> ~/.ssh/authorized_keys'
|
|
547
581
|
execute :rm, '-f', '/tmp/active_postgres_dns_key.pub'
|
|
548
582
|
end
|
|
549
583
|
end
|
|
@@ -560,11 +594,11 @@ module ActivePostgres
|
|
|
560
594
|
|
|
561
595
|
domains = normalize_dns_domains(dns_config)
|
|
562
596
|
primary_records = normalize_dns_records(dns_config[:primary_records] || dns_config[:primary_record],
|
|
563
|
-
|
|
564
|
-
|
|
597
|
+
default_prefix: 'db-primary',
|
|
598
|
+
domains: domains)
|
|
565
599
|
replica_records = normalize_dns_records(dns_config[:replica_records] || dns_config[:replica_record],
|
|
566
|
-
|
|
567
|
-
|
|
600
|
+
default_prefix: 'db-replica',
|
|
601
|
+
domains: domains)
|
|
568
602
|
|
|
569
603
|
_ = primary_records
|
|
570
604
|
_ = replica_records
|
|
@@ -732,10 +766,10 @@ module ActivePostgres
|
|
|
732
766
|
# Check replication status
|
|
733
767
|
begin
|
|
734
768
|
rep_status = executor.run_sql_on_backend(self,
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
769
|
+
'SELECT pg_is_in_recovery();',
|
|
770
|
+
postgres_user: postgres_user,
|
|
771
|
+
tuples_only: true,
|
|
772
|
+
capture: true).to_s
|
|
739
773
|
if rep_status.include?('t')
|
|
740
774
|
info '✓ Standby is in recovery mode (receiving replication)'
|
|
741
775
|
else
|
|
@@ -750,10 +784,10 @@ module ActivePostgres
|
|
|
750
784
|
# Check lag
|
|
751
785
|
begin
|
|
752
786
|
lag_result = executor.run_sql_on_backend(self,
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
787
|
+
'SELECT EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))::int AS lag;',
|
|
788
|
+
postgres_user: postgres_user,
|
|
789
|
+
tuples_only: true,
|
|
790
|
+
capture: true).to_s.strip
|
|
757
791
|
info "Replication lag: #{lag_result}"
|
|
758
792
|
rescue StandardError => e
|
|
759
793
|
error "Failed to get replication lag: #{e.message}"
|
|
@@ -785,7 +819,7 @@ module ActivePostgres
|
|
|
785
819
|
# Create .pgpass file for postgres user to avoid password exposure in logs
|
|
786
820
|
# Format: hostname:port:database:username:password
|
|
787
821
|
pgpass_content = build_pgpass_content(host, repmgr_password, replication_password: replication_password,
|
|
788
|
-
|
|
822
|
+
primary_ip: primary_ip)
|
|
789
823
|
|
|
790
824
|
ssh_executor.execute_on_host(host) do
|
|
791
825
|
# Create .pgpass in postgres user's home directory
|
|
@@ -854,8 +888,13 @@ module ActivePostgres
|
|
|
854
888
|
def escape_pgpass_value(value)
|
|
855
889
|
return '' if value.nil?
|
|
856
890
|
|
|
857
|
-
|
|
858
|
-
|
|
891
|
+
value.to_s.each_char.map do |char|
|
|
892
|
+
case char
|
|
893
|
+
when '\\' then '\\\\'
|
|
894
|
+
when ':' then '\\:'
|
|
895
|
+
else char
|
|
896
|
+
end
|
|
897
|
+
end.join
|
|
859
898
|
end
|
|
860
899
|
|
|
861
900
|
def build_primary_conninfo(standby_label)
|
|
@@ -871,6 +910,21 @@ module ActivePostgres
|
|
|
871
910
|
"host=#{primary_host} user=#{user} dbname=#{dbname} application_name=#{standby_label}"
|
|
872
911
|
end
|
|
873
912
|
|
|
913
|
+
def pgbackrest_restore_arguments(primary_conninfo)
|
|
914
|
+
command = Shellwords.join(
|
|
915
|
+
[
|
|
916
|
+
'pgbackrest', '--stanza=main', '--type=standby',
|
|
917
|
+
"--recovery-option=primary_conninfo=#{primary_conninfo}",
|
|
918
|
+
'restore'
|
|
919
|
+
]
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
[
|
|
923
|
+
:sudo, '-u', config.postgres_user,
|
|
924
|
+
:bash, '-lc', Shellwords.escape(command)
|
|
925
|
+
]
|
|
926
|
+
end
|
|
927
|
+
|
|
874
928
|
def normalize_repmgr_password(raw_password)
|
|
875
929
|
password = raw_password.to_s.rstrip
|
|
876
930
|
|
|
@@ -935,12 +989,12 @@ module ActivePostgres
|
|
|
935
989
|
ssh_executor.execute_on_host(host) do
|
|
936
990
|
repmgr_sql = repmgr_component.send(:build_repmgr_setup_sql, repmgr_user, repmgr_db, repmgr_password)
|
|
937
991
|
executor.run_sql_on_backend(self, repmgr_sql, postgres_user: 'postgres', port: 5432, tuples_only: false,
|
|
938
|
-
|
|
992
|
+
capture: false)
|
|
939
993
|
|
|
940
994
|
if replication_user != repmgr_user
|
|
941
995
|
repl_sql = repmgr_component.send(:build_replication_user_sql, replication_user, effective_replication_password)
|
|
942
996
|
executor.run_sql_on_backend(self, repl_sql, postgres_user: 'postgres', port: 5432, tuples_only: false,
|
|
943
|
-
|
|
997
|
+
capture: false)
|
|
944
998
|
end
|
|
945
999
|
|
|
946
1000
|
info '✓ Primary replication user is ready'
|
|
@@ -1065,7 +1119,13 @@ module ActivePostgres
|
|
|
1065
1119
|
end
|
|
1066
1120
|
|
|
1067
1121
|
def enable_repmgrd_if_configured(host, repmgr_config)
|
|
1068
|
-
|
|
1122
|
+
if repmgr_config[:auto_failover] == false
|
|
1123
|
+
ssh_executor.execute_on_host(host) do
|
|
1124
|
+
execute :sudo, 'systemctl', 'stop', 'repmgrd', raise_on_non_zero_exit: false
|
|
1125
|
+
execute :sudo, 'systemctl', 'disable', 'repmgrd', raise_on_non_zero_exit: false
|
|
1126
|
+
end
|
|
1127
|
+
return
|
|
1128
|
+
end
|
|
1069
1129
|
|
|
1070
1130
|
default_config = <<~CONF
|
|
1071
1131
|
REPMGRD_ENABLED=yes
|
|
@@ -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
|