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.
@@ -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 do |host|
26
- is_primary = host == config.primary_host
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
- ssh_executor.execute_on_host(host) do
38
- cluster_output = begin
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
- if cluster_output.include?("| #{node_id}") && cluster_output.include?('running')
45
- info "✓ Skipping repmgr removal from #{host} - node #{node_id} is running"
46
- else
47
- info "Removing repmgr from #{host}"
48
- execute :sudo, 'apt-get', 'remove', '-y', '-q', 'postgresql-*-repmgr', '||', 'true'
49
- end
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 will not be touched)..."
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.component_config(:repmgr)
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
- capture: false)
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
- capture: false)
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
- "repmgr cluster show -f /etc/repmgr.conf 2>&1", raise_on_non_zero_exit: false).to_s
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 && cluster_show.match?(/primary/i)
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.component_config(:repmgr)
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
- primary_ip: primary_replication_host)
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
- info 'Cloning from primary over private network...'
302
- # Create a temporary repmgr config for cloning that points to the primary
303
- # The regular config points to localhost which doesn't work during initial clone
304
- temp_repmgr_conf = <<~CONF
305
- node_id=#{node_id}
306
- node_name='#{standby_host}'
307
- conninfo='host=#{primary_replication_host} user=#{repmgr_user} dbname=#{repmgr_db} connect_timeout=10'
308
- data_directory='/var/lib/postgresql/#{version}/main'
309
- CONF
310
-
311
- info 'Creating temporary repmgr config for cloning...'
312
- upload! StringIO.new(temp_repmgr_conf), '/tmp/repmgr_clone.conf'
313
- execute :sudo, 'mv', '/tmp/repmgr_clone.conf', '/etc/repmgr_clone.conf'
314
- execute :sudo, 'chown', 'postgres:postgres', '/etc/repmgr_clone.conf'
315
- execute :sudo, 'chmod', '600', '/etc/repmgr_clone.conf'
316
-
317
- info 'Running: repmgr standby clone (this may take a few minutes to copy the database)...'
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
- execute :sudo, '-u', postgres_user, 'env',
322
- 'HOME=/var/lib/postgresql',
323
- 'repmgr', 'standby', 'clone',
324
- '-h', primary_replication_host,
325
- '-U', repmgr_user,
326
- '-d', repmgr_db,
327
- '-f', '/etc/repmgr_clone.conf',
328
- '--force',
329
- '--verbose'
330
-
331
- info 'Clone command completed'
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 'Data directory successfully cloned!'
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 command succeeded!"
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 "Clone command failed with exit code #{e.message}"
350
+ error "Standby seed command failed with exit code #{e.message}"
345
351
  rescue StandardError => e
346
- error "Clone command raised exception: #{e.class}: #{e.message}"
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 '✗ repmgr standby clone failed'
352
- raise 'repmgr standby clone failed to create data directory'
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
- other_keys.each do |key|
434
- next if key.to_s.empty?
435
-
436
- upload! StringIO.new("#{key}\n"), '/tmp/pg_peer_key.pub'
437
- execute :sudo, '-u', postgres_user, 'bash', '-c',
438
- "grep -qxF -f /tmp/pg_peer_key.pub /var/lib/postgresql/.ssh/authorized_keys 2>/dev/null || cat /tmp/pg_peer_key.pub >> /var/lib/postgresql/.ssh/authorized_keys"
439
- execute :rm, '-f', '/tmp/pg_peer_key.pub'
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| config.replication_host_for(h) }
443
- scan_cmd = "ssh-keyscan #{peer_ips.join(' ')} >> /var/lib/postgresql/.ssh/known_hosts 2>/dev/null || true"
444
- execute :sudo, '-u', postgres_user, 'bash', '-c', scan_cmd
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
- execute :sudo, '-u', postgres_user, 'touch', '/var/lib/postgresql/.ssh/known_hosts'
522
- scan_cmd = "ssh-keyscan -H #{dns_servers.join(' ')} >> /var/lib/postgresql/.ssh/known_hosts 2>/dev/null || true"
523
- execute :sudo, '-u', postgres_user, 'bash', '-c', scan_cmd
524
- execute :sudo, '-u', postgres_user, 'chmod', '600', '/var/lib/postgresql/.ssh/known_hosts'
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
- "grep -qxF -f /tmp/active_postgres_dns_key.pub ~/.ssh/authorized_keys || " \
546
- "cat /tmp/active_postgres_dns_key.pub >> ~/.ssh/authorized_keys"
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
- default_prefix: 'db-primary',
564
- domains: domains)
597
+ default_prefix: 'db-primary',
598
+ domains: domains)
565
599
  replica_records = normalize_dns_records(dns_config[:replica_records] || dns_config[:replica_record],
566
- default_prefix: 'db-replica',
567
- domains: domains)
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
- 'SELECT pg_is_in_recovery();',
736
- postgres_user: postgres_user,
737
- tuples_only: true,
738
- capture: true).to_s
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
- 'SELECT EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))::int AS lag;',
754
- postgres_user: postgres_user,
755
- tuples_only: true,
756
- capture: true).to_s.strip
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
- primary_ip: primary_ip)
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
- # Must escape backslashes first, then colons (order matters!)
858
- value.to_s.gsub('\\', '\\\\\\\\').gsub(':', '\\:')
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
- capture: false)
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
- capture: false)
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
- return if repmgr_config[:auto_failover] == false
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