active_postgres 0.9.1 → 0.9.3
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 +3 -5
- data/lib/active_postgres/components/core.rb +5 -5
- data/lib/active_postgres/components/pgbackrest.rb +1 -0
- data/lib/active_postgres/components/pgbouncer.rb +13 -0
- data/lib/active_postgres/components/repmgr.rb +81 -2
- data/lib/active_postgres/generators/active_postgres/install_generator.rb +1 -1
- data/lib/active_postgres/rails/database_config.rb +5 -1
- data/lib/active_postgres/secrets.rb +18 -0
- data/lib/active_postgres/ssh_executor.rb +1 -0
- data/lib/active_postgres/version.rb +1 -1
- data/templates/repmgr.conf.erb +7 -3
- data/templates/repmgr_dns_failover.sh.erb +9 -5
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 12db7ee31476f31cae2fa436ade1c6fe3a94367b5013765603442df5fdc90feb
|
|
4
|
+
data.tar.gz: e31aa6b40e45b90b2c985e5347078e4886385e2a26c359c87ce5c951adf04eb9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7ea49e121a8f72d4a8b271e5a270a7a25ce8ac5141d301ef623234b6951fb2874495bbf7d00e484df148e5be254cdbe12f1f54e677b81ed608326dd85a1d478e
|
|
7
|
+
data.tar.gz: 468c8eb9c90f8c51dbc666c81f89a3a0bae4477577309f82a49126fedec617bdbc228585bfe00cae7448e3de76f5fb587a1bf56748802c4243107c2720211902
|
data/README.md
CHANGED
|
@@ -187,12 +187,10 @@ rake postgres:backup:restore_at["2026-01-29 01:15:00",promote]
|
|
|
187
187
|
```
|
|
188
188
|
|
|
189
189
|
If you want the old primary to rejoin without a full re-clone after failover,
|
|
190
|
-
|
|
190
|
+
use `repmgr node rejoin --force-rewind` once the node is ready to reattach:
|
|
191
191
|
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
repmgr:
|
|
195
|
-
use_rewind: true
|
|
192
|
+
```bash
|
|
193
|
+
repmgr node rejoin -f /etc/repmgr.conf --force-rewind
|
|
196
194
|
```
|
|
197
195
|
|
|
198
196
|
### Stable app endpoint with PgBouncer
|
|
@@ -49,6 +49,11 @@ module ActivePostgres
|
|
|
49
49
|
create_app_user_and_database(config.primary_host)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
def install_packages_only(host)
|
|
53
|
+
puts " Installing packages on #{host} (cluster will be created by repmgr)..."
|
|
54
|
+
ssh_executor.install_postgres(host, config.version)
|
|
55
|
+
end
|
|
56
|
+
|
|
52
57
|
private
|
|
53
58
|
|
|
54
59
|
def install_on_host(host, is_primary:)
|
|
@@ -96,11 +101,6 @@ module ActivePostgres
|
|
|
96
101
|
optimal_settings.merge(user_postgresql)
|
|
97
102
|
end
|
|
98
103
|
|
|
99
|
-
def install_packages_only(host)
|
|
100
|
-
puts " Installing packages on #{host} (cluster will be created by repmgr)..."
|
|
101
|
-
ssh_executor.install_postgres(host, config.version)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
104
|
def cluster_exists?(host)
|
|
105
105
|
exists = false
|
|
106
106
|
version = config.version
|
|
@@ -141,6 +141,7 @@ module ActivePostgres
|
|
|
141
141
|
end
|
|
142
142
|
|
|
143
143
|
def setup_backup_schedules(host, pgbackrest_config)
|
|
144
|
+
remove_backup_schedule(host)
|
|
144
145
|
schedules = backup_schedules(pgbackrest_config)
|
|
145
146
|
schedules.each do |entry|
|
|
146
147
|
setup_backup_schedule(host, entry[:schedule], entry[:type], entry[:file])
|
|
@@ -99,6 +99,8 @@ module ActivePostgres
|
|
|
99
99
|
|
|
100
100
|
create_userlist(host)
|
|
101
101
|
|
|
102
|
+
ensure_firewall_port_open(host, pgbouncer_config[:listen_port] || 6432)
|
|
103
|
+
|
|
102
104
|
ssh_executor.execute_on_host(host) do
|
|
103
105
|
execute :sudo, 'systemctl', 'enable', 'pgbouncer'
|
|
104
106
|
execute :sudo, 'systemctl', 'restart', 'pgbouncer'
|
|
@@ -194,6 +196,17 @@ module ActivePostgres
|
|
|
194
196
|
end
|
|
195
197
|
end
|
|
196
198
|
|
|
199
|
+
def ensure_firewall_port_open(host, port)
|
|
200
|
+
ssh_executor.execute_on_host(host) do
|
|
201
|
+
has_reject = test(:sudo, 'iptables', '-C', 'INPUT', '-j', 'REJECT', '2>/dev/null')
|
|
202
|
+
if has_reject
|
|
203
|
+
execute :sudo, 'iptables', '-I', 'INPUT', '-p', 'tcp', '--dport', port.to_s, '-j', 'ACCEPT'
|
|
204
|
+
execute :sudo, 'sh', '-c', "'iptables-save > /etc/iptables/rules.v4 2>/dev/null || true'"
|
|
205
|
+
puts " ✓ Opened port #{port} in iptables"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
197
210
|
def install_follow_primary(host, pgbouncer_config)
|
|
198
211
|
interval = pgbouncer_config[:follow_primary_interval] || 5
|
|
199
212
|
interval = interval.to_i
|
|
@@ -85,6 +85,22 @@ module ActivePostgres
|
|
|
85
85
|
execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq',
|
|
86
86
|
"postgresql-#{version}-repmgr"
|
|
87
87
|
end
|
|
88
|
+
install_postgres_sudoers(host)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def install_postgres_sudoers(host)
|
|
93
|
+
version = config.version
|
|
94
|
+
sudoers_line = "postgres ALL=(ALL) NOPASSWD: /usr/bin/systemctl start postgresql@#{version}-main, " \
|
|
95
|
+
"/usr/bin/systemctl stop postgresql@#{version}-main, " \
|
|
96
|
+
"/usr/bin/systemctl restart postgresql@#{version}-main, " \
|
|
97
|
+
"/usr/bin/systemctl reload postgresql@#{version}-main, " \
|
|
98
|
+
"/usr/bin/systemctl status postgresql@#{version}-main"
|
|
99
|
+
ssh_executor.execute_on_host(host) do
|
|
100
|
+
upload! StringIO.new("#{sudoers_line}\n"), '/tmp/postgres-repmgr-sudoers'
|
|
101
|
+
execute :sudo, 'cp', '/tmp/postgres-repmgr-sudoers', '/etc/sudoers.d/postgres-repmgr'
|
|
102
|
+
execute :sudo, 'chmod', '440', '/etc/sudoers.d/postgres-repmgr'
|
|
103
|
+
execute :rm, '-f', '/tmp/postgres-repmgr-sudoers'
|
|
88
104
|
end
|
|
89
105
|
end
|
|
90
106
|
|
|
@@ -247,6 +263,7 @@ module ActivePostgres
|
|
|
247
263
|
effective_replication_password = replication_user == repmgr_user ? repmgr_password : replication_password
|
|
248
264
|
|
|
249
265
|
ensure_primary_registered
|
|
266
|
+
ensure_primary_replication_ready(repmgr_password, effective_replication_password)
|
|
250
267
|
|
|
251
268
|
setup_pgpass_file(standby_host, repmgr_password, replication_password: effective_replication_password,
|
|
252
269
|
primary_ip: primary_replication_host)
|
|
@@ -399,9 +416,42 @@ module ActivePostgres
|
|
|
399
416
|
end
|
|
400
417
|
|
|
401
418
|
register_standby_with_primary(standby_host)
|
|
419
|
+
setup_inter_node_ssh
|
|
402
420
|
enable_repmgrd_if_configured(standby_host, repmgr_config)
|
|
403
421
|
end
|
|
404
422
|
|
|
423
|
+
def setup_inter_node_ssh
|
|
424
|
+
dns_config = dns_failover_config
|
|
425
|
+
key_path = dns_config && dns_config[:ssh_key_path] || '/var/lib/postgresql/.ssh/active_postgres_dns'
|
|
426
|
+
postgres_user = config.postgres_user
|
|
427
|
+
all_hosts = config.all_hosts
|
|
428
|
+
pub_keys = {}
|
|
429
|
+
|
|
430
|
+
all_hosts.each do |host|
|
|
431
|
+
ssh_executor.execute_on_host(host) do
|
|
432
|
+
pub_keys[host] = capture(:sudo, '-u', postgres_user, 'cat', "#{key_path}.pub").strip
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
all_hosts.each do |host|
|
|
437
|
+
ssh_executor.execute_on_host(host) do
|
|
438
|
+
other_keys = pub_keys.reject { |h, _| h == host }.values
|
|
439
|
+
other_keys.each do |key|
|
|
440
|
+
next if key.to_s.empty?
|
|
441
|
+
|
|
442
|
+
upload! StringIO.new("#{key}\n"), '/tmp/pg_peer_key.pub'
|
|
443
|
+
execute :sudo, '-u', postgres_user, 'bash', '-c',
|
|
444
|
+
"grep -qxF -f /tmp/pg_peer_key.pub /var/lib/postgresql/.ssh/authorized_keys 2>/dev/null || cat /tmp/pg_peer_key.pub >> /var/lib/postgresql/.ssh/authorized_keys"
|
|
445
|
+
execute :rm, '-f', '/tmp/pg_peer_key.pub'
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
peer_ips = all_hosts.reject { |h| h == host }.map { |h| config.replication_host_for(h) }
|
|
449
|
+
scan_cmd = "ssh-keyscan #{peer_ips.join(' ')} >> /var/lib/postgresql/.ssh/known_hosts 2>/dev/null || true"
|
|
450
|
+
execute :sudo, '-u', postgres_user, 'bash', '-c', scan_cmd
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
405
455
|
def setup_dns_failover
|
|
406
456
|
dns_config = dns_failover_config
|
|
407
457
|
return unless dns_config
|
|
@@ -424,6 +474,8 @@ module ActivePostgres
|
|
|
424
474
|
authorize_dns_keys(dns_server, dns_user, pub_keys.values.compact)
|
|
425
475
|
end
|
|
426
476
|
|
|
477
|
+
setup_inter_node_ssh
|
|
478
|
+
|
|
427
479
|
config.all_hosts.each do |host|
|
|
428
480
|
install_dns_failover_script(host, dns_config, dns_private_ips, dns_user, dns_ssh_key_path, ssh_strict_host_key)
|
|
429
481
|
end
|
|
@@ -876,6 +928,33 @@ module ActivePostgres
|
|
|
876
928
|
is_registered
|
|
877
929
|
end
|
|
878
930
|
|
|
931
|
+
def ensure_primary_replication_ready(repmgr_password, effective_replication_password)
|
|
932
|
+
host = config.primary_host
|
|
933
|
+
repmgr_user = config.repmgr_user
|
|
934
|
+
repmgr_db = config.repmgr_database
|
|
935
|
+
replication_user = config.replication_user
|
|
936
|
+
repmgr_component = self
|
|
937
|
+
executor = ssh_executor
|
|
938
|
+
|
|
939
|
+
puts ' Ensuring repmgr user has correct password and privileges on primary...'
|
|
940
|
+
|
|
941
|
+
ssh_executor.execute_on_host(host) do
|
|
942
|
+
repmgr_sql = repmgr_component.send(:build_repmgr_setup_sql, repmgr_user, repmgr_db, repmgr_password)
|
|
943
|
+
executor.run_sql_on_backend(self, repmgr_sql, postgres_user: 'postgres', port: 5432, tuples_only: false,
|
|
944
|
+
capture: false)
|
|
945
|
+
|
|
946
|
+
if replication_user != repmgr_user
|
|
947
|
+
repl_sql = repmgr_component.send(:build_replication_user_sql, replication_user, effective_replication_password)
|
|
948
|
+
executor.run_sql_on_backend(self, repl_sql, postgres_user: 'postgres', port: 5432, tuples_only: false,
|
|
949
|
+
capture: false)
|
|
950
|
+
end
|
|
951
|
+
|
|
952
|
+
info '✓ Primary replication user is ready'
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
setup_pgpass_file(host, repmgr_password, replication_password: effective_replication_password)
|
|
956
|
+
end
|
|
957
|
+
|
|
879
958
|
def regenerate_ssl_certs(host, version)
|
|
880
959
|
ssl_config = config.component_config(:ssl)
|
|
881
960
|
ssl_cert = secrets.resolve('ssl_cert')
|
|
@@ -1011,9 +1090,9 @@ module ActivePostgres
|
|
|
1011
1090
|
'DO $$',
|
|
1012
1091
|
'BEGIN',
|
|
1013
1092
|
" IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '#{repmgr_user}') THEN",
|
|
1014
|
-
" CREATE USER #{repmgr_user} WITH SUPERUSER PASSWORD '#{escaped_password}';",
|
|
1093
|
+
" CREATE USER #{repmgr_user} WITH SUPERUSER REPLICATION PASSWORD '#{escaped_password}';",
|
|
1015
1094
|
' ELSE',
|
|
1016
|
-
" ALTER USER #{repmgr_user} WITH SUPERUSER PASSWORD '#{escaped_password}';",
|
|
1095
|
+
" ALTER USER #{repmgr_user} WITH SUPERUSER REPLICATION PASSWORD '#{escaped_password}';",
|
|
1017
1096
|
' END IF;',
|
|
1018
1097
|
'END $$;',
|
|
1019
1098
|
'',
|
|
@@ -82,7 +82,7 @@ module ActivePostgres
|
|
|
82
82
|
{
|
|
83
83
|
'adapter' => 'postgresql',
|
|
84
84
|
'encoding' => 'unicode',
|
|
85
|
-
'pool' =>
|
|
85
|
+
'pool' => pool_env_value,
|
|
86
86
|
'username' => env_value('POSTGRES_APP_USER', fallback: app_name),
|
|
87
87
|
'password' => env_value('POSTGRES_APP_PASSWORD'),
|
|
88
88
|
'variables' => {
|
|
@@ -138,6 +138,10 @@ module ActivePostgres
|
|
|
138
138
|
end
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
+
def pool_env_value
|
|
142
|
+
"<%= ENV.fetch('DB_POOL') { ENV.fetch('RAILS_MAX_THREADS') { 5 } } %>"
|
|
143
|
+
end
|
|
144
|
+
|
|
141
145
|
def normalize_app_name(custom_name)
|
|
142
146
|
chosen = custom_name || ENV['BORING_APP_NAME'] || default_app_name
|
|
143
147
|
chosen.to_s.tr('- ', '_')
|
|
@@ -81,6 +81,8 @@ module ActivePostgres
|
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
def fetch_from_rails_credentials(key_path)
|
|
84
|
+
ensure_rails_credentials_available
|
|
85
|
+
|
|
84
86
|
return nil unless defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
|
|
85
87
|
|
|
86
88
|
keys = key_path.split('.').map(&:to_sym)
|
|
@@ -89,6 +91,22 @@ module ActivePostgres
|
|
|
89
91
|
nil
|
|
90
92
|
end
|
|
91
93
|
|
|
94
|
+
def ensure_rails_credentials_available
|
|
95
|
+
return if @rails_boot_attempted
|
|
96
|
+
return if defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
|
|
97
|
+
|
|
98
|
+
@rails_boot_attempted = true
|
|
99
|
+
|
|
100
|
+
# Try loading just the Rails application (without full initialization)
|
|
101
|
+
# This makes Rails.application available for credential access
|
|
102
|
+
if File.exist?('./config/application.rb')
|
|
103
|
+
require './config/application'
|
|
104
|
+
end
|
|
105
|
+
rescue StandardError
|
|
106
|
+
# Rails boot failed — CLI running outside a Rails project
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
92
110
|
def execute_command(command)
|
|
93
111
|
# Preserve RAILS_ENV if set
|
|
94
112
|
env_prefix = ENV['RAILS_ENV'] ? "RAILS_ENV=#{ENV['RAILS_ENV']} " : ''
|
data/templates/repmgr.conf.erb
CHANGED
|
@@ -30,9 +30,13 @@ follow_command='repmgr standby follow -f /etc/repmgr.conf --upstream-node-id=%n'
|
|
|
30
30
|
|
|
31
31
|
reconnect_attempts=<%= repmgr_config[:reconnect_attempts] || 6 %>
|
|
32
32
|
reconnect_interval=<%= repmgr_config[:reconnect_interval] || 10 %>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
|
|
34
|
+
service_start_command='sudo systemctl start postgresql@<%= config.version %>-main'
|
|
35
|
+
service_stop_command='sudo systemctl stop postgresql@<%= config.version %>-main'
|
|
36
|
+
service_restart_command='sudo systemctl restart postgresql@<%= config.version %>-main'
|
|
37
|
+
service_reload_command='sudo systemctl reload postgresql@<%= config.version %>-main'
|
|
38
|
+
|
|
39
|
+
ssh_options='-i /var/lib/postgresql/.ssh/active_postgres_dns -o StrictHostKeyChecking=no'
|
|
36
40
|
|
|
37
41
|
log_level=INFO
|
|
38
42
|
log_facility=STDERR
|
|
@@ -12,13 +12,17 @@ SSH_STRICT_HOST_KEY="<%= ssh_strict_host_key %>"
|
|
|
12
12
|
SSH_KNOWN_HOSTS="/var/lib/postgresql/.ssh/known_hosts"
|
|
13
13
|
LOG_TAG="active_postgres_dns"
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
REPMGR_DB=$(grep -oP "dbname=\K[^ ']+" "$REPMGR_CONF" | head -1)
|
|
16
|
+
REPMGR_USER=$(grep -oP "user=\K[^ ']+" "$REPMGR_CONF" 2>/dev/null | head -1)
|
|
17
|
+
REPMGR_USER="${REPMGR_USER:-repmgr}"
|
|
18
|
+
|
|
19
|
+
cluster_data=$(sudo -u postgres psql -h /var/run/postgresql -d "$REPMGR_DB" -tAF',' -c "SELECT type, conninfo FROM repmgr.nodes WHERE active = true" 2>/dev/null || true)
|
|
20
|
+
if [[ -z "$cluster_data" ]]; then
|
|
17
21
|
exit 0
|
|
18
22
|
fi
|
|
19
23
|
|
|
20
|
-
primary_host=$(printf "%s\n" "$
|
|
21
|
-
standby_hosts=$(printf "%s\n" "$
|
|
24
|
+
primary_host=$(printf "%s\n" "$cluster_data" | awk -F',' '$1 == "primary" {print $2; exit}' | sed -n 's/.*host=\([^ ]*\).*/\1/p')
|
|
25
|
+
standby_hosts=$(printf "%s\n" "$cluster_data" | awk -F',' '$1 == "standby" {print $2}' | sed -n 's/.*host=\([^ ]*\).*/\1/p' | sort -u)
|
|
22
26
|
|
|
23
27
|
if [[ -z "$primary_host" ]]; then
|
|
24
28
|
exit 0
|
|
@@ -51,7 +55,7 @@ for server in "${DNS_SERVERS[@]}"; do
|
|
|
51
55
|
fi
|
|
52
56
|
|
|
53
57
|
if ! /usr/bin/ssh "${ssh_opts[@]}" "${DNS_USER}@${server}" \
|
|
54
|
-
"sudo bash -c 'cat > ${DNSMASQ_FILE} << \"EOF\"\\n${content}EOF\\n' &&
|
|
58
|
+
"sudo bash -c 'cat > ${DNSMASQ_FILE} << \"EOF\"\\n${content}EOF\\n' && sudo systemctl restart dnsmasq"; then
|
|
55
59
|
/usr/bin/logger -t "$LOG_TAG" "Failed updating dnsmasq on ${server}"
|
|
56
60
|
fi
|
|
57
61
|
done
|