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.
- checksums.yaml +4 -4
- data/README.md +87 -5
- 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 +91 -4
- data/lib/active_postgres/components/pgbackrest.rb +38 -2
- data/lib/active_postgres/components/pgbouncer.rb +43 -4
- data/lib/active_postgres/components/repmgr.rb +431 -62
- data/lib/active_postgres/configuration.rb +49 -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 +1 -0
- data/lib/active_postgres/generators/active_postgres/templates/postgres.yml.erb +18 -1
- data/lib/active_postgres/rollback_manager.rb +4 -8
- data/lib/active_postgres/secrets.rb +23 -5
- data/lib/active_postgres/ssh_executor.rb +36 -14
- data/lib/active_postgres/version.rb +1 -1
- data/lib/tasks/postgres.rake +10 -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 +7 -3
- data/templates/repmgr_dns_failover.sh.erb +49 -0
- metadata +5 -1
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
@@ -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: :
|
|
305
|
+
verify_host_key: config.ssh_host_key_verification || :always,
|
|
284
306
|
timeout: 10,
|
|
285
307
|
number_of_password_prompts: 0
|
|
286
308
|
}
|
data/lib/tasks/postgres.rake
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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'
|
|
@@ -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 %>
|