active_postgres 0.8.0 → 0.9.0

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.
@@ -2,7 +2,8 @@ require 'yaml'
2
2
 
3
3
  module ActivePostgres
4
4
  class Configuration
5
- attr_reader :environment, :version, :user, :ssh_key, :primary, :standbys, :components, :secrets_config, :database_config
5
+ attr_reader :environment, :version, :user, :ssh_key, :ssh_host_key_verification, :primary, :standbys, :components, :secrets_config,
6
+ :database_config
6
7
 
7
8
  def initialize(config_hash, environment = 'development')
8
9
  @environment = environment
@@ -13,6 +14,9 @@ module ActivePostgres
13
14
  @version = env_config['version'] || 18
14
15
  @user = env_config['user'] || 'ubuntu'
15
16
  @ssh_key = File.expand_path(env_config['ssh_key'] || '~/.ssh/id_rsa')
17
+ @ssh_host_key_verification = normalize_ssh_host_key_verification(
18
+ env_config['ssh_host_key_verification'] || env_config['ssh_verify_host_key']
19
+ )
16
20
 
17
21
  @primary = env_config['primary'] || {}
18
22
  @standbys = env_config['standby'] || []
@@ -91,6 +95,29 @@ module ActivePostgres
91
95
 
92
96
  # Validate required secrets if components are enabled
93
97
  raise Error, 'Missing replication_password secret' if component_enabled?(:repmgr) && !secrets_config['replication_password']
98
+ raise Error, 'Missing monitoring_password secret' if component_enabled?(:monitoring) && !secrets_config['monitoring_password']
99
+
100
+ if component_enabled?(:repmgr)
101
+ dns_failover = component_config(:repmgr)[:dns_failover]
102
+ if dns_failover && dns_failover[:enabled]
103
+ domain = dns_failover[:domain].to_s.strip
104
+ servers = Array(dns_failover[:dns_servers])
105
+ provider = (dns_failover[:provider] || 'dnsmasq').to_s.strip
106
+
107
+ raise Error, 'dns_failover.domain is required when enabled' if domain.empty?
108
+ raise Error, 'dns_failover.dns_servers is required when enabled' if servers.empty?
109
+ raise Error, "Unsupported dns_failover provider '#{provider}'" unless provider == 'dnsmasq'
110
+
111
+ servers.each do |server|
112
+ next unless server.is_a?(Hash)
113
+
114
+ ssh_host = server['ssh_host'] || server[:ssh_host] || server['host'] || server[:host]
115
+ private_ip = server['private_ip'] || server[:private_ip] || server['ip'] || server[:ip]
116
+ raise Error, 'dns_failover.dns_servers entries must include host/ssh_host or private_ip' if
117
+ (ssh_host.nil? || ssh_host.to_s.strip.empty?) && (private_ip.nil? || private_ip.to_s.strip.empty?)
118
+ end
119
+ end
120
+ end
94
121
 
95
122
  true
96
123
  end
@@ -108,6 +135,10 @@ module ActivePostgres
108
135
  component_config(:repmgr)[:database] || 'repmgr'
109
136
  end
110
137
 
138
+ def replication_user
139
+ component_config(:repmgr)[:replication_user] || 'replication'
140
+ end
141
+
111
142
  def pgbouncer_user
112
143
  component_config(:pgbouncer)[:user] || 'pgbouncer'
113
144
  end
@@ -195,5 +226,22 @@ module ActivePostgres
195
226
  value
196
227
  end
197
228
  end
229
+
230
+ def normalize_ssh_host_key_verification(value)
231
+ return :always if value.nil?
232
+
233
+ normalized = case value
234
+ when Symbol
235
+ value
236
+ else
237
+ value.to_s.strip.downcase.tr('-', '_').to_sym
238
+ end
239
+
240
+ return :always if normalized == :always
241
+ return :accept_new if %i[accept_new acceptnew new].include?(normalized)
242
+
243
+ raise Error,
244
+ "Invalid ssh_host_key_verification '#{value}'. Use 'always' or 'accept_new'."
245
+ end
198
246
  end
199
247
  end
@@ -321,10 +321,7 @@ module ActivePostgres
321
321
  WHERE passwd IS NOT NULL
322
322
  SQL
323
323
 
324
- upload! StringIO.new(sql), '/tmp/get_all_users.sql'
325
- execute :chmod, '644', '/tmp/get_all_users.sql'
326
- userlist = capture(:sudo, '-u', postgres_user, 'psql', '-t', '-f', '/tmp/get_all_users.sql').strip
327
- execute :rm, '-f', '/tmp/get_all_users.sql'
324
+ userlist = @ssh_executor.run_sql_on_backend(self, sql, postgres_user: postgres_user).to_s.strip
328
325
 
