active_postgres 0.8.0 → 0.9.1

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +142 -5
  3. data/lib/active_postgres/cli.rb +7 -0
  4. data/lib/active_postgres/components/core.rb +2 -7
  5. data/lib/active_postgres/components/extensions.rb +40 -35
  6. data/lib/active_postgres/components/monitoring.rb +256 -4
  7. data/lib/active_postgres/components/pgbackrest.rb +91 -5
  8. data/lib/active_postgres/components/pgbouncer.rb +58 -7
  9. data/lib/active_postgres/components/repmgr.rb +448 -62
  10. data/lib/active_postgres/configuration.rb +66 -1
  11. data/lib/active_postgres/connection_pooler.rb +3 -12
  12. data/lib/active_postgres/credentials.rb +3 -3
  13. data/lib/active_postgres/direct_executor.rb +1 -2
  14. data/lib/active_postgres/generators/active_postgres/install_generator.rb +2 -0
  15. data/lib/active_postgres/generators/active_postgres/templates/postgres.yml.erb +28 -1
  16. data/lib/active_postgres/health_checker.rb +29 -7
  17. data/lib/active_postgres/installer.rb +9 -0
  18. data/lib/active_postgres/overview.rb +351 -0
  19. data/lib/active_postgres/rollback_manager.rb +4 -8
  20. data/lib/active_postgres/secrets.rb +23 -5
  21. data/lib/active_postgres/ssh_executor.rb +44 -19
  22. data/lib/active_postgres/version.rb +1 -1
  23. data/lib/active_postgres.rb +1 -0
  24. data/lib/tasks/postgres.rake +77 -4
  25. data/lib/tasks/rotate_credentials.rake +4 -16
  26. data/templates/pg_hba.conf.erb +4 -1
  27. data/templates/pgbackrest.conf.erb +28 -0
  28. data/templates/pgbouncer-follow-primary.service.erb +8 -0
  29. data/templates/pgbouncer-follow-primary.timer.erb +11 -0
  30. data/templates/pgbouncer.ini.erb +2 -0
  31. data/templates/pgbouncer_follow_primary.sh.erb +34 -0
  32. data/templates/postgresql.conf.erb +4 -0
  33. data/templates/repmgr.conf.erb +10 -3
  34. data/templates/repmgr_dns_failover.sh.erb +59 -0
  35. metadata +6 -1
@@ -15,7 +15,7 @@ module ActivePostgres
15
15
  return nil unless secret_value
16
16
 
17
17
  resolved = resolve_secret_value(secret_value)
18
- @cache[secret_key] = resolved
18
+ @cache[secret_key] = resolved unless resolved.nil?
19
19
  resolved
20
20
  end
21
21
 
@@ -25,6 +25,21 @@ module ActivePostgres
25
25
  end
26
26
  end
27
27
 
28
+ def resolve_value(value)
29
+ case value
30
+ when Hash
31
+ value.each_with_object({}) do |(k, v), result|
32
+ result[k] = resolve_value(v)
33
+ end
34
+ when Array
35
+ value.map { |v| resolve_value(v) }
36
+ when String
37
+ resolve_secret_value(value)
38
+ else
39
+ value
40
+ end
41
+ end
42
+
28
43
  def cache_to_files(directory = '.secrets')
29
44
  require 'fileutils'
30
45
 
@@ -45,9 +60,10 @@ module ActivePostgres
45
60
 
46
61
  def resolve_secret_value(value)
47
62
  case value
48
- when /^rails_credentials:(.+)$/
63
+ when /^(rails_credentials|credentials):(.+)$/
49
64
  # Rails credentials: rails_credentials:postgres.superuser_password
50
- key_path = ::Regexp.last_match(1)
65
+ # Alias: credentials:postgres.superuser_password
66
+ key_path = ::Regexp.last_match(2).to_s.strip
51
67
  fetch_from_rails_credentials(key_path)
52
68
  when /^\$\((.+)\)$/
53
69
  # Command execution: $(op read "op://...")
@@ -65,10 +81,12 @@ module ActivePostgres
65
81
  end
66
82
 
67
83
  def fetch_from_rails_credentials(key_path)
