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.
- checksums.yaml +4 -4
- data/README.md +142 -5
- data/lib/active_postgres/cli.rb +7 -0
- data/lib/active_postgres/components/core.rb +2 -7
- data/lib/active_postgres/components/extensions.rb +40 -35
- data/lib/active_postgres/components/monitoring.rb +256 -4
- data/lib/active_postgres/components/pgbackrest.rb +91 -5
- data/lib/active_postgres/components/pgbouncer.rb +58 -7
- data/lib/active_postgres/components/repmgr.rb +448 -62
- data/lib/active_postgres/configuration.rb +66 -1
- data/lib/active_postgres/connection_pooler.rb +3 -12
- data/lib/active_postgres/credentials.rb +3 -3
- data/lib/active_postgres/direct_executor.rb +1 -2
- data/lib/active_postgres/generators/active_postgres/install_generator.rb +2 -0
- data/lib/active_postgres/generators/active_postgres/templates/postgres.yml.erb +28 -1
- data/lib/active_postgres/health_checker.rb +29 -7
- data/lib/active_postgres/installer.rb +9 -0
- data/lib/active_postgres/overview.rb +351 -0
- data/lib/active_postgres/rollback_manager.rb +4 -8
- data/lib/active_postgres/secrets.rb +23 -5
- data/lib/active_postgres/ssh_executor.rb +44 -19
- data/lib/active_postgres/version.rb +1 -1
- data/lib/active_postgres.rb +1 -0
- data/lib/tasks/postgres.rake +77 -4
- data/lib/tasks/rotate_credentials.rake +4 -16
- data/templates/pg_hba.conf.erb +4 -1
- data/templates/pgbackrest.conf.erb +28 -0
- data/templates/pgbouncer-follow-primary.service.erb +8 -0
- data/templates/pgbouncer-follow-primary.timer.erb +11 -0
- data/templates/pgbouncer.ini.erb +2 -0
- data/templates/pgbouncer_follow_primary.sh.erb +34 -0
- data/templates/postgresql.conf.erb +4 -0
- data/templates/repmgr.conf.erb +10 -3
- data/templates/repmgr_dns_failover.sh.erb +59 -0
- 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
|
-
|
|
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
|
|
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
|
-
'
|
|
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
|
-
|
|
210
|
+
executor = self
|
|
206
211
|
execute_on_host(host) do
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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: :
|
|
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
|
data/lib/active_postgres.rb
CHANGED
|
@@ -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'
|
data/lib/tasks/postgres.rake
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/templates/pg_hba.conf.erb
CHANGED
|
@@ -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
|
|
data/templates/pgbouncer.ini.erb
CHANGED
|
@@ -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] %>
|
data/templates/repmgr.conf.erb
CHANGED
|
@@ -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 %>
|
|
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
|
|
22
|
+
failover=<%= repmgr_config[:auto_failover] == false ? 'manual' : 'automatic' %>
|
|
23
23
|
priority=<%= repmgr_config[:priority] || 100 %>
|
|
24
24
|
<% else %>
|
|
25
|
-
failover
|
|
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.
|
|
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
|