329
326
  unless userlist.include?(pgbouncer_user)
330
327
  pgbouncer_pass = SecureRandom.hex(16)
@@ -335,10 +332,7 @@ module ActivePostgres
335
332
  "GRANT CONNECT ON DATABASE postgres TO #{pgbouncer_user};"
336
333
  ].join("\n")
337
334
 
338
- upload! StringIO.new(sql), '/tmp/create_pgbouncer_user.sql'
339
- execute :chmod, '644', '/tmp/create_pgbouncer_user.sql'
340
- execute :sudo, '-u', postgres_user, 'psql', '-f', '/tmp/create_pgbouncer_user.sql'
341
- execute :rm, '-f', '/tmp/create_pgbouncer_user.sql'
335
+ @ssh_executor.run_sql_on_backend(self, sql, postgres_user: postgres_user, tuples_only: false, capture: false)
342
336
 
343
337
  sql = <<~SQL.strip
344
338
  SELECT passwd
@@ -346,10 +340,7 @@ module ActivePostgres
346
340
  WHERE usename = '#{pgbouncer_user}'
347
341
  SQL
348
342
 
349
- upload! StringIO.new(sql), '/tmp/get_pgbouncer_pass.sql'
350
- execute :chmod, '644', '/tmp/get_pgbouncer_pass.sql'
351
- encrypted = capture(:sudo, '-u', postgres_user, 'psql', '-t', '-f', '/tmp/get_pgbouncer_pass.sql').strip
352
- execute :rm, '-f', '/tmp/get_pgbouncer_pass.sql'
343
+ encrypted = @ssh_executor.run_sql_on_backend(self, sql, postgres_user: postgres_user).to_s.strip
353
344
 
354
345
  userlist += "\n\"#{pgbouncer_user}\" \"#{encrypted}\""
355
346
  end
@@ -2,8 +2,8 @@ module ActivePostgres
2
2
  class Credentials
3
3
  def self.get(key_path)
4
4
  # Try to get from Rails credentials if Rails is available
5
- if defined?(Rails) && Rails.respond_to?(:application) && Rails.application
6
- value = Rails.application.credentials.dig(*key_path.split('.').map(&:to_sym))
5
+ if defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
6
+ value = ::Rails.application.credentials.dig(*key_path.split('.').map(&:to_sym))
7
7
  return value if value
8
8
  end
9
9
 
@@ -11,7 +11,7 @@ module ActivePostgres
11
11
  end
12
12
 
13
13
  def self.available?
14
- defined?(Rails) && Rails.respond_to?(:application) && Rails.application&.credentials
14
+ defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application&.credentials
15
15
  end
16
16
  end
17
17
  end
@@ -83,8 +83,7 @@ module ActivePostgres
83
83
  path = value.sub('rails_credentials:', '')
84
84
  keys = path.split('.').map(&:to_sym)
85
85
 
86
- credentials = Rails.application.credentials
87
- keys.reduce(credentials) { |obj, key| obj&.dig(key) }
86
+ Rails.application.credentials.dig(*keys)
88
87
  rescue NameError
89
88
  nil
90
89
  end
@@ -112,6 +112,7 @@ module ActivePostgres
112
112
  puts " superuser_password: \"#{generate_secure_password}\""
113
113
  puts " replication_password: \"#{generate_secure_password}\""
114
114
  puts " repmgr_password: \"#{generate_secure_password}\""
115
+ puts " monitoring_password: \"#{generate_secure_password}\""
115
116
  puts
116
117
  puts '🔐 Secure passwords have been auto-generated above.'
117
118
  puts '📌 Update primary_host and replica_host with your actual IPs after provisioning.'
@@ -11,6 +11,8 @@ shared: &shared
11
11
  version: 18
12
12
  user: ubuntu
13
13
  ssh_key: ~/.ssh/id_rsa
14
+ # SSH host key verification: always (strict) or accept_new (first connection)
15
+ ssh_host_key_verification: always
14
16
 
15
17
  components:
16
18
  core:
@@ -31,17 +33,32 @@ shared: &shared
31
33
  # Optional: Override default repmgr user/database
32
34
  # user: repmgr
33
35
  # database: repmgr
36
+ # replication_user: replication
37
+ # dns_failover:
38
+ # enabled: true
39
+ # provider: dnsmasq
40
+ # domain: mesh.internal
41
+ # dns_servers:
42
+ # - host: 18.170.173.14
43
+ # private_ip: 10.8.0.10
44
+ # - host: 98.85.183.175
45
+ # private_ip: 10.8.0.110
46
+ # primary_record: db-primary.mesh.internal
47
+ # replica_record: db-replica.mesh.internal
34
48
 
35
49
  pgbouncer:
36
50
  enabled: false
37
51
  # Optional: Override default pgbouncer user
