active_postgres 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +142 -5
  3. data/lib/active_postgres/cli.rb +7 -0
  4. data/lib/active_postgres/components/core.rb +2 -7
  5. data/lib/active_postgres/components/extensions.rb +40 -35
  6. data/lib/active_postgres/components/monitoring.rb +256 -4
  7. data/lib/active_postgres/components/pgbackrest.rb +91 -5
  8. data/lib/active_postgres/components/pgbouncer.rb +58 -7
  9. data/lib/active_postgres/components/repmgr.rb +448 -62
  10. data/lib/active_postgres/configuration.rb +66 -1
  11. data/lib/active_postgres/connection_pooler.rb +3 -12
  12. data/lib/active_postgres/credentials.rb +3 -3
  13. data/lib/active_postgres/direct_executor.rb +1 -2
  14. data/lib/active_postgres/generators/active_postgres/install_generator.rb +2 -0
  15. data/lib/active_postgres/generators/active_postgres/templates/postgres.yml.erb +28 -1
  16. data/lib/active_postgres/health_checker.rb +29 -7
  17. data/lib/active_postgres/installer.rb +9 -0
  18. data/lib/active_postgres/overview.rb +351 -0
  19. data/lib/active_postgres/rollback_manager.rb +4 -8
  20. data/lib/active_postgres/secrets.rb +23 -5
  21. data/lib/active_postgres/ssh_executor.rb +44 -19
  22. data/lib/active_postgres/version.rb +1 -1
  23. data/lib/active_postgres.rb +1 -0
  24. data/lib/tasks/postgres.rake +77 -4
  25. data/lib/tasks/rotate_credentials.rake +4 -16
  26. data/templates/pg_hba.conf.erb +4 -1
  27. data/templates/pgbackrest.conf.erb +28 -0
  28. data/templates/pgbouncer-follow-primary.service.erb +8 -0
  29. data/templates/pgbouncer-follow-primary.timer.erb +11 -0
  30. data/templates/pgbouncer.ini.erb +2 -0
  31. data/templates/pgbouncer_follow_primary.sh.erb +34 -0
  32. data/templates/postgresql.conf.erb +4 -0
  33. data/templates/repmgr.conf.erb +10 -3
  34. data/templates/repmgr_dns_failover.sh.erb +59 -0
  35. metadata +6 -1
@@ -19,6 +19,8 @@ module ActivePostgres
19
19
  config.all_hosts.each do |host|
20
20
  ssh_executor.execute_on_host(host) do
21
21
  execute :sudo, 'apt-get', 'remove', '-y', 'pgbackrest'
22
+ execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup'
23
+ execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-incremental'
22
24
  end
23
25
  end
24
26
  end
@@ -27,13 +29,21 @@ module ActivePostgres
27
29
  puts "pgBackRest is a backup tool and doesn't run as a service."
28
30
  end
29
31
 
32
+ def install_on_standby(host)
33
+ puts " Installing pgBackRest on standby #{host}..."
34
+ install_on_host(host, create_stanza: false)
35
+ end
36
+
30
37
  def run_backup(type = 'full')
31
38
  puts "Running #{type} backup..."
32
39
  postgres_user = config.postgres_user
33
40
 
41
+ output = nil
34
42
  ssh_executor.execute_on_host(config.primary_host) do
