active_postgres 0.4.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 +7 -0
- data/LICENSE +23 -0
- data/README.md +158 -0
- data/exe/activepostgres +5 -0
- data/lib/active_postgres/cli.rb +157 -0
- data/lib/active_postgres/cluster_deployment_flow.rb +85 -0
- data/lib/active_postgres/component_resolver.rb +24 -0
- data/lib/active_postgres/components/base.rb +38 -0
- data/lib/active_postgres/components/core.rb +158 -0
- data/lib/active_postgres/components/extensions.rb +99 -0
- data/lib/active_postgres/components/monitoring.rb +55 -0
- data/lib/active_postgres/components/pgbackrest.rb +94 -0
- data/lib/active_postgres/components/pgbouncer.rb +137 -0
- data/lib/active_postgres/components/repmgr.rb +651 -0
- data/lib/active_postgres/components/ssl.rb +86 -0
- data/lib/active_postgres/configuration.rb +190 -0
- data/lib/active_postgres/connection_pooler.rb +429 -0
- data/lib/active_postgres/credentials.rb +17 -0
- data/lib/active_postgres/deployment_flow.rb +154 -0
- data/lib/active_postgres/error_handler.rb +185 -0
- data/lib/active_postgres/failover.rb +83 -0
- data/lib/active_postgres/generators/active_postgres/install_generator.rb +186 -0
- data/lib/active_postgres/health_checker.rb +244 -0
- data/lib/active_postgres/installer.rb +114 -0
- data/lib/active_postgres/log_sanitizer.rb +67 -0
- data/lib/active_postgres/logger.rb +125 -0
- data/lib/active_postgres/performance_tuner.rb +246 -0
- data/lib/active_postgres/rails/database_config.rb +174 -0
- data/lib/active_postgres/rails/migration_guard.rb +25 -0
- data/lib/active_postgres/railtie.rb +28 -0
- data/lib/active_postgres/retry_helper.rb +80 -0
- data/lib/active_postgres/rollback_manager.rb +140 -0
- data/lib/active_postgres/secrets.rb +86 -0
- data/lib/active_postgres/ssh_executor.rb +288 -0
- data/lib/active_postgres/standby_deployment_flow.rb +122 -0
- data/lib/active_postgres/validator.rb +143 -0
- data/lib/active_postgres/version.rb +3 -0
- data/lib/active_postgres.rb +67 -0
- data/lib/tasks/postgres.rake +855 -0
- data/lib/tasks/rolling_update.rake +258 -0
- data/lib/tasks/rotate_credentials.rake +193 -0
- data/templates/pg_hba.conf.erb +47 -0
- data/templates/pgbackrest.conf.erb +43 -0
- data/templates/pgbouncer.ini.erb +55 -0
- data/templates/postgresql.conf.erb +157 -0
- data/templates/repmgr.conf.erb +40 -0
- metadata +224 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
namespace :postgres do
|
|
2
|
+
namespace :update do
|
|
3
|
+
desc 'Rolling update PostgreSQL version (zero downtime)'
|
|
4
|
+
task :version, [:new_version] => :environment do |_t, args|
|
|
5
|
+
require 'active_postgres'
|
|
6
|
+
|
|
7
|
+
new_version = args[:new_version]
|
|
8
|
+
unless new_version
|
|
9
|
+
puts 'Usage: rake postgres:update:version[18]'
|
|
10
|
+
puts 'Example: rake postgres:update:version[18] (upgrade from 16 to 18)'
|
|
11
|
+
exit 1
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
config = ActivePostgres::Configuration.load
|
|
15
|
+
ssh_executor = ActivePostgres::SSHExecutor.new(config)
|
|
16
|
+
current_version = config.version
|
|
17
|
+
|
|
18
|
+
if current_version.to_s == new_version.to_s
|
|
19
|
+
puts "Already running version #{new_version}"
|
|
20
|
+
exit 0
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
puts "š Rolling update: PostgreSQL #{current_version} ā #{new_version}"
|
|
24
|
+
puts ''
|
|
25
|
+
puts 'This will:'
|
|
26
|
+
puts ' 1. Update standby to new version'
|
|
27
|
+
puts ' 2. Verify standby health'
|
|
28
|
+
puts ' 3. Promote standby to primary (brief switchover ~5-10s)'
|
|
29
|
+
puts ' 4. Update old primary to new version'
|
|
30
|
+
puts ' 5. Optionally switchback to original primary'
|
|
31
|
+
puts ''
|
|
32
|
+
puts 'ā ļø IMPORTANT: Test this in staging first!'
|
|
33
|
+
puts ''
|
|
34
|
+
print 'Continue? (yes/no): '
|
|
35
|
+
response = $stdin.gets.chomp.downcase
|
|
36
|
+
exit 0 unless %w[yes y].include?(response)
|
|
37
|
+
|
|
38
|
+
primary = config.primary_host
|
|
39
|
+
standby = config.standby_hosts.first
|
|
40
|
+
|
|
41
|
+
unless standby
|
|
42
|
+
puts 'ā No standby configured - cannot do rolling update'
|
|
43
|
+
puts 'For single-node updates, use: rake postgres:update:in_place'
|
|
44
|
+
exit 1
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
puts "\nš Cluster Status:"
|
|
48
|
+
puts " Primary: #{primary} (version #{current_version})"
|
|
49
|
+
puts " Standby: #{standby} (version #{current_version})"
|
|
50
|
+
puts ''
|
|
51
|
+
|
|
52
|
+
puts '=' * 60
|
|
53
|
+
puts 'STEP 1: Update standby to new version'
|
|
54
|
+
puts '=' * 60
|
|
55
|
+
|
|
56
|
+
puts "\nUpdating #{standby}..."
|
|
57
|
+
ssh_executor.execute_on_host(standby) do
|
|
58
|
+
execute :sudo, 'systemctl', 'stop', "postgresql@#{current_version}-main"
|
|
59
|
+
execute :sudo, 'apt-get', 'update'
|
|
60
|
+
execute :sudo, 'apt-get', 'install', '-y', "postgresql-#{new_version}", "postgresql-contrib-#{new_version}"
|
|
61
|
+
|
|
62
|
+
puts "Upgrading cluster from #{current_version} to #{new_version}..."
|
|
63
|
+
execute :sudo, 'pg_upgradecluster', current_version.to_s, 'main'
|
|
64
|
+
execute :sudo, 'pg_dropcluster', '--stop', current_version.to_s, 'main'
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
puts "ā Standby upgraded to version #{new_version}"
|
|
68
|
+
puts ''
|
|
69
|
+
|
|
70
|
+
puts '=' * 60
|
|
71
|
+
puts 'STEP 2: Verify standby health'
|
|
72
|
+
puts '=' * 60
|
|
73
|
+
|
|
74
|
+
Rake::Task['postgres:verify'].invoke
|
|
75
|
+
puts ''
|
|
76
|
+
|
|
77
|
+
puts '=' * 60
|
|
78
|
+
puts 'STEP 3: Promote standby to primary'
|
|
79
|
+
puts '=' * 60
|
|
80
|
+
|
|
81
|
+
puts 'ā ļø Brief downtime during switchover (~5-10 seconds)'
|
|
82
|
+
puts ''
|
|
83
|
+
print 'Promote standby? (yes/no): '
|
|
84
|
+
response = $stdin.gets.chomp.downcase
|
|
85
|
+
exit 0 unless %w[yes y].include?(response)
|
|
86
|
+
|
|
87
|
+
Rake::Task['postgres:repmgr:promote'].invoke(standby)
|
|
88
|
+
puts ''
|
|
89
|
+
|
|
90
|
+
puts 'ā Switchover complete'
|
|
91
|
+
puts " New primary: #{standby} (version #{new_version})"
|
|
92
|
+
puts " Old primary: #{primary} (version #{current_version})"
|
|
93
|
+
puts ''
|
|
94
|
+
|
|
95
|
+
puts '=' * 60
|
|
96
|
+
puts 'STEP 4: Update old primary'
|
|
97
|
+
puts '=' * 60
|
|
98
|
+
|
|
99
|
+
puts "\nUpdating #{primary}..."
|
|
100
|
+
ssh_executor.execute_on_host(primary) do
|
|
101
|
+
execute :sudo, 'systemctl', 'stop', "postgresql@#{current_version}-main"
|
|
102
|
+
execute :sudo, 'apt-get', 'update'
|
|
103
|
+
execute :sudo, 'apt-get', 'install', '-y', "postgresql-#{new_version}", "postgresql-contrib-#{new_version}"
|
|
104
|
+
|
|
105
|
+
puts "Upgrading cluster from #{current_version} to #{new_version}..."
|
|
106
|
+
execute :sudo, 'pg_upgradecluster', current_version.to_s, 'main'
|
|
107
|
+
execute :sudo, 'pg_dropcluster', '--stop', current_version.to_s, 'main'
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
puts "ā All nodes upgraded to version #{new_version}"
|
|
111
|
+
puts ''
|
|
112
|
+
|
|
113
|
+
puts '=' * 60
|
|
114
|
+
puts 'ā
Rolling update complete!'
|
|
115
|
+
puts '=' * 60
|
|
116
|
+
puts ''
|
|
117
|
+
puts "Cluster is now running PostgreSQL #{new_version}"
|
|
118
|
+
puts "Current primary: #{standby}"
|
|
119
|
+
puts ''
|
|
120
|
+
puts 'š Next steps:'
|
|
121
|
+
puts " 1. Update config/postgres.yml: version: #{new_version}"
|
|
122
|
+
puts ' 2. Test application thoroughly'
|
|
123
|
+
puts " 3. Optionally switchback: rake postgres:repmgr:promote[#{primary}]"
|
|
124
|
+
puts ' 4. Commit config changes'
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
desc 'Patch current PostgreSQL version (zero downtime)'
|
|
128
|
+
task patch: :environment do
|
|
129
|
+
require 'active_postgres'
|
|
130
|
+
|
|
131
|
+
config = ActivePostgres::Configuration.load
|
|
132
|
+
ssh_executor = ActivePostgres::SSHExecutor.new(config)
|
|
133
|
+
version = config.version
|
|
134
|
+
|
|
135
|
+
primary = config.primary_host
|
|
136
|
+
standby = config.standby_hosts.first
|
|
137
|
+
|
|
138
|
+
unless standby
|
|
139
|
+
puts 'ā No standby configured - cannot do rolling patch'
|
|
140
|
+
puts 'For single-node patching, use: rake postgres:update:in_place_patch'
|
|
141
|
+
exit 1
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
puts 'š Rolling security patch update'
|
|
145
|
+
puts ''
|
|
146
|
+
puts 'This will:'
|
|
147
|
+
puts ' 1. Patch standby and restart'
|
|
148
|
+
puts ' 2. Verify standby health'
|
|
149
|
+
puts ' 3. Promote standby (brief switchover ~5s)'
|
|
150
|
+
puts ' 4. Patch old primary'
|
|
151
|
+
puts ' 5. Switchback'
|
|
152
|
+
puts ''
|
|
153
|
+
print 'Continue? (yes/no): '
|
|
154
|
+
response = $stdin.gets.chomp.downcase
|
|
155
|
+
exit 0 unless %w[yes y].include?(response)
|
|
156
|
+
|
|
157
|
+
puts "\nš Step 1: Patch standby #{standby}"
|
|
158
|
+
ssh_executor.execute_on_host(standby) do
|
|
159
|
+
execute :sudo, 'apt-get', 'update'
|
|
160
|
+
execute :sudo, 'apt-get', 'install', '--only-upgrade', '-y', "postgresql-#{version}"
|
|
161
|
+
execute :sudo, 'systemctl', 'restart', "postgresql@#{version}-main"
|
|
162
|
+
end
|
|
163
|
+
puts 'ā Standby patched and restarted'
|
|
164
|
+
sleep 5
|
|
165
|
+
|
|
166
|
+
puts "\nš Step 2: Promote standby"
|
|
167
|
+
Rake::Task['postgres:repmgr:promote'].invoke(standby)
|
|
168
|
+
sleep 5
|
|
169
|
+
|
|
170
|
+
puts "\nš Step 3: Patch old primary #{primary}"
|
|
171
|
+
ssh_executor.execute_on_host(primary) do
|
|
172
|
+
execute :sudo, 'apt-get', 'update'
|
|
173
|
+
execute :sudo, 'apt-get', 'install', '--only-upgrade', '-y', "postgresql-#{version}"
|
|
174
|
+
execute :sudo, 'systemctl', 'restart', "postgresql@#{version}-main"
|
|
175
|
+
end
|
|
176
|
+
puts 'ā Old primary patched'
|
|
177
|
+
sleep 5
|
|
178
|
+
|
|
179
|
+
puts "\nš Step 4: Switchback to #{primary}"
|
|
180
|
+
Rake::Task['postgres:repmgr:promote'].invoke(primary)
|
|
181
|
+
|
|
182
|
+
puts ''
|
|
183
|
+
puts 'ā
Security patches applied!'
|
|
184
|
+
puts ' Total downtime: ~10 seconds (during switchovers)'
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
desc 'In-place update (requires downtime)'
|
|
188
|
+
task :in_place, [:new_version] => :environment do |_t, args|
|
|
189
|
+
require 'active_postgres'
|
|
190
|
+
|
|
191
|
+
new_version = args[:new_version]
|
|
192
|
+
unless new_version
|
|
193
|
+
puts 'Usage: rake postgres:update:in_place[18]'
|
|
194
|
+
exit 1
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
config = ActivePostgres::Configuration.load
|
|
198
|
+
ssh_executor = ActivePostgres::SSHExecutor.new(config)
|
|
199
|
+
current_version = config.version
|
|
200
|
+
host = config.primary_host
|
|
201
|
+
|
|
202
|
+
puts 'ā ļø WARNING: In-place update requires downtime'
|
|
203
|
+
puts ' For zero-downtime updates, use a standby server'
|
|
204
|
+
puts ''
|
|
205
|
+
puts "Updating PostgreSQL #{current_version} ā #{new_version} on #{host}"
|
|
206
|
+
puts ''
|
|
207
|
+
print 'Continue? (yes/no): '
|
|
208
|
+
response = $stdin.gets.chomp.downcase
|
|
209
|
+
exit 0 unless %w[yes y].include?(response)
|
|
210
|
+
|
|
211
|
+
puts "\nš Starting in-place upgrade..."
|
|
212
|
+
ssh_executor.execute_on_host(host) do
|
|
213
|
+
execute :sudo, 'systemctl', 'stop', "postgresql@#{current_version}-main"
|
|
214
|
+
execute :sudo, 'apt-get', 'update'
|
|
215
|
+
execute :sudo, 'apt-get', 'install', '-y', "postgresql-#{new_version}"
|
|
216
|
+
|
|
217
|
+
execute :sudo, 'pg_upgradecluster', current_version.to_s, 'main'
|
|
218
|
+
execute :sudo, 'pg_dropcluster', '--stop', current_version.to_s, 'main'
|
|
219
|
+
execute :sudo, 'systemctl', 'start', "postgresql@#{new_version}-main"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
puts "ā
Upgraded to PostgreSQL #{new_version}"
|
|
223
|
+
puts " Update config/postgres.yml: version: #{new_version}"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
namespace :repmgr do
|
|
228
|
+
desc 'Promote standby to primary (switchover)'
|
|
229
|
+
task :promote, [:host] => :environment do |_t, args|
|
|
230
|
+
require 'active_postgres'
|
|
231
|
+
|
|
232
|
+
host = args[:host]
|
|
233
|
+
unless host
|
|
234
|
+
puts 'Usage: rake postgres:repmgr:promote[host]'
|
|
235
|
+
exit 1
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
config = ActivePostgres::Configuration.load
|
|
239
|
+
ssh_executor = ActivePostgres::SSHExecutor.new(config)
|
|
240
|
+
|
|
241
|
+
puts "š Promoting #{host} to primary..."
|
|
242
|
+
puts 'ā±ļø Expected downtime: 5-10 seconds'
|
|
243
|
+
puts ''
|
|
244
|
+
|
|
245
|
+
ssh_executor.execute_on_host(host) do
|
|
246
|
+
execute :sudo, '-u', 'postgres', 'repmgr', 'standby', 'promote'
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
sleep 3
|
|
250
|
+
|
|
251
|
+
puts 'ā
Promotion complete!'
|
|
252
|
+
puts " New primary: #{host}"
|
|
253
|
+
puts ''
|
|
254
|
+
puts 'š Verify cluster:'
|
|
255
|
+
puts ' rake postgres:verify'
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
namespace :postgres do
|
|
2
|
+
namespace :credentials do
|
|
3
|
+
desc 'Rotate app user password (zero downtime)'
|
|
4
|
+
task :rotate, [:new_password] => :environment do |_t, args|
|
|
5
|
+
require 'active_postgres'
|
|
6
|
+
|
|
7
|
+
config = ActivePostgres::Configuration.load
|
|
8
|
+
ssh_executor = ActivePostgres::SSHExecutor.new(config)
|
|
9
|
+
ActivePostgres::Secrets.new(config)
|
|
10
|
+
|
|
11
|
+
new_password = args[:new_password]
|
|
12
|
+
unless new_password
|
|
13
|
+
puts 'Usage: rake postgres:credentials:rotate[new_password]'
|
|
14
|
+
puts 'Or generate random: rake postgres:credentials:rotate_random'
|
|
15
|
+
exit 1
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
app_user = config.app_user
|
|
19
|
+
host = config.primary_host
|
|
20
|
+
|
|
21
|
+
puts "Rotating password for user '#{app_user}' on #{host}..."
|
|
22
|
+
puts 'ā ļø IMPORTANT: Update Rails credentials after this completes!'
|
|
23
|
+
puts ''
|
|
24
|
+
|
|
25
|
+
ssh_executor.execute_on_host(host) do
|
|
26
|
+
escaped_password = new_password.gsub("'", "''")
|
|
27
|
+
|
|
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'
|
|
33
|
+
|
|
34
|
+
puts "ā Updated PostgreSQL password for #{app_user}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if config.component_enabled?(:pgbouncer)
|
|
38
|
+
puts 'Updating PgBouncer userlist...'
|
|
39
|
+
|
|
40
|
+
ssh_executor.execute_on_host(host) do
|
|
41
|
+
postgres_user = config.postgres_user
|
|
42
|
+
userlist_entries = []
|
|
43
|
+
|
|
44
|
+
[postgres_user, app_user].compact.uniq.each do |user|
|
|
45
|
+
sql = <<~SQL.strip
|
|
46
|
+
SELECT concat('"', rolname, '" "', rolpassword, '"')
|
|
47
|
+
FROM pg_authid
|
|
48
|
+
WHERE rolname = '#{user}'
|
|
49
|
+
SQL
|
|
50
|
+
|
|
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'
|
|
55
|
+
|
|
56
|
+
userlist_entries << user_hash if user_hash && !user_hash.empty?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if userlist_entries.any?
|
|
60
|
+
userlist_content = "#{userlist_entries.join("\n")}\n"
|
|
61
|
+
upload! StringIO.new(userlist_content), '/tmp/userlist.txt'
|
|
62
|
+
execute :sudo, 'mv', '/tmp/userlist.txt', '/etc/pgbouncer/userlist.txt'
|
|
63
|
+
execute :sudo, 'chmod', '640', '/etc/pgbouncer/userlist.txt'
|
|
64
|
+
execute :sudo, 'chown', 'postgres:postgres', '/etc/pgbouncer/userlist.txt'
|
|
65
|
+
execute :sudo, 'systemctl', 'reload', 'pgbouncer'
|
|
66
|
+
|
|
67
|
+
puts 'ā Updated PgBouncer userlist and reloaded (zero downtime)'
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
puts ''
|
|
73
|
+
puts 'ā
Password rotation complete!'
|
|
74
|
+
puts ''
|
|
75
|
+
puts 'š Next steps:'
|
|
76
|
+
puts '1. Update Rails credentials:'
|
|
77
|
+
puts ' rails credentials:edit'
|
|
78
|
+
puts ''
|
|
79
|
+
puts ' Add/update:'
|
|
80
|
+
puts ' postgres:'
|
|
81
|
+
puts " password: \"#{new_password}\""
|
|
82
|
+
puts ''
|
|
83
|
+
puts '2. Restart Rails app to use new password'
|
|
84
|
+
puts ' cap production deploy:restart'
|
|
85
|
+
puts ''
|
|
86
|
+
puts 'ā ļø Old password still works until Rails restarts'
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
desc 'Rotate app user password with random generated password (zero downtime)'
|
|
90
|
+
task rotate_random: :environment do
|
|
91
|
+
require 'securerandom'
|
|
92
|
+
new_password = SecureRandom.base64(32)
|
|
93
|
+
|
|
94
|
+
puts 'š Generated secure random password'
|
|
95
|
+
puts ''
|
|
96
|
+
|
|
97
|
+
Rake::Task['postgres:credentials:rotate'].invoke(new_password)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
desc 'Rotate all passwords (app, repmgr, superuser) - zero downtime'
|
|
101
|
+
task rotate_all: :environment do
|
|
102
|
+
require 'active_postgres'
|
|
103
|
+
require 'securerandom'
|
|
104
|
+
|
|
105
|
+
config = ActivePostgres::Configuration.load
|
|
106
|
+
ssh_executor = ActivePostgres::SSHExecutor.new(config)
|
|
107
|
+
|
|
108
|
+
users = [
|
|
109
|
+
{ name: config.app_user, credential_key: 'password' },
|
|
110
|
+
{ name: config.repmgr_user, credential_key: 'repmgr_password' },
|
|
111
|
+
{ name: 'postgres', credential_key: 'superuser_password' }
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
new_passwords = {}
|
|
115
|
+
host = config.primary_host
|
|
116
|
+
|
|
117
|
+
puts 'š Rotating all PostgreSQL passwords...'
|
|
118
|
+
puts ''
|
|
119
|
+
|
|
120
|
+
users.each do |user_info|
|
|
121
|
+
username = user_info[:name]
|
|
122
|
+
new_password = SecureRandom.base64(32)
|
|
123
|
+
new_passwords[user_info[:credential_key]] = new_password
|
|
124
|
+
|
|
125
|
+
puts "Rotating #{username}..."
|
|
126
|
+
|
|
127
|
+
ssh_executor.execute_on_host(host) do
|
|
128
|
+
escaped_password = new_password.gsub("'", "''")
|
|
129
|
+
|
|
130
|
+
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'
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
puts "ā Updated #{username}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
if config.component_enabled?(:pgbouncer)
|
|
141
|
+
puts ''
|
|
142
|
+
puts 'Updating PgBouncer userlist...'
|
|
143
|
+
|
|
144
|
+
ssh_executor.execute_on_host(host) do
|
|
145
|
+
postgres_user = 'postgres'
|
|
146
|
+
userlist_entries = []
|
|
147
|
+
|
|
148
|
+
users.map { |u| u[:name] }.compact.uniq.each do |user|
|
|
149
|
+
sql = <<~SQL.strip
|
|
150
|
+
SELECT concat('"', rolname, '" "', rolpassword, '"')
|
|
151
|
+
FROM pg_authid
|
|
152
|
+
WHERE rolname = '#{user}'
|
|
153
|
+
SQL
|
|
154
|
+
|
|
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'
|
|
159
|
+
|
|
160
|
+
userlist_entries << user_hash if user_hash && !user_hash.empty?
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
if userlist_entries.any?
|
|
164
|
+
userlist_content = "#{userlist_entries.join("\n")}\n"
|
|
165
|
+
execute :sudo, 'tee', '/etc/pgbouncer/userlist.txt', stdin: StringIO.new(userlist_content)
|
|
166
|
+
execute :sudo, 'chmod', '640', '/etc/pgbouncer/userlist.txt'
|
|
167
|
+
execute :sudo, 'chown', 'postgres:postgres', '/etc/pgbouncer/userlist.txt'
|
|
168
|
+
execute :sudo, 'systemctl', 'reload', 'pgbouncer'
|
|
169
|
+
|
|
170
|
+
puts 'ā PgBouncer userlist updated and reloaded'
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
puts ''
|
|
176
|
+
puts 'ā
All passwords rotated!'
|
|
177
|
+
puts ''
|
|
178
|
+
puts 'š New passwords to add to Rails credentials:'
|
|
179
|
+
puts ''
|
|
180
|
+
puts 'rails credentials:edit'
|
|
181
|
+
puts ''
|
|
182
|
+
puts 'postgres:'
|
|
183
|
+
new_passwords.each do |key, password|
|
|
184
|
+
puts " #{key}: \"#{password}\""
|
|
185
|
+
end
|
|
186
|
+
puts ''
|
|
187
|
+
puts 'ā ļø Save these passwords securely before continuing!'
|
|
188
|
+
puts ''
|
|
189
|
+
puts 'After updating credentials:'
|
|
190
|
+
puts ' cap production deploy:restart'
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# PostgreSQL Client Authentication Configuration File
|
|
2
|
+
# Generated by active_postgres
|
|
3
|
+
<%
|
|
4
|
+
core_config = config.component_config(:core)
|
|
5
|
+
pg_hba_rules = core_config[:pg_hba] || []
|
|
6
|
+
|
|
7
|
+
# Detect network from primary private_ip
|
|
8
|
+
detected_network = if config.primary && config.primary['private_ip']
|
|
9
|
+
ip_parts = config.primary['private_ip'].split('.')
|
|
10
|
+
"#{ip_parts[0]}.#{ip_parts[1]}.0.0/16"
|
|
11
|
+
else
|
|
12
|
+
'10.8.0.0/16' # Default to common VPN/private network
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Default rules if none specified
|
|
16
|
+
if pg_hba_rules.empty?
|
|
17
|
+
pg_hba_rules = [
|
|
18
|
+
{type: 'local', database: 'all', user: 'all', method: 'peer'},
|
|
19
|
+
{type: 'host', database: 'all', user: 'all', address: '127.0.0.1/32', method: 'scram-sha-256'},
|
|
20
|
+
{type: 'host', database: 'all', user: 'all', address: detected_network, method: 'scram-sha-256'}
|
|
21
|
+
]
|
|
22
|
+
end
|
|
23
|
+
%>
|
|
24
|
+
|
|
25
|
+
# TYPE DATABASE USER ADDRESS METHOD
|
|
26
|
+
|
|
27
|
+
<% pg_hba_rules.each do |rule| %>
|
|
28
|
+
<% if rule[:type] == 'local' %>
|
|
29
|
+
<%= rule[:type] %> <%= rule[:database] %> <%= rule[:user] %> <%= rule[:method] %>
|
|
30
|
+
<% else %>
|
|
31
|
+
<%= rule[:type] %> <%= rule[:database] %> <%= rule[:user] %> <%= rule[:address] %> <%= rule[:method] %>
|
|
32
|
+
<% end %>
|
|
33
|
+
<% end %>
|
|
34
|
+
|
|
35
|
+
<% if config.component_enabled?(:repmgr) && !pg_hba_rules.any? { |r| r[:database] == 'replication' } %>
|
|
36
|
+
# Replication connections (auto-added for repmgr)
|
|
37
|
+
<%
|
|
38
|
+
# Find the network rule (not 127.0.0.1) for replication, or fall back to 10.8.0.0/24
|
|
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
|
+
repmgr_user = config.repmgr_user
|
|
41
|
+
repmgr_db = config.repmgr_database
|
|
42
|
+
%>
|
|
43
|
+
host replication <%= repmgr_user %> <%= repmgr_network %> scram-sha-256
|
|
44
|
+
host <%= repmgr_db %> <%= repmgr_user %> <%= repmgr_network %> scram-sha-256
|
|
45
|
+
<% end %>
|
|
46
|
+
|
|
47
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[global]
|
|
2
|
+
<% if pgbackrest_config[:repo_type] == 's3' %>
|
|
3
|
+
repo1-type=s3
|
|
4
|
+
repo1-path=<%= pgbackrest_config[:repo_path] || '/backups' %>
|
|
5
|
+
repo1-s3-bucket=<%= pgbackrest_config[:s3_bucket] %>
|
|
6
|
+
repo1-s3-region=<%= pgbackrest_config[:s3_region] || 'us-east-1' %>
|
|
7
|
+
<% if pgbackrest_config[:s3_endpoint] %>
|
|
8
|
+
# Custom S3-compatible endpoint (DigitalOcean Spaces, Backblaze B2, Wasabi, MinIO, etc.)
|
|
9
|
+
repo1-s3-endpoint=<%= pgbackrest_config[:s3_endpoint] %>
|
|
10
|
+
repo1-s3-uri-style=<%= pgbackrest_config[:s3_uri_style] || 'path' %>
|
|
11
|
+
<% else %>
|
|
12
|
+
# AWS S3
|
|
13
|
+
repo1-s3-endpoint=s3.<%= pgbackrest_config[:s3_region] || 'us-east-1' %>.amazonaws.com
|
|
14
|
+
<% end %>
|
|
15
|
+
<% if secrets_obj.resolve('s3_access_key') %>
|
|
16
|
+
repo1-s3-key=<%= secrets_obj.resolve('s3_access_key') %>
|
|
17
|
+
repo1-s3-key-secret=<%= secrets_obj.resolve('s3_secret_key') %>
|
|
18
|
+
<% end %>
|
|
19
|
+
<% else %>
|
|
20
|
+
# Local storage
|
|
21
|
+
repo1-path=<%= pgbackrest_config[:repo_path] || '/var/lib/pgbackrest' %>
|
|
22
|
+
<% end %>
|
|
23
|
+
|
|
24
|
+
# Retention
|
|
25
|
+
repo1-retention-full=<%= pgbackrest_config[:retention_full] || 7 %>
|
|
26
|
+
repo1-retention-archive=<%= pgbackrest_config[:retention_archive] || 14 %>
|
|
27
|
+
|
|
28
|
+
# Compression
|
|
29
|
+
compress-type=lz4
|
|
30
|
+
compress-level=3
|
|
31
|
+
|
|
32
|
+
<% if pgbackrest_config[:encryption] %>
|
|
33
|
+
# Encryption (recommended for cloud storage)
|
|
34
|
+
repo1-cipher-type=aes-256-cbc
|
|
35
|
+
repo1-cipher-pass=<%= secrets_obj.resolve('backup_encryption_key') %>
|
|
36
|
+
<% end %>
|
|
37
|
+
|
|
38
|
+
[main]
|
|
39
|
+
pg1-path=/var/lib/postgresql/<%= config.version %>/main
|
|
40
|
+
pg1-port=5432
|
|
41
|
+
pg1-socket-path=/var/run/postgresql
|
|
42
|
+
|
|
43
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[databases]
|
|
2
|
+
* = host=<%= pgbouncer_config[:database_host] || '127.0.0.1' %> port=<%= pgbouncer_config[:database_port] || 5432 %>
|
|
3
|
+
|
|
4
|
+
[pgbouncer]
|
|
5
|
+
# Network settings
|
|
6
|
+
listen_port = <%= pgbouncer_config[:listen_port] || 6432 %>
|
|
7
|
+
listen_addr = <%= pgbouncer_config[:listen_addr] || '*' %>
|
|
8
|
+
unix_socket_dir = <%= pgbouncer_config[:unix_socket_dir] || '/var/run/postgresql' %>
|
|
9
|
+
|
|
10
|
+
# Authentication
|
|
11
|
+
auth_type = <%= pgbouncer_config[:auth_type] || 'scram-sha-256' %>
|
|
12
|
+
auth_file = <%= pgbouncer_config[:auth_file] || '/etc/pgbouncer/userlist.txt' %>
|
|
13
|
+
|
|
14
|
+
# Pool settings (automatically calculated based on PostgreSQL max_connections)
|
|
15
|
+
pool_mode = <%= pgbouncer_config[:pool_mode] || 'transaction' %>
|
|
16
|
+
max_client_conn = <%= pgbouncer_config[:max_client_conn] || 1000 %>
|
|
17
|
+
default_pool_size = <%= pgbouncer_config[:default_pool_size] || 25 %>
|
|
18
|
+
<% if pgbouncer_config[:min_pool_size] %>
|
|
19
|
+
min_pool_size = <%= pgbouncer_config[:min_pool_size] %>
|
|
20
|
+
<% end %>
|
|
21
|
+
reserve_pool_size = <%= pgbouncer_config[:reserve_pool_size] || 5 %>
|
|
22
|
+
reserve_pool_timeout = <%= pgbouncer_config[:reserve_pool_timeout] || 5 %>
|
|
23
|
+
<% if pgbouncer_config[:max_db_connections] %>
|
|
24
|
+
max_db_connections = <%= pgbouncer_config[:max_db_connections] %>
|
|
25
|
+
<% end %>
|
|
26
|
+
<% if pgbouncer_config[:max_user_connections] %>
|
|
27
|
+
max_user_connections = <%= pgbouncer_config[:max_user_connections] %>
|
|
28
|
+
<% end %>
|
|
29
|
+
|
|
30
|
+
# Connection behavior
|
|
31
|
+
server_reset_query = <%= pgbouncer_config[:server_reset_query] || 'DISCARD ALL' %>
|
|
32
|
+
server_lifetime = <%= pgbouncer_config[:server_lifetime] || 3600 %>
|
|
33
|
+
server_idle_timeout = <%= pgbouncer_config[:server_idle_timeout] || 600 %>
|
|
34
|
+
server_connect_timeout = <%= pgbouncer_config[:server_connect_timeout] || 15 %>
|
|
35
|
+
client_idle_timeout = <%= pgbouncer_config[:client_idle_timeout] || 0 %>
|
|
36
|
+
client_login_timeout = <%= pgbouncer_config[:client_login_timeout] || 60 %>
|
|
37
|
+
query_timeout = <%= pgbouncer_config[:query_timeout] || 0 %>
|
|
38
|
+
query_wait_timeout = <%= pgbouncer_config[:query_wait_timeout] || 120 %>
|
|
39
|
+
|
|
40
|
+
# Logging and administration
|
|
41
|
+
admin_users = <%= pgbouncer_config[:admin_users] || config.postgres_user %>
|
|
42
|
+
stats_users = <%= pgbouncer_config[:stats_users] || config.postgres_user %>
|
|
43
|
+
log_connections = <%= pgbouncer_config[:log_connections] || 1 %>
|
|
44
|
+
log_disconnections = <%= pgbouncer_config[:log_disconnections] || 1 %>
|
|
45
|
+
log_pooler_errors = <%= pgbouncer_config[:log_pooler_errors] || 1 %>
|
|
46
|
+
|
|
47
|
+
# Process management
|
|
48
|
+
<% if pgbouncer_config[:pidfile] %>
|
|
49
|
+
pidfile = <%= pgbouncer_config[:pidfile] %>
|
|
50
|
+
<% end %>
|
|
51
|
+
<% if pgbouncer_config[:logfile] %>
|
|
52
|
+
logfile = <%= pgbouncer_config[:logfile] %>
|
|
53
|
+
<% end %>
|
|
54
|
+
|
|
55
|
+
|