38
52
  # user: pgbouncer
53
+ # follow_primary: true
54
+ # follow_primary_interval: 5
39
55
 
40
56
  pgbackrest:
41
57
  enabled: false
42
58
 
43
59
  monitoring:
44
60
  enabled: false
61
+ # user: postgres_exporter
45
62
 
46
63
  ssl:
47
64
  enabled: false
@@ -80,6 +97,6 @@ production:
80
97
  superuser_password: rails_credentials:postgres.superuser_password
81
98
  replication_password: rails_credentials:postgres.replication_password
82
99
  repmgr_password: rails_credentials:postgres.repmgr_password
100
+ monitoring_password: rails_credentials:postgres.monitoring_password
83
101
  pgbouncer_password: rails_credentials:postgres.password
84
102
  app_password: rails_credentials:postgres.password
85
-
@@ -113,12 +113,10 @@ module ActivePostgres
113
113
 
114
114
  def register_database_removal(host, database_name)
115
115
  postgres_user = config.postgres_user
116
+ executor = ssh_executor
116
117
  register("Drop database #{database_name} on #{host}", host: host) do
117
118
  sql = "DROP DATABASE IF EXISTS #{database_name};"
118
- upload! StringIO.new(sql), '/tmp/drop_database.sql'
119
- execute :chmod, '644', '/tmp/drop_database.sql'
120
- execute :sudo, '-u', postgres_user, 'psql', '-f', '/tmp/drop_database.sql'
121
- execute :rm, '-f', '/tmp/drop_database.sql'
119
+ executor.run_sql_on_backend(self, sql, postgres_user: postgres_user, tuples_only: false, capture: false)
122
120
  rescue StandardError
123
121
  nil
124
122
  end
@@ -126,12 +124,10 @@ module ActivePostgres
126
124
 
127
125
  def register_postgres_user_removal(host, username)
128
126
  postgres_user = config.postgres_user
127
+ executor = ssh_executor
129
128
  register("Drop PostgreSQL user #{username} on #{host}", host: host) do
130
129
  sql = "DROP USER IF EXISTS #{username};"
131
- upload! StringIO.new(sql), '/tmp/drop_user.sql'
132
- execute :chmod, '644', '/tmp/drop_user.sql'
133
- execute :sudo, '-u', postgres_user, 'psql', '-f', '/tmp/drop_user.sql'
134
- execute :rm, '-f', '/tmp/drop_user.sql'
130
+ executor.run_sql_on_backend(self, sql, postgres_user: postgres_user, tuples_only: false, capture: false)
135
131
  rescue StandardError
136
132
  nil
137
133
  end
@@ -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"
@@ -280,7 +302,7 @@ module ActivePostgres
280
302
  keys_only: true,
281
303
  forward_agent: false,
282
304
  auth_methods: ['publickey'],
283
- verify_host_key: :accept_new,
305
+ verify_host_key: config.ssh_host_key_verification || :always,
284
306
  timeout: 10,
285
307
  number_of_password_prompts: 0
286
308
  }
@@ -1,3 +1,3 @@
1
1
  module ActivePostgres
2
- VERSION = '0.8.0'.freeze
2
+ VERSION = '0.9.0'.freeze
3
3
  end
@@ -372,6 +372,15 @@ namespace :postgres do
372
372
  installer.setup_component('monitoring')
373
373
  end
374
374
 
375
+ desc 'Setup only pgBackRest backups'
376
+ task pgbackrest: :environment do
377
+ require 'active_postgres'
378
+
379
+ config = ActivePostgres::Configuration.load
380
+ installer = ActivePostgres::Installer.new(config)
381
+ installer.setup_component('pgbackrest')
382
+ end
383
+
375
384
  desc 'Setup only repmgr'
376
385
  task repmgr: :environment do
377
386
  require 'active_postgres'
@@ -412,10 +421,7 @@ namespace :postgres do
412
421
  WHERE rolname = '#{user}'
413
422
  SQL
414
423
 
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'
424
+ user_hash = ssh_executor.run_sql_on_backend(self, sql, postgres_user: postgres_user).to_s.strip
419
425
 
420
426
  if user_hash && !user_hash.empty?
421
427
  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'
@@ -38,3 +38,7 @@ log_file='/var/log/postgresql/repmgr.log'
38
38
  monitoring_history=yes
39
39
  monitor_interval_secs=5
40
40
 
41
+ <% if repmgr_config[:dns_failover] && repmgr_config[:dns_failover][:enabled] %>
42
+ event_notification_command='/usr/local/bin/active-postgres-dns-failover'
43
+ event_notifications='<%= repmgr_config[:dns_failover][:events] || 'repmgrd_failover_promote,standby_promote,standby_switchover,standby_follow' %>'
44
+ <% end %>