68
- return nil unless Credentials.available?
84
+ return nil unless defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
69
85
 
70
86
  keys = key_path.split('.').map(&:to_sym)
71
- Rails.application.credentials.dig(*keys)
87
+ ::Rails.application.credentials.dig(*keys)
88
+ rescue StandardError
89
+ nil
72
90
  end
73
91
 
74
92
  def execute_command(command)
@@ -22,6 +22,10 @@ module ActivePostgres
22
22
  on("#{config.user}@#{host}", &)
23
23
  end
24
24
 
25
+ def execute_on_host_as(host, user, &)
26
+ on("#{user}@#{host}", &)
27
+ end
28
+
25
29
  def execute_on_primary(&)
26
30
  execute_on_host(config.primary_host, &)
27
31
  end
@@ -94,7 +98,7 @@ module ActivePostgres
94
98
 
95
99
  info 'Configuring PostgreSQL apt repository...'
96
100
  pgdg_repo = "'echo \"deb [signed-by=/usr/share/keyrings/postgresql-archive-keyring.gpg] " \
97
- 'http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > ' \
101
+ 'https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > ' \
98
102
  "/etc/apt/sources.list.d/pgdg.list'"
99
103
  execute :sudo, 'sh', '-c', pgdg_repo
100
104
 
@@ -200,24 +204,42 @@ module ActivePostgres
200
204
  result
201
205
  end
202
206
 
203
- def run_sql(host, sql)
207
+ def run_sql(host, sql, postgres_user: config.postgres_user, port: nil, database: nil, tuples_only: true,
208
+ capture: true)
204
209
  result = nil
205
- postgres_user = config.postgres_user
210
+ executor = self
206
211
  execute_on_host(host) do
207
- # Use a temporary file to avoid shell escaping issues with special characters
208
- temp_file = "/tmp/query_#{SecureRandom.hex(8)}.sql"
209
- upload! StringIO.new(sql), temp_file
210
- execute :chmod, '644', temp_file
211
-
212
- begin
213
- result = capture(:sudo, '-u', postgres_user, 'psql', '-t', '-f', temp_file)
214
- ensure
215
- execute :rm, '-f', temp_file
216
- end
212
+ backend = self
213
+ result = executor.run_sql_on_backend(backend, sql,
214
+ postgres_user: postgres_user,
215
+ port: port,
216
+ database: database,
217
+ tuples_only: tuples_only,
218
+ capture: capture)
217
219
  end
218
220
  result
219
221
  end
220
222
 
223
+ def run_sql_on_backend(backend, sql, postgres_user: config.postgres_user, port: nil, database: nil,
224
+ tuples_only: true, capture: true)
225
+ # Use a temporary file to avoid shell escaping issues with special characters
226
+ temp_file = "/tmp/active_postgres_#{SecureRandom.hex(8)}.sql"
227
+ backend.upload! StringIO.new(sql), temp_file
228
+ backend.execute :chmod, '600', temp_file
229
+ backend.execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", temp_file
230
+
231
+ cmd = [:sudo, '-u', postgres_user, 'psql']
232
+ cmd << '-t' if tuples_only
233
+ cmd += ['-p', port.to_s] if port
234
+ cmd += ['-d', database.to_s] if database
235
+ cmd += ['-f', temp_file]
236
+
237
+ result = capture ? backend.capture(*cmd) : backend.execute(*cmd)
238
+ result
239
+ ensure
240
+ backend.execute :sudo, 'rm', '-f', temp_file
241
+ end
242
+
221
243
  def ensure_cluster_exists(host, version)
222
244
  execute_on_host(host) do
223
245
  data_dir = "/var/lib/postgresql/#{version}/main"
@@ -272,18 +294,21 @@ module ActivePostgres
272
294
  SSHKit.config.format = :pretty
273
295
  end
274
296
 
275
- return unless File.exist?(config.ssh_key)
276
-
277
297
  SSHKit::Backend::Netssh.configure do |ssh|