35
- execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', "--type=#{type}", 'backup'
43
+ output = capture(:sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', "--type=#{type}", 'backup')
36
44
  end
45
+
46
+ puts output if output
37
47
  end
38
48
 
39
49
  def run_restore(backup_id)
@@ -45,20 +55,43 @@ module ActivePostgres
45
55
  execute :sudo, 'systemctl', 'stop', 'postgresql'
46
56
 
47
57
  # Restore
48
- execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', "--set=#{backup_id}", 'restore'
58
+ output = capture(:sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', "--set=#{backup_id}", 'restore')
59
+ puts output if output
49
60
 
50
61
  # Start PostgreSQL
51
62
  execute :sudo, 'systemctl', 'start', 'postgresql'
52
63
  end
53
64
  end
54
65
 
66
+ def run_restore_at(target_time, target_action: 'promote')
67
+ puts "Restoring to #{target_time} (PITR)..."
68
+ postgres_user = config.postgres_user
69
+
70
+ ssh_executor.execute_on_host(config.primary_host) do
71
+ execute :sudo, 'systemctl', 'stop', 'postgresql'
72
+
73
+ output = capture(:sudo, '-u', postgres_user, 'pgbackrest',
74
+ '--stanza=main',
75
+ '--type=time',
76
+ "--target=#{target_time}",
77
+ "--target-action=#{target_action}",
78
+ 'restore')
79
+ puts output if output
80
+
81
+ execute :sudo, 'systemctl', 'start', 'postgresql'
82
+ end
83
+ end
84
+
55
85
  def list_backups
56
86
  puts 'Available backups:'
57
87
  postgres_user = config.postgres_user
58
88
 
89
+ output = nil
59
90
  ssh_executor.execute_on_host(config.primary_host) do
60
- execute :sudo, '-u', postgres_user, 'pgbackrest', 'info'
91
+ output = capture(:sudo, '-u', postgres_user, 'pgbackrest', 'info')
61
92
  end
93
+
94
+ puts output if output
62
95
  end
63
96
 
64
97
  private
@@ -66,7 +99,7 @@ module ActivePostgres
66
99
  def install_on_host(host, create_stanza: true)
67
100
  puts " Installing pgBackRest on #{host}..."
68
101
 
69
- pgbackrest_config = config.component_config(:pgbackrest)
102
+ pgbackrest_config = secrets.resolve_value(config.component_config(:pgbackrest))
70
103
  postgres_user = config.postgres_user
71
104
  secrets_obj = secrets
72
105
  _ = pgbackrest_config # Used in ERB template
@@ -78,7 +111,8 @@ module ActivePostgres
78
111
  end
79
112
 
80
113
  # Upload configuration
81
- upload_template(host, 'pgbackrest.conf.erb', '/etc/pgbackrest.conf', binding, mode: '644')
114
+ upload_template(host, 'pgbackrest.conf.erb', '/etc/pgbackrest.conf', binding,
115
+ mode: '640', owner: "root:#{postgres_user}")
82
116
 
83
117
  ssh_executor.execute_on_host(host) do
84
118
  execute :sudo, 'rm', '-rf', '/var/lib/pgbackrest', '||', 'true'
@@ -99,6 +133,58 @@ module ActivePostgres
99
133
  execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', 'stanza-create'
100
134
  end
101
135
  end
136
+
137
+ # Set up scheduled backups on primary only
138
+ if create_stanza
139
+ setup_backup_schedules(host, pgbackrest_config)
140
+ end
141
+ end
142
+
143
+ def setup_backup_schedules(host, pgbackrest_config)
144
+ schedules = backup_schedules(pgbackrest_config)
145
+ schedules.each do |entry|
146
+ setup_backup_schedule(host, entry[:schedule], entry[:type], entry[:file])
147
+ end
148
+ end
149
+
150
+ def backup_schedules(pgbackrest_config)
151
+ schedule_full = pgbackrest_config[:schedule_full] || pgbackrest_config[:schedule]
152
+ schedule_incremental = pgbackrest_config[:schedule_incremental]
153
+
154
+ schedules = []
155
+ if schedule_full
156
+ schedules << { type: 'full', schedule: schedule_full, file: '/etc/cron.d/pgbackrest-backup' }
157
+ end
158
+ if schedule_incremental
159
+ schedules << { type: 'incremental', schedule: schedule_incremental, file: '/etc/cron.d/pgbackrest-backup-incremental' }
160
+ end
161
+
162
+ schedules
163
+ end
164
+
165
+ def setup_backup_schedule(host, schedule, backup_type = 'full', cron_file = '/etc/cron.d/pgbackrest-backup')
166
+ puts " Setting up #{backup_type} backup schedule: #{schedule}"
167
+ postgres_user = config.postgres_user
168
+
169
+ # Create cron job for scheduled backups
170
+ # /etc/cron.d format requires username after time spec
171
+ cron_content = <<~CRON
172
+ # pgBackRest scheduled backups (managed by active_postgres)
173
+ SHELL=/bin/bash
174
+ PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
175
+ #{schedule} #{postgres_user} pgbackrest --stanza=main --type=#{backup_type} backup
176
+ CRON
177
+
178
+ # Install cron job in /etc/cron.d (system cron directory)
179
+ # Use a temp file + sudo mv to avoid shell redirection permission issues.
180
+ ssh_executor.upload_file(host, cron_content, cron_file, mode: '644', owner: 'root:root')
181
+ end
182
+
183
+ def remove_backup_schedule(host)
184
+ ssh_executor.execute_on_host(host) do
185
+ execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup'
186
+ execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-incremental'
187
+ end
102
188
  end
103
189
  end
104
190
  end
@@ -5,7 +5,8 @@ module ActivePostgres
5
5
  puts 'Installing PgBouncer for connection pooling...'
6
6
 
7
7
  config.all_hosts.each do |host|
8
- install_on_host(host)
8
+ is_standby = config.standby_hosts.include?(host)
9
+ install_on_host(host, is_standby: is_standby)
9
10
  end
10
11
  end
11
12
 
@@ -14,6 +15,11 @@ module ActivePostgres
14
15
 
15
16
  config.all_hosts.each do |host|
16
17
  ssh_executor.execute_on_host(host) do
18
+ execute :sudo, 'systemctl', 'disable', '--now', 'pgbouncer-follow-primary.timer', '||', 'true'
19
+ execute :sudo, 'rm', '-f', '/usr/local/bin/pgbouncer-follow-primary',
20
+ '/etc/systemd/system/pgbouncer-follow-primary.service',
21
+ '/etc/systemd/system/pgbouncer-follow-primary.timer'
22
+ execute :sudo, 'systemctl', 'daemon-reload'
17
23
  execute :sudo, 'systemctl', 'stop', 'pgbouncer'
18
24
  execute :sudo, 'apt-get', 'remove', '-y', 'pgbouncer'
19
25
  end
@@ -44,21 +50,42 @@ module ActivePostgres
44
50
 
45
51
  def install_on_standby(standby_host)
46
52
  puts "Installing PgBouncer on standby #{standby_host}..."
47
- install_on_host(standby_host)
53
+ install_on_host(standby_host, is_standby: true)
48
54
  end
49
55
 
50
56
  private
51
57
 
52
- def install_on_host(host)
58
+ def install_on_host(host, is_standby: false)
53
59
  puts " Installing PgBouncer on #{host}..."
54
60
 
55
61
  user_config = config.component_config(:pgbouncer)
56
62
 
63
+ # Determine follow_primary behavior:
64
+ # - Primary: use global follow_primary setting
65
+ # - Standby: check per-standby pgbouncer_follow_primary, default false (use localhost for read replicas)
66
+ if is_standby
67
+ standby_config = config.standby_config_for(host) || {}
68
+ follow_primary = standby_config['pgbouncer_follow_primary'] == true
69
+ else
70
+ follow_primary = user_config[:follow_primary] == true
71
+ end
72
+
73
+ if follow_primary && !config.component_enabled?(:repmgr)
74
+ raise Error, 'PgBouncer follow_primary requires repmgr to be enabled'
75
+ end
76
+
57
77
  max_connections = get_postgres_max_connections(host)
58
78
  optimal_pool = ConnectionPooler.calculate_optimal_pool_sizes(max_connections)
59
79
 
60
80
  pgbouncer_config = optimal_pool.merge(user_config)
81
+ # For standbys not following primary, use localhost; otherwise use primary host
82
+ pgbouncer_config[:database_host] = follow_primary ? config.primary_replication_host : '127.0.0.1'
61
83
  ssl_enabled = config.component_enabled?(:ssl)
84
+ has_ca_cert = ssl_enabled && secrets.resolve('ssl_chain')
85
+ secrets_obj = secrets
86
+ _ = pgbouncer_config
87
+ _ = has_ca_cert
88
+ _ = secrets_obj
62
89
 
63
90
  puts " Calculated pool settings for max_connections=#{max_connections}"
64
91
 
@@ -76,6 +103,8 @@ module ActivePostgres
76
103
  execute :sudo, 'systemctl', 'enable', 'pgbouncer'
77
104
  execute :sudo, 'systemctl', 'restart', 'pgbouncer'
78
105
  end
106
+
107
+ install_follow_primary(host, pgbouncer_config) if follow_primary
79
108
  end
80
109
 
81
110
  def setup_ssl_certs(host)
@@ -130,10 +159,7 @@ module ActivePostgres
130
159
  def fetch_user_hash(backend, user, postgres_user)
131
160
  sql = build_user_hash_sql(user)
132
161
 
133
- backend.upload! StringIO.new(sql), '/tmp/get_user_hash.sql'
134
- backend.execute :chmod, '644', '/tmp/get_user_hash.sql'
135
- user_hash = backend.capture(:sudo, '-u', postgres_user, 'psql', '-t', '-f', '/tmp/get_user_hash.sql').strip
136
- backend.execute :rm, '-f', '/tmp/get_user_hash.sql'
162
+ user_hash = ssh_executor.run_sql_on_backend(backend, sql, postgres_user: postgres_user).to_s.strip
137
163
 
138
164
  if user_hash && !user_hash.empty?
139
165
  puts " ✓ Added #{user} to PgBouncer userlist"
@@ -167,6 +193,31 @@ module ActivePostgres
167
193
  warn ' Warning: No users added to userlist - connections may fail'
168
194
  end
169
195
  end
196
+
197
+ def install_follow_primary(host, pgbouncer_config)
198
+ interval = pgbouncer_config[:follow_primary_interval] || 5
199
+ interval = interval.to_i
200
+ interval = 5 if interval <= 0
201
+
202
+ repmgr_conf = '/etc/repmgr.conf'
203
+ postgres_user = config.postgres_user
204
+ _ = interval
205
+ _ = repmgr_conf
206
+ _ = postgres_user
207
+
208
+ upload_template(host, 'pgbouncer_follow_primary.sh.erb', '/usr/local/bin/pgbouncer-follow-primary', binding,
209
+ mode: '755', owner: 'root:root')
210
+ upload_template(host, 'pgbouncer-follow-primary.service.erb',
211
+ '/etc/systemd/system/pgbouncer-follow-primary.service', binding, mode: '644', owner: 'root:root')
212
+ upload_template(host, 'pgbouncer-follow-primary.timer.erb',
213
+ '/etc/systemd/system/pgbouncer-follow-primary.timer', binding, mode: '644', owner: 'root:root')
214
+
215
+ ssh_executor.execute_on_host(host) do
216
+ execute :sudo, 'systemctl', 'daemon-reload'
217
+ execute :sudo, 'systemctl', 'enable', '--now', 'pgbouncer-follow-primary.timer'
218
+ execute :sudo, 'systemctl', 'start', 'pgbouncer-follow-primary.service'
219
+ end
220
+ end
170
221
  end
171
222
  end
172
223
  end