active_postgres 0.4.0 → 0.6.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/lib/active_postgres/cluster_deployment_flow.rb +10 -0
- data/lib/active_postgres/components/core.rb +100 -3
- data/lib/active_postgres/components/pgbackrest.rb +16 -5
- data/lib/active_postgres/components/pgbouncer.rb +29 -7
- data/lib/active_postgres/components/ssl.rb +19 -7
- data/lib/active_postgres/generators/active_postgres/templates/database.active_postgres.yml.erb +1 -0
- data/lib/active_postgres/generators/active_postgres/templates/postgres.yml.erb +85 -0
- data/lib/active_postgres/health_checker.rb +81 -6
- data/lib/active_postgres/ssh_executor.rb +3 -1
- data/lib/active_postgres/standby_deployment_flow.rb +1 -1
- data/lib/active_postgres/version.rb +1 -1
- data/lib/tasks/postgres.rake +30 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cccb8eff8b3ff2ae0f3413bef89b81a1b5b487e474222a9ff3e85fc74e57c5e9
|
|
4
|
+
data.tar.gz: 05a21ba9f1a29b6b5d451b3c059fd6a15135b8d6ecd7bad7d6232e6c30a5d63a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '05819a2a73506fb087831e9ad69b6901b8bd6f14f0559a8e08d8c7fbbbff5c20eabc616bf0c862dd95757dab18a52666d8c0c1c00a63dc23b2094849160ce80b'
|
|
7
|
+
data.tar.gz: 2733384099851358c38788c41f77768e7a781cd99b7492aa4c10c85758a520167ad954657e0842d7b55681b2f932515933e887706ba3e2d39a50c44fdbd1e7e5
|
|
@@ -54,6 +54,9 @@ module ActivePostgres
|
|
|
54
54
|
|
|
55
55
|
# Create application users AFTER repmgr to avoid being wiped by cluster recreation
|
|
56
56
|
create_application_users_if_configured
|
|
57
|
+
|
|
58
|
+
# Update pgbouncer userlist AFTER app users are created so they can authenticate
|
|
59
|
+
update_pgbouncer_userlist if config.component_enabled?(:pgbouncer)
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
def list_next_steps
|
|
@@ -81,5 +84,12 @@ module ActivePostgres
|
|
|
81
84
|
core_component = Components::Core.new(config, ssh_executor, secrets)
|
|
82
85
|
core_component.create_application_users
|
|
83
86
|
end
|
|
87
|
+
|
|
88
|
+
def update_pgbouncer_userlist
|
|
89
|
+
logger.task('Updating PgBouncer userlist with app users') do
|
|
90
|
+
component = Components::PgBouncer.new(config, ssh_executor, secrets)
|
|
91
|
+
component.update_userlist
|
|
92
|
+
end
|
|
93
|
+
end
|
|
84
94
|
end
|
|
85
95
|
end
|
|
@@ -11,11 +11,16 @@ module ActivePostgres
|
|
|
11
11
|
# See: create_application_user_and_database method called from deployment flow
|
|
12
12
|
|
|
13
13
|
# Install on standbys
|
|
14
|
-
# If repmgr is enabled, only install packages (cluster will be cloned by repmgr)
|
|
15
|
-
# If repmgr is disabled, install everything including cluster creation
|
|
16
14
|
config.standby_hosts.each do |host|
|
|
17
15
|
if config.component_enabled?(:repmgr)
|
|
18
|
-
|
|
16
|
+
# Check if cluster already exists (config update vs fresh install)
|
|
17
|
+
if cluster_exists?(host)
|
|
18
|
+
# Existing cluster - just update configs
|
|
19
|
+
update_configs_on_host(host)
|
|
20
|
+
else
|
|
21
|
+
# Fresh install - only install packages, repmgr will clone the cluster
|
|
22
|
+
install_packages_only(host)
|
|
23
|
+
end
|
|
19
24
|
else
|
|
20
25
|
install_on_host(host, is_primary: false)
|
|
21
26
|
end
|
|
@@ -62,6 +67,10 @@ module ActivePostgres
|
|
|
62
67
|
else
|
|
63
68
|
component_config[:postgresql] || {}
|
|
64
69
|
end
|
|
70
|
+
|
|
71
|
+
# Substitute ${private_ip} with the host's actual private IP
|
|
72
|
+
private_ip = config.replication_host_for(host)
|
|
73
|
+
pg_config = substitute_private_ip(pg_config, private_ip)
|
|
65
74
|
_ = pg_config # Used in ERB template
|
|
66
75
|
|
|
67
76
|
upload_template(host, 'postgresql.conf.erb', "/etc/postgresql/#{config.version}/main/postgresql.conf", binding,
|
|
@@ -87,11 +96,99 @@ module ActivePostgres
|
|
|
87
96
|
optimal_settings.merge(user_postgresql)
|
|
88
97
|
end
|
|
89
98
|
|
|
99
|
+
def substitute_private_ip(pg_config, private_ip)
|
|
100
|
+
pg_config.transform_values do |value|
|
|
101
|
+
if value.is_a?(String)
|
|
102
|
+
value.gsub('${private_ip}', private_ip)
|
|
103
|
+
else
|
|
104
|
+
value
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
90
109
|
def install_packages_only(host)
|
|
91
110
|
puts " Installing packages on #{host} (cluster will be created by repmgr)..."
|
|
92
111
|
ssh_executor.install_postgres(host, config.version)
|
|
93
112
|
end
|
|
94
113
|
|
|
114
|
+
def cluster_exists?(host)
|
|
115
|
+
exists = false
|
|
116
|
+
version = config.version
|
|
117
|
+
ssh_executor.execute_on_host(host) do
|
|
118
|
+
exists = test(:sudo, 'test', '-d', "/var/lib/postgresql/#{version}/main/base")
|
|
119
|
+
end
|
|
120
|
+
exists
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def update_configs_on_host(host)
|
|
124
|
+
puts " Updating configs on #{host}..."
|
|
125
|
+
|
|
126
|
+
component_config = config.component_config(:core)
|
|
127
|
+
|
|
128
|
+
pg_config = if config.component_enabled?(:performance_tuning)
|
|
129
|
+
tuned = calculate_tuned_settings(host, component_config)
|
|
130
|
+
# Standbys must have replication-critical settings >= primary
|
|
131
|
+
ensure_standby_compatible(tuned)
|
|
132
|
+
else
|
|
133
|
+
component_config[:postgresql] || {}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private_ip = config.replication_host_for(host)
|
|
137
|
+
pg_config = substitute_private_ip(pg_config, private_ip)
|
|
138
|
+
_ = pg_config
|
|
139
|
+
|
|
140
|
+
upload_template(host, 'postgresql.conf.erb', "/etc/postgresql/#{config.version}/main/postgresql.conf", binding,
|
|
141
|
+
owner: 'postgres:postgres')
|
|
142
|
+
upload_template(host, 'pg_hba.conf.erb', "/etc/postgresql/#{config.version}/main/pg_hba.conf", binding,
|
|
143
|
+
owner: 'postgres:postgres')
|
|
144
|
+
|
|
145
|
+
ssh_executor.restart_postgres(host, config.version)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def ensure_standby_compatible(pg_config)
|
|
149
|
+
primary_settings = get_primary_replication_settings
|
|
150
|
+
return pg_config if primary_settings.empty?
|
|
151
|
+
|
|
152
|
+
adjustments = []
|
|
153
|
+
|
|
154
|
+
%i[max_connections max_worker_processes max_wal_senders
|
|
155
|
+
max_prepared_transactions max_locks_per_transaction].each do |setting|
|
|
156
|
+
primary_val = primary_settings[setting]
|
|
157
|
+
next unless primary_val
|
|
158
|
+
|
|
159
|
+
current_val = pg_config[setting]
|
|
160
|
+
if current_val.nil? || current_val.to_i < primary_val.to_i
|
|
161
|
+
adjustments << "#{setting}: #{current_val || 'unset'} → #{primary_val}"
|
|
162
|
+
pg_config[setting] = primary_val
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
if adjustments.any?
|
|
167
|
+
puts " ⚠️ Adjusting standby settings to match primary minimum:"
|
|
168
|
+
adjustments.each { |adj| puts " #{adj}" }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
pg_config
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def get_primary_replication_settings
|
|
175
|
+
@primary_replication_settings ||= begin
|
|
176
|
+
settings = {}
|
|
177
|
+
sql = "SELECT name || '=' || setting FROM pg_settings WHERE name IN ('max_connections', 'max_worker_processes', 'max_wal_senders', 'max_prepared_transactions', 'max_locks_per_transaction')"
|
|
178
|
+
result = ssh_executor.run_sql(config.primary_host, sql)
|
|
179
|
+
result.strip.split("\n").each do |line|
|
|
180
|
+
name, val = line.split('=')
|
|
181
|
+
next unless name && val
|
|
182
|
+
|
|
183
|
+
settings[name.strip.to_sym] = val.strip.to_i
|
|
184
|
+
end
|
|
185
|
+
settings
|
|
186
|
+
rescue StandardError => e
|
|
187
|
+
puts " Warning: Could not get primary settings: #{e.message}"
|
|
188
|
+
{}
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
95
192
|
def create_app_user_and_database(host)
|
|
96
193
|
app_user = config.app_user
|
|
97
194
|
app_database = config.app_database
|
|
@@ -4,14 +4,22 @@ module ActivePostgres
|
|
|
4
4
|
def install
|
|
5
5
|
puts 'Installing pgBackRest for backups...'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
# Install on primary with full setup (stanza-create)
|
|
8
|
+
install_on_host(config.primary_host, create_stanza: true)
|
|
9
|
+
|
|
10
|
+
# Install on standbys (package + config only, no stanza-create)
|
|
11
|
+
config.standby_hosts.each do |host|
|
|
12
|
+
install_on_host(host, create_stanza: false)
|
|
13
|
+
end
|
|
8
14
|
end
|
|
9
15
|
|
|
10
16
|
def uninstall
|
|
11
17
|
puts 'Uninstalling pgBackRest...'
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
config.all_hosts.each do |host|
|
|
20
|
+
ssh_executor.execute_on_host(host) do
|
|
21
|
+
execute :sudo, 'apt-get', 'remove', '-y', 'pgbackrest'
|
|
22
|
+
end
|
|
15
23
|
end
|
|
16
24
|
end
|
|
17
25
|
|
|
@@ -55,7 +63,7 @@ module ActivePostgres
|
|
|
55
63
|
|
|
56
64
|
private
|
|
57
65
|
|
|
58
|
-
def install_on_host(host)
|
|
66
|
+
def install_on_host(host, create_stanza: true)
|
|
59
67
|
puts " Installing pgBackRest on #{host}..."
|
|
60
68
|
|
|
61
69
|
pgbackrest_config = config.component_config(:pgbackrest)
|
|
@@ -86,7 +94,10 @@ module ActivePostgres
|
|
|
86
94
|
execute :sudo, 'mkdir', '-p', '/var/spool/pgbackrest'
|
|
87
95
|
execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", '/var/spool/pgbackrest'
|
|
88
96
|
|
|
89
|
-
|
|
97
|
+
# Only create stanza on primary - standbys share the same backup repo
|
|
98
|
+
if create_stanza
|
|
99
|
+
execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', 'stanza-create'
|
|
100
|
+
end
|
|
90
101
|
end
|
|
91
102
|
end
|
|
92
103
|
end
|
|
@@ -4,27 +4,49 @@ module ActivePostgres
|
|
|
4
4
|
def install
|
|
5
5
|
puts 'Installing PgBouncer for connection pooling...'
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
config.all_hosts.each do |host|
|
|
8
|
+
install_on_host(host)
|
|
9
|
+
end
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def uninstall
|
|
12
13
|
puts 'Uninstalling PgBouncer...'
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
config.all_hosts.each do |host|
|
|
16
|
+
ssh_executor.execute_on_host(host) do
|
|
17
|
+
execute :sudo, 'systemctl', 'stop', 'pgbouncer'
|
|
18
|
+
execute :sudo, 'apt-get', 'remove', '-y', 'pgbouncer'
|
|
19
|
+
end
|
|
17
20
|
end
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def restart
|
|
21
24
|
puts 'Restarting PgBouncer...'
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
config.all_hosts.each do |host|
|
|
27
|
+
ssh_executor.execute_on_host(host) do
|
|
28
|
+
execute :sudo, 'systemctl', 'restart', 'pgbouncer'
|
|
29
|
+
end
|
|
25
30
|
end
|
|
26
31
|
end
|
|
27
32
|
|
|
33
|
+
def update_userlist
|
|
34
|
+
puts 'Updating PgBouncer userlist on all hosts...'
|
|
35
|
+
|
|
36
|
+
config.all_hosts.each do |host|
|
|
37
|
+
create_userlist(host)
|
|
38
|
+
|
|
39
|
+
ssh_executor.execute_on_host(host) do
|
|
40
|
+
execute :sudo, 'systemctl', 'reload', 'pgbouncer'
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def install_on_standby(standby_host)
|
|
46
|
+
puts "Installing PgBouncer on standby #{standby_host}..."
|
|
47
|
+
install_on_host(standby_host)
|
|
48
|
+
end
|
|
49
|
+
|
|
28
50
|
private
|
|
29
51
|
|
|
30
52
|
def install_on_host(host)
|
|
@@ -14,7 +14,6 @@ module ActivePostgres
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def restart
|
|
17
|
-
# SSL doesn't have its own service, restart PostgreSQL
|
|
18
17
|
ssh_executor.restart_postgres(config.primary_host)
|
|
19
18
|
|
|
20
19
|
config.standby_hosts.each do |host|
|
|
@@ -37,7 +36,6 @@ module ActivePostgres
|
|
|
37
36
|
|
|
38
37
|
ssh_executor.ensure_postgres_user(host)
|
|
39
38
|
|
|
40
|
-
# Ensure the PostgreSQL config directory exists
|
|
41
39
|
ssh_executor.execute_on_host(host) do
|
|
42
40
|
execute :sudo, 'mkdir', '-p', "/etc/postgresql/#{version}/main"
|
|
43
41
|
execute :sudo, 'chown', 'postgres:postgres', "/etc/postgresql/#{version}/main"
|
|
@@ -47,17 +45,31 @@ module ActivePostgres
|
|
|
47
45
|
ssl_key = secrets.resolve('ssl_key')
|
|
48
46
|
|
|
49
47
|
if ssl_cert && ssl_key
|
|
50
|
-
|
|
51
|
-
ssh_executor.upload_file(host, ssl_cert, "/etc/postgresql/#{version}/main/server.crt", mode: '644',
|
|
52
|
-
owner: 'postgres:postgres')
|
|
53
|
-
ssh_executor.upload_file(host, ssl_key, "/etc/postgresql/#{version}/main/server.key", mode: '600',
|
|
54
|
-
owner: 'postgres:postgres')
|
|
48
|
+
install_custom_cert(host, ssl_cert, ssl_key, ssl_config)
|
|
55
49
|
else
|
|
56
50
|
puts ' Generating self-signed SSL certificates...'
|
|
57
51
|
generate_self_signed_cert(host, ssl_config)
|
|
58
52
|
end
|
|
59
53
|
end
|
|
60
54
|
|
|
55
|
+
def install_custom_cert(host, ssl_cert, ssl_key, _ssl_config)
|
|
56
|
+
version = config.version
|
|
57
|
+
ssl_chain = secrets.resolve('ssl_chain')
|
|
58
|
+
|
|
59
|
+
puts ' Using SSL certificates from secrets...'
|
|
60
|
+
|
|
61
|
+
full_cert = if ssl_chain
|
|
62
|
+
"#{ssl_cert.strip}\n#{ssl_chain.strip}\n"
|
|
63
|
+
else
|
|
64
|
+
ssl_cert
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
ssh_executor.upload_file(host, full_cert, "/etc/postgresql/#{version}/main/server.crt",
|
|
68
|
+
mode: '644', owner: 'postgres:postgres')
|
|
69
|
+
ssh_executor.upload_file(host, ssl_key, "/etc/postgresql/#{version}/main/server.key",
|
|
70
|
+
mode: '600', owner: 'postgres:postgres')
|
|
71
|
+
end
|
|
72
|
+
|
|
61
73
|
def generate_self_signed_cert(host, ssl_config)
|
|
62
74
|
version = config.version
|
|
63
75
|
cert_path = "/etc/postgresql/#{version}/main/server.crt"
|
data/lib/active_postgres/generators/active_postgres/templates/database.active_postgres.yml.erb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= ActivePostgres::Rails::DatabaseConfig.render_partial('production', app_name: boring_app_name).strip %>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# PostgreSQL High Availability Configuration
|
|
2
|
+
# Generated by active_postgres
|
|
3
|
+
#
|
|
4
|
+
# 💡 Either:
|
|
5
|
+
# 1. Edit this file manually with your database servers
|
|
6
|
+
# 2. Use Terraform to auto-generate this file
|
|
7
|
+
#
|
|
8
|
+
# See: config/postgres.example.yml in gem for full examples
|
|
9
|
+
|
|
10
|
+
shared: &shared
|
|
11
|
+
version: 18
|
|
12
|
+
user: ubuntu
|
|
13
|
+
ssh_key: ~/.ssh/id_rsa
|
|
14
|
+
|
|
15
|
+
components:
|
|
16
|
+
core:
|
|
17
|
+
locale: en_US.UTF-8
|
|
18
|
+
encoding: UTF8
|
|
19
|
+
# Optional: Override default PostgreSQL user and app database config
|
|
20
|
+
# postgres_user: postgres
|
|
21
|
+
# app_user: app
|
|
22
|
+
# app_database: app_production
|
|
23
|
+
postgresql:
|
|
24
|
+
listen_addresses: '*'
|
|
25
|
+
port: 5432
|
|
26
|
+
max_connections: 100
|
|
27
|
+
shared_buffers: 256MB
|
|
28
|
+
|
|
29
|
+
repmgr:
|
|
30
|
+
enabled: false
|
|
31
|
+
# Optional: Override default repmgr user/database
|
|
32
|
+
# user: repmgr
|
|
33
|
+
# database: repmgr
|
|
34
|
+
|
|
35
|
+
pgbouncer:
|
|
36
|
+
enabled: false
|
|
37
|
+
# Optional: Override default pgbouncer user
|
|
38
|
+
# user: pgbouncer
|
|
39
|
+
|
|
40
|
+
pgbackrest:
|
|
41
|
+
enabled: false
|
|
42
|
+
|
|
43
|
+
monitoring:
|
|
44
|
+
enabled: false
|
|
45
|
+
|
|
46
|
+
ssl:
|
|
47
|
+
enabled: false
|
|
48
|
+
|
|
49
|
+
development:
|
|
50
|
+
<<: *shared
|
|
51
|
+
primary:
|
|
52
|
+
host: localhost
|
|
53
|
+
port: 5432
|
|
54
|
+
|
|
55
|
+
production:
|
|
56
|
+
<<: *shared
|
|
57
|
+
|
|
58
|
+
# TODO: Configure your primary database server
|
|
59
|
+
# host: Public IP for SSH deployment (like Kamal)
|
|
60
|
+
# private_ip: Private/VPC IP for database connections (optional, falls back to host)
|
|
61
|
+
primary:
|
|
62
|
+
host: YOUR_PRIMARY_PUBLIC_IP
|
|
63
|
+
private_ip: YOUR_PRIMARY_PRIVATE_IP
|
|
64
|
+
label: us-east-1
|
|
65
|
+
|
|
66
|
+
# TODO: Add standby servers for HA (optional)
|
|
67
|
+
# standby:
|
|
68
|
+
# - host: YOUR_STANDBY_PUBLIC_IP
|
|
69
|
+
# private_ip: YOUR_STANDBY_PRIVATE_IP
|
|
70
|
+
# label: us-west-2
|
|
71
|
+
|
|
72
|
+
# Enable components as needed
|
|
73
|
+
components:
|
|
74
|
+
repmgr:
|
|
75
|
+
enabled: false # Enable for HA with standbys
|
|
76
|
+
|
|
77
|
+
# Secrets: Using Rails credentials (update via: rails credentials:edit)
|
|
78
|
+
# The rails_credentials: prefix fetches values from Rails.application.credentials
|
|
79
|
+
secrets:
|
|
80
|
+
superuser_password: rails_credentials:postgres.superuser_password
|
|
81
|
+
replication_password: rails_credentials:postgres.replication_password
|
|
82
|
+
repmgr_password: rails_credentials:postgres.repmgr_password
|
|
83
|
+
pgbouncer_password: rails_credentials:postgres.password
|
|
84
|
+
app_password: rails_credentials:postgres.password
|
|
85
|
+
|
|
@@ -51,6 +51,27 @@ module ActivePostgres
|
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# Check pgbouncer on all hosts
|
|
55
|
+
if config.component_enabled?(:pgbouncer)
|
|
56
|
+
puts
|
|
57
|
+
puts '==> Checking PgBouncer...'
|
|
58
|
+
config.all_hosts.each do |host|
|
|
59
|
+
print "PgBouncer (#{host})... "
|
|
60
|
+
pgbouncer_ok = check_pgbouncer_running(host)
|
|
61
|
+
userlist_ok = check_pgbouncer_userlist(host)
|
|
62
|
+
|
|
63
|
+
if pgbouncer_ok && userlist_ok
|
|
64
|
+
puts '✓'
|
|
65
|
+
elsif pgbouncer_ok && !userlist_ok
|
|
66
|
+
puts '✗ (missing app user in userlist)'
|
|
67
|
+
all_ok = false
|
|
68
|
+
else
|
|
69
|
+
puts '✗'
|
|
70
|
+
all_ok = false
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
54
75
|
puts
|
|
55
76
|
if all_ok
|
|
56
77
|
puts '✓ All checks passed'
|
|
@@ -99,7 +120,8 @@ module ActivePostgres
|
|
|
99
120
|
label: primary_config&.dig('label') || '-',
|
|
100
121
|
status: check_postgres_running(config.primary_host) ? '✓ running' : '✗ down',
|
|
101
122
|
connections: get_connection_count(config.primary_host),
|
|
102
|
-
lag: '-'
|
|
123
|
+
lag: '-',
|
|
124
|
+
pgbouncer: check_pgbouncer_status(config.primary_host)
|
|
103
125
|
}
|
|
104
126
|
|
|
105
127
|
# Standbys
|
|
@@ -114,7 +136,8 @@ module ActivePostgres
|
|
|
114
136
|
label: standby_config&.dig('label') || '-',
|
|
115
137
|
status: running ? '✓ streaming' : '✗ down',
|
|
116
138
|
connections: running ? get_connection_count(host) : 0,
|
|
117
|
-
lag: running ? get_replication_lag(host) : '-'
|
|
139
|
+
lag: running ? get_replication_lag(host) : '-',
|
|
140
|
+
pgbouncer: check_pgbouncer_status(host)
|
|
118
141
|
}
|
|
119
142
|
end
|
|
120
143
|
|
|
@@ -128,7 +151,7 @@ module ActivePostgres
|
|
|
128
151
|
end
|
|
129
152
|
|
|
130
153
|
def calculate_column_widths(nodes)
|
|
131
|
-
{
|
|
154
|
+
cols = {
|
|
132
155
|
role: [4, nodes.map { |n| n[:role].length }.max].max,
|
|
133
156
|
host: [4, nodes.map { |n| n[:host].length }.max].max,
|
|
134
157
|
private_ip: [10, nodes.map { |n| n[:private_ip].to_s.length }.max].max,
|
|
@@ -137,12 +160,25 @@ module ActivePostgres
|
|
|
137
160
|
conn: 5,
|
|
138
161
|
lag: [3, nodes.map { |n| n[:lag].to_s.length }.max].max
|
|
139
162
|
}
|
|
163
|
+
|
|
164
|
+
if config.component_enabled?(:pgbouncer)
|
|
165
|
+
cols[:pgbouncer] = [9, nodes.map { |n| n[:pgbouncer].to_s.length }.max].max
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
cols
|
|
140
169
|
end
|
|
141
170
|
|
|
142
171
|
def print_table_header(cols)
|
|
143
172
|
fmt = "%-#{cols[:role]}s %-#{cols[:host]}s %-#{cols[:private_ip]}s " \
|
|
144
173
|
"%-#{cols[:label]}s %-#{cols[:status]}s %#{cols[:conn]}s %#{cols[:lag]}s"
|
|
145
|
-
|
|
174
|
+
headers = %w[Role Host Private\ IP Label Status Conn Lag]
|
|
175
|
+
|
|
176
|
+
if cols[:pgbouncer]
|
|
177
|
+
fmt += " %-#{cols[:pgbouncer]}s"
|
|
178
|
+
headers << 'PgBouncer'
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
header = format(fmt, *headers)
|
|
146
182
|
puts header
|
|
147
183
|
puts '-' * header.length
|
|
148
184
|
end
|
|
@@ -150,8 +186,15 @@ module ActivePostgres
|
|
|
150
186
|
def print_table_row(node, cols)
|
|
151
187
|
fmt = "%-#{cols[:role]}s %-#{cols[:host]}s %-#{cols[:private_ip]}s " \
|
|
152
188
|
"%-#{cols[:label]}s %-#{cols[:status]}s %#{cols[:conn]}d %#{cols[:lag]}s"
|
|
153
|
-
|
|
154
|
-
|
|
189
|
+
values = [node[:role], node[:host], node[:private_ip], node[:label],
|
|
190
|
+
node[:status], node[:connections], node[:lag]]
|
|
191
|
+
|
|
192
|
+
if cols[:pgbouncer]
|
|
193
|
+
fmt += " %-#{cols[:pgbouncer]}s"
|
|
194
|
+
values << node[:pgbouncer]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
puts format(fmt, *values)
|
|
155
198
|
end
|
|
156
199
|
|
|
157
200
|
def print_components
|
|
@@ -240,5 +283,37 @@ module ActivePostgres
|
|
|
240
283
|
mb = kb / 1024.0
|
|
241
284
|
"#{mb.round(1)} MB"
|
|
242
285
|
end
|
|
286
|
+
|
|
287
|
+
def check_pgbouncer_status(host)
|
|
288
|
+
return '-' unless config.component_enabled?(:pgbouncer)
|
|
289
|
+
|
|
290
|
+
ssh_executor.execute_on_host(host) do
|
|
291
|
+
result = capture(:sudo, 'systemctl', 'is-active', 'pgbouncer').strip
|
|
292
|
+
result == 'active' ? '✓ running' : '✗ down'
|
|
293
|
+
end
|
|
294
|
+
rescue StandardError
|
|
295
|
+
'✗ down'
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def check_pgbouncer_running(host)
|
|
299
|
+
ssh_executor.execute_on_host(host) do
|
|
300
|
+
result = capture(:sudo, 'systemctl', 'is-active', 'pgbouncer').strip
|
|
301
|
+
result == 'active'
|
|
302
|
+
end
|
|
303
|
+
rescue StandardError
|
|
304
|
+
false
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def check_pgbouncer_userlist(host)
|
|
308
|
+
app_user = config.app_user
|
|
309
|
+
return true unless app_user # No app user configured, skip check
|
|
310
|
+
|
|
311
|
+
ssh_executor.execute_on_host(host) do
|
|
312
|
+
userlist = capture(:sudo, 'cat', '/etc/pgbouncer/userlist.txt').strip
|
|
313
|
+
userlist.include?(app_user)
|
|
314
|
+
end
|
|
315
|
+
rescue StandardError
|
|
316
|
+
false
|
|
317
|
+
end
|
|
243
318
|
end
|
|
244
319
|
end
|
|
@@ -95,7 +95,7 @@ module ActivePostgres
|
|
|
95
95
|
def deploy_pgbouncer
|
|
96
96
|
logger.task('Setting up pgbouncer on standby') do
|
|
97
97
|
component = Components::PgBouncer.new(config, ssh_executor, secrets)
|
|
98
|
-
component.install_on_standby(standby_host)
|
|
98
|
+
component.install_on_standby(standby_host)
|
|
99
99
|
end
|
|
100
100
|
end
|
|
101
101
|
|
data/lib/tasks/postgres.rake
CHANGED
|
@@ -336,6 +336,24 @@ namespace :postgres do
|
|
|
336
336
|
end
|
|
337
337
|
|
|
338
338
|
namespace :setup do
|
|
339
|
+
desc 'Setup only core PostgreSQL (updates postgresql.conf and pg_hba.conf)'
|
|
340
|
+
task core: :environment do
|
|
341
|
+
require 'active_postgres'
|
|
342
|
+
|
|
343
|
+
config = ActivePostgres::Configuration.load
|
|
344
|
+
installer = ActivePostgres::Installer.new(config)
|
|
345
|
+
installer.setup_component('core')
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
desc 'Setup only SSL certificates'
|
|
349
|
+
task ssl: :environment do
|
|
350
|
+
require 'active_postgres'
|
|
351
|
+
|
|
352
|
+
config = ActivePostgres::Configuration.load
|
|
353
|
+
installer = ActivePostgres::Installer.new(config)
|
|
354
|
+
installer.setup_component('ssl')
|
|
355
|
+
end
|
|
356
|
+
|
|
339
357
|
desc 'Setup only PgBouncer'
|
|
340
358
|
task pgbouncer: :environment do
|
|
341
359
|
require 'active_postgres'
|
|
@@ -508,6 +526,18 @@ namespace :postgres do
|
|
|
508
526
|
key_valid = test('[ -f /etc/postgresql/*/main/server.key ]')
|
|
509
527
|
if cert_valid && key_valid
|
|
510
528
|
info ' Certificates: Present ✅'
|
|
529
|
+
cert_issuer = begin
|
|
530
|
+
capture(:sudo, 'openssl', 'x509', '-in', "/etc/postgresql/#{config.version}/main/server.crt",
|
|
531
|
+
'-noout', '-issuer', '2>/dev/null').strip
|
|
532
|
+
rescue StandardError
|
|
533
|
+
nil
|
|
534
|
+
end
|
|
535
|
+
if cert_issuer
|
|
536
|
+
issuer_o = cert_issuer.match(/O\s*=\s*"?([^",\/]+)"?/)&.captures&.first
|
|
537
|
+
issuer_cn = cert_issuer.match(/CN\s*=\s*([^,\/]+)/)&.captures&.first
|
|
538
|
+
issuer_name = issuer_o || issuer_cn || cert_issuer.sub('issuer=', '')
|
|
539
|
+
info " Issuer: #{issuer_name.strip}"
|
|
540
|
+
end
|
|
511
541
|
results[:passed] << "#{label}: SSL enabled with certificates"
|
|
512
542
|
else
|
|
513
543
|
warn ' Certificates: Missing ⚠️'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_postgres
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- BoringCache
|
|
@@ -172,6 +172,8 @@ files:
|
|
|
172
172
|
- lib/active_postgres/error_handler.rb
|
|
173
173
|
- lib/active_postgres/failover.rb
|
|
174
174
|
- lib/active_postgres/generators/active_postgres/install_generator.rb
|
|
175
|
+
- lib/active_postgres/generators/active_postgres/templates/database.active_postgres.yml.erb
|
|
176
|
+
- lib/active_postgres/generators/active_postgres/templates/postgres.yml.erb
|
|
175
177
|
- lib/active_postgres/health_checker.rb
|
|
176
178
|
- lib/active_postgres/installer.rb
|
|
177
179
|
- lib/active_postgres/log_sanitizer.rb
|
|
@@ -218,7 +220,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
218
220
|
- !ruby/object:Gem::Version
|
|
219
221
|
version: '0'
|
|
220
222
|
requirements: []
|
|
221
|
-
rubygems_version: 3.
|
|
223
|
+
rubygems_version: 3.7.2
|
|
222
224
|
specification_version: 4
|
|
223
225
|
summary: PostgreSQL High Availability for Rails, made simple
|
|
224
226
|
test_files: []
|