278
- ssh.ssh_options = {
279
- keys: [config.ssh_key],
280
- keys_only: true,
298
+ options = {
281
299
  forward_agent: false,
282
300
  auth_methods: ['publickey'],
283
- verify_host_key: :accept_new,
301
+ verify_host_key: config.ssh_host_key_verification || :always,
284
302
  timeout: 10,
285
303
  number_of_password_prompts: 0
286
304
  }
305
+
306
+ if config.ssh_key && File.exist?(config.ssh_key)
307
+ options[:keys] = [config.ssh_key]
308
+ options[:keys_only] = true
309
+ end
310
+
311
+ ssh.ssh_options = options
287
312
  end
288
313
  end
289
314
  end
@@ -1,3 +1,3 @@
1
1
  module ActivePostgres
2
- VERSION = '0.8.0'.freeze
2
+ VERSION = '0.9.1'.freeze
3
3
  end
@@ -23,6 +23,7 @@ require_relative 'active_postgres/installer'
23
23
  require_relative 'active_postgres/ssh_executor'
24
24
  require_relative 'active_postgres/direct_executor'
25
25
  require_relative 'active_postgres/health_checker'
26
+ require_relative 'active_postgres/overview'
26
27
  require_relative 'active_postgres/failover'
27
28
  require_relative 'active_postgres/performance_tuner'
28
29
  require_relative 'active_postgres/connection_pooler'
@@ -150,6 +150,15 @@ namespace :postgres do
150
150
  health_checker.show_status
151
151
  end
152
152
 
153
+ desc 'Show control tower overview'
154
+ task overview: :environment do
155
+ require 'active_postgres'
156
+
157
+ config = ActivePostgres::Configuration.load
158
+ overview = ActivePostgres::Overview.new(config)
159
+ overview.show
160
+ end
161
+
153
162
  desc 'Visualize cluster nodes and topology'
154
163
  task nodes: :environment do
155
164
  require 'active_postgres'
@@ -270,6 +279,50 @@ namespace :postgres do
270
279
  puts "\n#{'=' * 80}\n"
271
280
  end
272
281
 
282
+ desc 'Show help for PostgreSQL rake tasks'
283
+ task help: :environment do
284
+ puts "\nPostgreSQL Rake Tasks"
285
+ puts '=' * 70
286
+ puts "\nTip: set RAILS_ENV=production for production targets"
287
+
288
+ puts "\nSetup & Maintenance"
289
+ puts " rake postgres:setup # Deploy HA cluster (CLEAN=true for fresh install)"
290
+ puts " rake postgres:purge # Destroy cluster (DESTRUCTIVE)"
291
+ puts " rake postgres:setup:core # PostgreSQL config (postgresql.conf, pg_hba.conf)"
292
+ puts " rake postgres:setup:repmgr # HA + failover (repmgr)"
293
+ puts " rake postgres:setup:pgbouncer # PgBouncer pooling"
294
+ puts " rake postgres:setup:pgbackrest # Backups (pgBackRest)"
295
+ puts " rake postgres:setup:monitoring # postgres_exporter"
296
+ puts " rake postgres:setup:ssl # SSL certs"
297
+
298
+ puts "\nStatus & Health"
299
+ puts " rake postgres:status # Cluster status (SSH by default)"
300
+ puts " rake postgres:overview # Control tower overview"
301
+ puts " rake postgres:nodes # Topology view"
302
+ puts " rake postgres:verify # Comprehensive checklist"
303
+ puts " ACTIVE_POSTGRES_STATUS_MODE=ssh|direct rake postgres:status"
304
+
305
+ puts "\nBackups"
306
+ puts " rake postgres:backup:full # Full backup"
307
+ puts " rake postgres:backup:incremental # Incremental backup"
308
+ puts " rake postgres:backup:list # List backups"
309
+ puts " rake postgres:backup:restore[ID] # Restore backup set"
310
+ puts " rake postgres:backup:restore_at[\"YYYY-MM-DD HH:MM:SS\",promote] # PITR"
311
+
312
+ puts "\nMigrations"
313
+ puts " rake postgres:migrate # Run migrations on primary only"
314
+
315
+ puts "\nPgBouncer"
316
+ puts " rake postgres:pgbouncer:update_userlist[users] # Update userlist"
317
+ puts " rake postgres:pgbouncer:stats # Service status + stats"
318
+
319
+ puts "\nTests"
320
+ puts " rake postgres:test:replication[rows] # Replication stress test"
321
+ puts " rake postgres:test:pgbouncer[connections] # PgBouncer load test"
322
+
323
+ puts
324
+ end
325
+
273
326
  desc 'Promote standby to primary'
274
327
  task :promote, [:host] => :environment do |_t, args|
275
328
  require 'active_postgres'
@@ -325,6 +378,20 @@ namespace :postgres do
325
378
  installer.run_restore(args[:backup_id])
326
379
  end
327
380
 
381
+ desc 'Restore to a point in time (PITR)'
382
+ task :restore_at, [:target_time, :target_action] => :environment do |_t, args|
383
+ require 'active_postgres'
384
+
385
+ unless args[:target_time]
386
+ puts 'Usage: rake postgres:backup:restore_at["2026-01-29 01:15:00",promote]'
387
+ exit 1
388
+ end
389
+
390
+ config = ActivePostgres::Configuration.load
391
+ installer = ActivePostgres::Installer.new(config)
392
+ installer.run_restore_at(args[:target_time], target_action: args[:target_action] || 'promote')
393
+ end
394
+
328
395
  desc 'List available backups'
329
396
  task list: :environment do
330
397
  require 'active_postgres'
@@ -372,6 +439,15 @@ namespace :postgres do
372
439
  installer.setup_component('monitoring')
373
440
  end
374
441
 
442
+ desc 'Setup only pgBackRest backups'
443
+ task pgbackrest: :environment do
444
+ require 'active_postgres'
445
+
446
+ config = ActivePostgres::Configuration.load
447
+ installer = ActivePostgres::Installer.new(config)
448
+ installer.setup_component('pgbackrest')
449
+ end
450
+
375
451
  desc 'Setup only repmgr'
376
452
  task repmgr: :environment do
377
453
  require 'active_postgres'
@@ -412,10 +488,7 @@ namespace :postgres do
412
488
  WHERE rolname = '#{user}'
413
489
  SQL
414
490
 
415
- upload! StringIO.new(sql), '/tmp/get_user_hash.sql'
416
- execute :chmod, '644', '/tmp/get_user_hash.sql'
417
- user_hash = capture(:sudo, '-u', postgres_user, 'psql', '-t', '-f', '/tmp/get_user_hash.sql').strip
418
- execute :rm, '-f', '/tmp/get_user_hash.sql'
491
+ user_hash = ssh_executor.run_sql_on_backend(self, sql, postgres_user: postgres_user).to_s.strip
419
492
 
420
493
  if user_hash && !user_hash.empty?
421
494
  userlist_entries << user_hash
@@ -26,10 +26,7 @@ namespace :postgres do
26
26
  escaped_password = new_password.gsub("'", "''")
27
27
 
28
28
  sql = "ALTER USER #{app_user} WITH PASSWORD '#{escaped_password}';"
29
- upload! StringIO.new(sql), '/tmp/rotate_password.sql'
30
- execute :chmod, '644', '/tmp/rotate_password.sql'
31
- execute :sudo, '-u', 'postgres', 'psql', '-f', '/tmp/rotate_password.sql'
32
- execute :rm, '-f', '/tmp/rotate_password.sql'
29
+ ssh_executor.run_sql_on_backend(self, sql, postgres_user: 'postgres', tuples_only: false, capture: false)
33
30
 
34
31
  puts "✓ Updated PostgreSQL password for #{app_user}"
35
32
  end
@@ -48,10 +45,7 @@ namespace :postgres do
48
45
  WHERE rolname = '#{user}'
49
46
  SQL
50
47
 
51
- upload! StringIO.new(sql), '/tmp/get_user_hash.sql'
52
- execute :chmod, '644', '/tmp/get_user_hash.sql'
53
- user_hash = capture(:sudo, '-u', postgres_user, 'psql', '-t', '-f', '/tmp/get_user_hash.sql').strip
54
- execute :rm, '-f', '/tmp/get_user_hash.sql'
48
+ user_hash = ssh_executor.run_sql_on_backend(self, sql, postgres_user: postgres_user).to_s.strip
55
49
 
56
50
  userlist_entries << user_hash if user_hash && !user_hash.empty?
57
51
  end
@@ -128,10 +122,7 @@ namespace :postgres do
128
122
  escaped_password = new_password.gsub("'", "''")
129
123
 
130
124
  sql = "ALTER USER #{username} WITH PASSWORD '#{escaped_password}';"
131
- upload! StringIO.new(sql), '/tmp/rotate_password.sql'
132
- execute :chmod, '644', '/tmp/rotate_password.sql'
133
- execute :sudo, '-u', 'postgres', 'psql', '-f', '/tmp/rotate_password.sql'
134
- execute :rm, '-f', '/tmp/rotate_password.sql'
125
+ ssh_executor.run_sql_on_backend(self, sql, postgres_user: 'postgres', tuples_only: false, capture: false)
135
126
  end
136
127
 
137
128
  puts "✓ Updated #{username}"
@@ -152,10 +143,7 @@ namespace :postgres do
152
143
  WHERE rolname = '#{user}'
153
144
  SQL
154
145
 
155
- upload! StringIO.new(sql), '/tmp/get_user_hash.sql'
156
- execute :chmod, '644', '/tmp/get_user_hash.sql'
157
- user_hash = capture(:sudo, '-u', postgres_user, 'psql', '-t', '-f', '/tmp/get_user_hash.sql').strip
158
- execute :rm, '-f', '/tmp/get_user_hash.sql'
146
+ user_hash = ssh_executor.run_sql_on_backend(self, sql, postgres_user: postgres_user).to_s.strip
159
147
 
160
148
  userlist_entries << user_hash if user_hash && !user_hash.empty?
161
149
  end
@@ -39,9 +39,12 @@
39
39
  repmgr_network = pg_hba_rules.find { |r| r[:type] == 'host' && r[:address] && !r[:address].start_with?('127.0.0.1') }&.dig(:address) || '10.8.0.0/24'
40
40
  repmgr_user = config.repmgr_user
41
41
  repmgr_db = config.repmgr_database
42
+ replication_user = config.replication_user
42
43
  %>
43
44
  host replication <%= repmgr_user %> <%= repmgr_network %> scram-sha-256
45
+ <% if replication_user && replication_user != repmgr_user %>
46
+ host replication <%= replication_user %> <%= repmgr_network %> scram-sha-256
47
+ <% end %>
44
48
  host <%= repmgr_db %> <%= repmgr_user %> <%= repmgr_network %> scram-sha-256
45
49
  <% end %>
46
50
 
47
-
@@ -16,6 +16,21 @@ repo1-s3-endpoint=s3.<%= pgbackrest_config[:s3_region] || 'us-east-1' %>.amazona
16
16
  repo1-s3-key=<%= secrets_obj.resolve('s3_access_key') %>
17
17
  repo1-s3-key-secret=<%= secrets_obj.resolve('s3_secret_key') %>
18
18
  <% end %>
19
+ <% elsif pgbackrest_config[:repo_type] == 'gcs' %>
20
+ repo1-type=gcs
21
+ repo1-path=<%= pgbackrest_config[:repo_path] || '/backups' %>
22
+ repo1-gcs-bucket=<%= pgbackrest_config[:gcs_bucket] %>
23
+ <% if secrets_obj.resolve('gcs_key_file') %>
24
+ repo1-gcs-key=<%= secrets_obj.resolve('gcs_key_file') %>
25
+ <% end %>
26
+ <% elsif pgbackrest_config[:repo_type] == 'azure' %>
27
+ repo1-type=azure
28
+ repo1-path=<%= pgbackrest_config[:repo_path] || '/backups' %>
29
+ repo1-azure-container=<%= pgbackrest_config[:azure_container] %>
30
+ <% if secrets_obj.resolve('azure_account') %>
31
+ repo1-azure-account=<%= secrets_obj.resolve('azure_account') %>
32
+ repo1-azure-key=<%= secrets_obj.resolve('azure_key') %>
33
+ <% end %>
19
34
  <% else %>
20
35
  # Local storage
21
36
  repo1-path=<%= pgbackrest_config[:repo_path] || '/var/lib/pgbackrest' %>
@@ -35,9 +50,22 @@ repo1-cipher-type=aes-256-cbc
35
50
  repo1-cipher-pass=<%= secrets_obj.resolve('backup_encryption_key') %>
36
51
  <% end %>
37
52
 
53
+ # Logging
54
+ log-level-console=info
55
+ log-level-file=detail
56
+ log-path=/var/log/pgbackrest
57
+
58
+ # Process settings
59
+ process-max=<%= pgbackrest_config[:process_max] || 2 %>
60
+
61
+ # Start/Stop (for consistent backups)
62
+ start-fast=y
63
+ stop-auto=y
64
+
38
65
  [main]
39
66
  pg1-path=/var/lib/postgresql/<%= config.version %>/main
40
67
  pg1-port=5432
41
68
  pg1-socket-path=/var/run/postgresql
69
+ pg1-user=<%= config.postgres_user %>
42
70
 
43
71
 
@@ -0,0 +1,8 @@
1
+ [Unit]
2
+ Description=Update PgBouncer target to current PostgreSQL primary
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=oneshot
8
+ ExecStart=/usr/local/bin/pgbouncer-follow-primary
@@ -0,0 +1,11 @@
1
+ [Unit]
2
+ Description=Run pgbouncer-follow-primary periodically
3
+
4
+ [Timer]
5
+ OnBootSec=10s
6
+ OnUnitActiveSec=<%= interval %>s
7
+ AccuracySec=1s
8
+ Unit=pgbouncer-follow-primary.service
9
+
10
+ [Install]
11
+ WantedBy=timers.target
@@ -49,8 +49,10 @@ log_pooler_errors = <%= pgbouncer_config[:log_pooler_errors] || 1 %>
49
49
  client_tls_sslmode = require
50
50
  client_tls_key_file = /etc/pgbouncer/server.key
51
51
  client_tls_cert_file = /etc/pgbouncer/server.crt
52
+ <% if has_ca_cert %>
52
53
  client_tls_ca_file = /etc/pgbouncer/ca.crt
53
54
  <% end %>
55
+ <% end %>
54
56
 
55
57
  # Process management
56
58
  <% if pgbouncer_config[:pidfile] %>
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ REPMGR_CONF="<%= repmgr_conf %>"
5
+ PGBOUNCER_INI="/etc/pgbouncer/pgbouncer.ini"
6
+ PGBOUNCER_SERVICE="pgbouncer"
7
+ LOG_TAG="pgbouncer-follow-primary"
8
+
9
+ cluster_csv=$(/usr/bin/sudo -u <%= postgres_user %> env HOME=/var/lib/postgresql \
10
+ repmgr -f "$REPMGR_CONF" cluster show --csv 2>/dev/null || true)
11
+ if [[ -z "$cluster_csv" ]]; then
12
+ exit 0
13
+ fi
14
+
15
+ primary_conninfo=$(printf "%s\n" "$cluster_csv" | awk -F',' 'NR>1 && tolower($3) ~ /primary/ {print $NF; exit}')
16
+ if [[ -z "$primary_conninfo" ]]; then
17
+ exit 0
18
+ fi
19
+
20
+ primary_host=$(printf "%s" "$primary_conninfo" | sed -n 's/.*host=\\([^ ]*\\).*/\\1/p')
21
+ if [[ -z "$primary_host" ]]; then
22
+ exit 0
23
+ fi
24
+
25
+ current_host=$(awk -F'host=' '/^\\* = host=/{print $2}' "$PGBOUNCER_INI" | awk '{print $1}')
26
+ if [[ -z "$current_host" ]]; then
27
+ exit 0
28
+ fi
29
+
30
+ if [[ "$current_host" != "$primary_host" ]]; then
31
+ /usr/bin/sed -i -E "s/^(\\* = host=)[^ ]+/\\1${primary_host}/" "$PGBOUNCER_INI"
32
+ /usr/bin/systemctl reload "$PGBOUNCER_SERVICE"
33
+ /usr/bin/logger -t "$LOG_TAG" "Updated PgBouncer primary target to ${primary_host}"
34
+ fi
@@ -44,9 +44,13 @@ wal_compression = <%= pg_config[:wal_compression] %>
44
44
  <% end %>
45
45
  <% if pg_config[:archive_mode] %>
46
46
  archive_mode = <%= pg_config[:archive_mode] %>
47
+ <% elsif config.component_enabled?(:pgbackrest) %>
48
+ archive_mode = on
47
49
  <% end %>
48
50
  <% if pg_config[:archive_command] %>
49
51
  archive_command = '<%= pg_config[:archive_command] %>'
52
+ <% elsif config.component_enabled?(:pgbackrest) %>
53
+ archive_command = 'pgbackrest --stanza=main archive-push %p'
50
54
  <% end %>
51
55
  <% if pg_config[:shared_memory_type] %>
52
56
  shared_memory_type = <%= pg_config[:shared_memory_type] %>
@@ -15,14 +15,14 @@ node_name='<%= node_label %>'
15
15
  # conninfo describes how OTHER nodes connect to THIS node
16
16
  repmgr_host = config.replication_host_for(host)
17
17
  %>
18
- conninfo='host=<%= repmgr_host %> user=<%= config.repmgr_user %> dbname=<%= config.repmgr_database %> password=<%= secrets_obj.resolve('repmgr_password') %> connect_timeout=2'
18
+ conninfo='host=<%= repmgr_host %> user=<%= config.repmgr_user %> dbname=<%= config.repmgr_database %> connect_timeout=2'
19
19
  data_directory='/var/lib/postgresql/<%= config.version %>/main'
20
20
 
21
21
  <% if host == config.primary_host %>
22
- failover=automatic
22
+ failover=<%= repmgr_config[:auto_failover] == false ? 'manual' : 'automatic' %>
23
23
  priority=<%= repmgr_config[:priority] || 100 %>
24
24
  <% else %>
25
- failover=automatic
25
+ failover=<%= repmgr_config[:auto_failover] == false ? 'manual' : 'automatic' %>
26
26
  priority=<%= repmgr_config[:priority] || 100 %>
27
27
  promote_command='repmgr standby promote -f /etc/repmgr.conf'
28
28
  follow_command='repmgr standby follow -f /etc/repmgr.conf --upstream-node-id=%n'
@@ -30,6 +30,9 @@ follow_command='repmgr standby follow -f /etc/repmgr.conf --upstream-node-id=%n'
30
30
 
31
31
  reconnect_attempts=<%= repmgr_config[:reconnect_attempts] || 6 %>
32
32
  reconnect_interval=<%= repmgr_config[:reconnect_interval] || 10 %>
33
+ <% if repmgr_config.key?(:use_rewind) %>
34
+ use_rewind=<%= repmgr_config[:use_rewind] ? 'yes' : 'no' %>
35
+ <% end %>
33
36
 
34
37
  log_level=INFO
35
38
  log_facility=STDERR
@@ -38,3 +41,7 @@ log_file='/var/log/postgresql/repmgr.log'
38
41
  monitoring_history=yes
39
42
  monitor_interval_secs=5
40
43
 
44
+ <% if repmgr_config[:dns_failover] && repmgr_config[:dns_failover][:enabled] %>
45
+ event_notification_command='/usr/local/bin/active-postgres-dns-failover'
46
+ event_notifications='<%= repmgr_config[:dns_failover][:events] || 'repmgrd_failover_promote,standby_promote,standby_switchover,standby_follow' %>'
47
+ <% end %>
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ REPMGR_CONF="/etc/repmgr.conf"
5
+ DNS_USER="<%= dns_user %>"
6
+ DNS_SERVERS=(<%= dns_servers.map(&:to_s).join(' ') %>)
7
+ DNS_SSH_KEY="<%= dns_ssh_key_path %>"
8
+ DNSMASQ_FILE="/etc/dnsmasq.d/active_postgres.conf"
9
+ PRIMARY_RECORDS=(<%= primary_records.map(&:to_s).join(' ') %>)
10
+ REPLICA_RECORDS=(<%= replica_records.map(&:to_s).join(' ') %>)
11
+ SSH_STRICT_HOST_KEY="<%= ssh_strict_host_key %>"
12
+ SSH_KNOWN_HOSTS="/var/lib/postgresql/.ssh/known_hosts"
13
+ LOG_TAG="active_postgres_dns"
14
+
15
+ cluster_csv=$(repmgr -f "$REPMGR_CONF" cluster show --csv 2>/dev/null || true)
16
+ if [[ -z "$cluster_csv" ]]; then
17
+ exit 0
18
+ fi
19
+
20
+ primary_host=$(printf "%s\n" "$cluster_csv" | awk -F',' 'NR>1 && tolower($3) ~ /primary/ {print $NF; exit}' | sed -n 's/.*host=\\([^ ]*\\).*/\\1/p')
21
+ standby_hosts=$(printf "%s\n" "$cluster_csv" | awk -F',' 'NR>1 && tolower($3) ~ /standby/ {print $NF}' | sed -n 's/.*host=\\([^ ]*\\).*/\\1/p' | sort -u)
22
+
23
+ if [[ -z "$primary_host" ]]; then
24
+ exit 0
25
+ fi
26
+
27
+ content=$'# Managed by active_postgres\n'
28
+ for record in "${PRIMARY_RECORDS[@]}"; do
29
+ if [[ -n "$record" ]]; then
30
+ printf -v content '%saddress=/%s/%s\n' "$content" "$record" "$primary_host"
31
+ fi
32
+ done
33
+
34
+ if [[ -n "$standby_hosts" ]]; then
35
+ for record in "${REPLICA_RECORDS[@]}"; do
36
+ if [[ -z "$record" ]]; then
37
+ continue
38
+ fi
39
+
40
+ for host in $standby_hosts; do
41
+ printf -v content '%saddress=/%s/%s\n' "$content" "$record" "$host"
42
+ done
43
+ done
44
+ fi
45
+
46
+ ssh_opts=(-i "$DNS_SSH_KEY" -o BatchMode=yes -o StrictHostKeyChecking="$SSH_STRICT_HOST_KEY" -o UserKnownHostsFile="$SSH_KNOWN_HOSTS")
47
+
48
+ for server in "${DNS_SERVERS[@]}"; do
49
+ if [[ -z "$server" ]]; then
50
+ continue
51
+ fi
52
+
53
+ if ! /usr/bin/ssh "${ssh_opts[@]}" "${DNS_USER}@${server}" \
54
+ "sudo bash -c 'cat > ${DNSMASQ_FILE} << \"EOF\"\\n${content}EOF\\n' && (sudo systemctl reload dnsmasq || sudo systemctl restart dnsmasq)"; then
55
+ /usr/bin/logger -t "$LOG_TAG" "Failed updating dnsmasq on ${server}"
56
+ fi
57
+ done
58
+
59
+ /usr/bin/logger -t "$LOG_TAG" "Updated DNS records for ${PRIMARY_RECORDS[*]} and ${REPLICA_RECORDS[*]} (primary=${primary_host})"
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.8.0
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - BoringCache
@@ -179,6 +179,7 @@ files:
179
179
  - lib/active_postgres/installer.rb
180
180
  - lib/active_postgres/log_sanitizer.rb
181
181
  - lib/active_postgres/logger.rb
182
+ - lib/active_postgres/overview.rb
182
183
  - lib/active_postgres/performance_tuner.rb
183
184
  - lib/active_postgres/rails/database_config.rb
184
185
  - lib/active_postgres/rails/migration_guard.rb
@@ -195,9 +196,13 @@ files:
195
196
  - lib/tasks/rotate_credentials.rake
196
197
  - templates/pg_hba.conf.erb
197
198
  - templates/pgbackrest.conf.erb
199
+ - templates/pgbouncer-follow-primary.service.erb
200
+ - templates/pgbouncer-follow-primary.timer.erb
198
201
  - templates/pgbouncer.ini.erb
202
+ - templates/pgbouncer_follow_primary.sh.erb
199
203
  - templates/postgresql.conf.erb
200
204
  - templates/repmgr.conf.erb
205
+ - templates/repmgr_dns_failover.sh.erb
201
206
  homepage: https://github.com/boringcache/active_postgres
202
207
  licenses:
203
208
  - MIT