active_postgres 0.9.5 → 0.9.6
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/CHANGELOG.md +23 -0
- data/README.md +20 -4
- data/SECURITY.md +21 -0
- data/lib/active_postgres/components/base.rb +2 -0
- data/lib/active_postgres/components/core.rb +11 -4
- data/lib/active_postgres/components/monitoring.rb +2 -2
- data/lib/active_postgres/components/pgbackrest.rb +9 -17
- data/lib/active_postgres/components/pgbouncer.rb +5 -9
- data/lib/active_postgres/components/repmgr.rb +75 -39
- data/lib/active_postgres/configuration.rb +21 -22
- data/lib/active_postgres/direct_executor.rb +3 -9
- data/lib/active_postgres/error_handler.rb +18 -18
- data/lib/active_postgres/health_checker.rb +10 -12
- data/lib/active_postgres/overview.rb +10 -10
- data/lib/active_postgres/secrets.rb +6 -7
- data/lib/active_postgres/ssh_executor.rb +5 -5
- data/lib/active_postgres/version.rb +1 -1
- data/lib/tasks/postgres.rake +26 -26
- data/templates/postgresql.conf.erb +5 -0
- data/templates/repmgr_dns_failover.sh.erb +2 -2
- metadata +16 -56
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5d06925c9a64ca3f5b96402257fda9ad13f32bbd0e3ac5377032d737c71fb2ea
|
|
4
|
+
data.tar.gz: 19cde3ee7e86d325378661aa72350d51d1bfebf33a2e6f03a1c90d34c987a997
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2dbcc33b6ad2ffadeeea4145bc5bc36910203002af908030893298df2005089e87fa8e5d465a425ffae11c60ee01b66f301e1ae594e3bb662d747c0c342b20cb
|
|
7
|
+
data.tar.gz: 2772e4a192f87c60d54ccaaeb5ea2952f7a98ce34f3daec5839e77758179f75f3f9ac09a76ab74bc929311b8b417e074e5ae156f286bbba55423c3a923b47d77
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Active Postgres will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project follows [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
## 0.9.6 - 2026-08-20
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Require Ruby 4.0 or newer and Rails 8.1 or newer for Rails integration.
|
|
15
|
+
- Align runtime and development dependencies with the maintained toolchain.
|
|
16
|
+
- Add blocking test, lint, dependency-audit, and package-build CI checks.
|
|
17
|
+
- Add automated Bundler and GitHub Actions dependency updates.
|
|
18
|
+
- Document network, credential, failover, and recovery responsibilities.
|
|
19
|
+
|
|
20
|
+
## 0.9.5
|
|
21
|
+
|
|
22
|
+
- Current published release. See the repository history for changes included in
|
|
23
|
+
earlier releases.
|
data/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# active_postgres
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
PostgreSQL high-availability automation for Ruby and Rails applications.
|
|
4
|
+
|
|
5
|
+
Active Postgres configures hosts you control over SSH. It is an operations tool,
|
|
6
|
+
not a managed database service: you remain responsible for network isolation,
|
|
7
|
+
capacity planning, monitoring, backups, recovery drills, and failover decisions.
|
|
4
8
|
|
|
5
9
|
## Features
|
|
6
10
|
|
|
@@ -272,11 +276,23 @@ User.all # → Replica
|
|
|
272
276
|
|
|
273
277
|
## Requirements
|
|
274
278
|
|
|
275
|
-
- Ruby
|
|
276
|
-
- PostgreSQL
|
|
279
|
+
- Ruby 4.0+
|
|
280
|
+
- A PostgreSQL version supported by the target Linux distribution
|
|
277
281
|
- Ubuntu 20.04+ / Debian 11+ with systemd
|
|
278
282
|
- SSH key-based authentication (hosts must be in `~/.ssh/known_hosts`)
|
|
279
|
-
- Rails
|
|
283
|
+
- Rails 8.1+ (optional)
|
|
284
|
+
|
|
285
|
+
## Operational safety
|
|
286
|
+
|
|
287
|
+
- Keep PostgreSQL, PgBouncer, exporters, and backup endpoints behind a private
|
|
288
|
+
network or narrowly scoped firewall rules.
|
|
289
|
+
- Test promotion, application reconnection, backup restore, and point-in-time
|
|
290
|
+
recovery in a disposable environment before using them in production.
|
|
291
|
+
- Review generated configuration and package changes before each rollout;
|
|
292
|
+
automatic failover cannot determine whether an application is safe to write
|
|
293
|
+
after a network partition.
|
|
294
|
+
- Store database, replication, monitoring, and backup credentials in a secret
|
|
295
|
+
manager. Do not commit resolved values or generated configuration.
|
|
280
296
|
|
|
281
297
|
### SSH host key verification
|
|
282
298
|
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
|
+
|
|
5
|
+
Please do not open a public issue for a potential vulnerability. Email
|
|
6
|
+
[security@boringcache.com](mailto:security@boringcache.com) or use
|
|
7
|
+
[GitHub private vulnerability reporting](https://github.com/boringcache/active_postgres/security/advisories/new).
|
|
8
|
+
|
|
9
|
+
Include the affected version, expected impact, and enough reproduction detail
|
|
10
|
+
for us to investigate. Do not include database credentials, private keys,
|
|
11
|
+
backups, customer data, or production configuration.
|
|
12
|
+
|
|
13
|
+
## Supported Versions
|
|
14
|
+
|
|
15
|
+
Only the latest published release is supported. Security fixes are normally
|
|
16
|
+
shipped in a new release rather than backported.
|
|
17
|
+
|
|
18
|
+
## Scope
|
|
19
|
+
|
|
20
|
+
Reports about PostgreSQL authentication, replication, backup/restore, SSH host
|
|
21
|
+
verification, secret handling, generated SQL, or release integrity are welcome.
|
|
@@ -136,7 +136,7 @@ module ActivePostgres
|
|
|
136
136
|
end
|
|
137
137
|
|
|
138
138
|
def ensure_standby_compatible(pg_config)
|
|
139
|
-
primary_settings =
|
|
139
|
+
primary_settings = primary_replication_settings
|
|
140
140
|
return pg_config if primary_settings.empty?
|
|
141
141
|
|
|
142
142
|
adjustments = []
|
|
@@ -154,17 +154,24 @@ module ActivePostgres
|
|
|
154
154
|
end
|
|
155
155
|
|
|
156
156
|
if adjustments.any?
|
|
157
|
-
puts
|
|
157
|
+
puts ' ⚠️ Adjusting standby settings to match primary minimum:'
|
|
158
158
|
adjustments.each { |adj| puts " #{adj}" }
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
pg_config
|
|
162
162
|
end
|
|
163
163
|
|
|
164
|
-
def
|
|
164
|
+
def primary_replication_settings
|
|
165
165
|
@primary_replication_settings ||= begin
|
|
166
166
|
settings = {}
|
|
167
|
-
|
|
167
|
+
setting_names = %w[
|
|
168
|
+
max_connections
|
|
169
|
+
max_worker_processes
|
|
170
|
+
max_wal_senders
|
|
171
|
+
max_prepared_transactions
|
|
172
|
+
max_locks_per_transaction
|
|
173
|
+
]
|
|
174
|
+
sql = "SELECT name || '=' || setting FROM pg_settings WHERE name IN (#{setting_names.map { |name| "'#{name}'" }.join(', ')})"
|
|
168
175
|
result = ssh_executor.run_sql(config.primary_host, sql)
|
|
169
176
|
result.strip.split("\n").each do |line|
|
|
170
177
|
name, val = line.split('=')
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require 'cgi'
|
|
1
|
+
require 'cgi/escape'
|
|
2
2
|
|
|
3
3
|
module ActivePostgres
|
|
4
4
|
module Components
|
|
@@ -83,7 +83,7 @@ module ActivePostgres
|
|
|
83
83
|
sql = build_monitoring_user_sql(monitoring_user, monitoring_password)
|
|
84
84
|
|
|
85
85
|
ssh_executor.run_sql(config.primary_host, sql, postgres_user: 'postgres', port: 5432, tuples_only: false,
|
|
86
|
-
|
|
86
|
+
capture: false)
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
def build_monitoring_user_sql(user, password)
|
|
@@ -3,9 +3,9 @@ require 'shellwords'
|
|
|
3
3
|
module ActivePostgres
|
|
4
4
|
module Components
|
|
5
5
|
class PgBackRest < Base
|
|
6
|
-
LOG_ARCHIVE_CRON_FILE = '/etc/cron.d/postgres-log-archive'
|
|
7
|
-
LOG_ARCHIVE_ENV_FILE = '/etc/active-postgres-log-archive.env'
|
|
8
|
-
LOG_ARCHIVE_SCRIPT = '/usr/local/bin/active-postgres-archive-logs'
|
|
6
|
+
LOG_ARCHIVE_CRON_FILE = '/etc/cron.d/postgres-log-archive'.freeze
|
|
7
|
+
LOG_ARCHIVE_ENV_FILE = '/etc/active-postgres-log-archive.env'.freeze
|
|
8
|
+
LOG_ARCHIVE_SCRIPT = '/usr/local/bin/active-postgres-archive-logs'.freeze
|
|
9
9
|
|
|
10
10
|
def install
|
|
11
11
|
puts 'Installing pgBackRest for backups...'
|
|
@@ -135,9 +135,7 @@ module ActivePostgres
|
|
|
135
135
|
execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", '/var/spool/pgbackrest'
|
|
136
136
|
|
|
137
137
|
# Only create stanza on primary - standbys share the same backup repo
|
|
138
|
-
if create_stanza
|
|
139
|
-
execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', 'stanza-create'
|
|
140
|
-
end
|
|
138
|
+
execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', 'stanza-create' if create_stanza
|
|
141
139
|
end
|
|
142
140
|
|
|
143
141
|
if log_archive_enabled?(pgbackrest_config)
|
|
@@ -167,12 +165,8 @@ module ActivePostgres
|
|
|
167
165
|
schedule_incremental = pgbackrest_config[:schedule_incremental]
|
|
168
166
|
|
|
169
167
|
schedules = []
|
|
170
|
-
if schedule_full
|
|
171
|
-
|
|
172
|
-
end
|
|
173
|
-
if schedule_incremental
|
|
174
|
-
schedules << { type: 'incremental', schedule: schedule_incremental, file: '/etc/cron.d/pgbackrest-backup-incremental' }
|
|
175
|
-
end
|
|
168
|
+
schedules << { type: 'full', schedule: schedule_full, file: '/etc/cron.d/pgbackrest-backup' } if schedule_full
|
|
169
|
+
schedules << { type: 'incr', schedule: schedule_incremental, file: '/etc/cron.d/pgbackrest-backup-incremental' } if schedule_incremental
|
|
176
170
|
|
|
177
171
|
schedules
|
|
178
172
|
end
|
|
@@ -204,9 +198,7 @@ module ActivePostgres
|
|
|
204
198
|
|
|
205
199
|
def setup_log_archive(host, pgbackrest_config)
|
|
206
200
|
log_archive_config = pgbackrest_config[:log_archive] || {}
|
|
207
|
-
unless pgbackrest_config[:repo_type] == 's3'
|
|
208
|
-
raise Error, 'pgbackrest.log_archive requires pgbackrest.repo_type: s3'
|
|
209
|
-
end
|
|
201
|
+
raise Error, 'pgbackrest.log_archive requires pgbackrest.repo_type: s3' unless pgbackrest_config[:repo_type] == 's3'
|
|
210
202
|
|
|
211
203
|
postgres_user = config.postgres_user
|
|
212
204
|
env_content = log_archive_env(pgbackrest_config, log_archive_config, host)
|
|
@@ -260,13 +252,13 @@ module ActivePostgres
|
|
|
260
252
|
'POSTGRES_LOG_ARCHIVE_NODE' => config.node_label_for(host) || host
|
|
261
253
|
}.compact
|
|
262
254
|
|
|
263
|
-
env.map { |key, value| "#{key}=#{Shellwords.escape(value.to_s)}" }.join("\n")
|
|
255
|
+
"#{env.map { |key, value| "#{key}=#{Shellwords.escape(value.to_s)}" }.join("\n")}\n"
|
|
264
256
|
end
|
|
265
257
|
|
|
266
258
|
def s3_endpoint_url(pgbackrest_config)
|
|
267
259
|
endpoint = pgbackrest_config[:s3_endpoint]
|
|
268
260
|
return nil if endpoint.to_s.empty?
|
|
269
|
-
return endpoint if endpoint.match?(
|
|
261
|
+
return endpoint if endpoint.match?(%r{\Ahttps?://})
|
|
270
262
|
|
|
271
263
|
"https://#{endpoint}"
|
|
272
264
|
end
|
|
@@ -81,9 +81,7 @@ module ActivePostgres
|
|
|
81
81
|
|
|
82
82
|
follow_primary = follow_primary_for?(host, is_standby: is_standby, user_config: user_config)
|
|
83
83
|
|
|
84
|
-
if follow_primary && !config.component_enabled?(:repmgr)
|
|
85
|
-
raise Error, 'PgBouncer follow_primary requires repmgr to be enabled'
|
|
86
|
-
end
|
|
84
|
+
raise Error, 'PgBouncer follow_primary requires repmgr to be enabled' if follow_primary && !config.component_enabled?(:repmgr)
|
|
87
85
|
|
|
88
86
|
max_connections = get_postgres_max_connections(host)
|
|
89
87
|
optimal_pool = ConnectionPooler.calculate_optimal_pool_sizes(max_connections)
|
|
@@ -188,9 +186,9 @@ module ActivePostgres
|
|
|
188
186
|
end
|
|
189
187
|
|
|
190
188
|
ssl_chain = secrets.resolve('ssl_chain')
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
189
|
+
return unless ssl_chain
|
|
190
|
+
|
|
191
|
+
ssh_executor.upload_file(host, ssl_chain, '/etc/pgbouncer/ca.crt', mode: '644', owner: 'postgres:postgres')
|
|
194
192
|
end
|
|
195
193
|
|
|
196
194
|
def setup_app_ssl_certs(host)
|
|
@@ -200,9 +198,7 @@ module ActivePostgres
|
|
|
200
198
|
ssl_key = secrets.resolve('ssl_key')
|
|
201
199
|
ssl_chain = secrets.resolve('ssl_chain')
|
|
202
200
|
|
|
203
|
-
unless ssl_cert && ssl_key
|
|
204
|
-
raise Error, 'PgBouncer app_hosts require ssl_cert and ssl_key secrets when ssl is enabled'
|
|
205
|
-
end
|
|
201
|
+
raise Error, 'PgBouncer app_hosts require ssl_cert and ssl_key secrets when ssl is enabled' unless ssl_cert && ssl_key
|
|
206
202
|
|
|
207
203
|
full_cert = if ssl_chain
|
|
208
204
|
"#{ssl_cert.strip}\n#{ssl_chain.strip}\n"
|
|
@@ -114,6 +114,7 @@ module ActivePostgres
|
|
|
114
114
|
if replication_user == repmgr_user && replication_password != repmgr_password
|
|
115
115
|
raise Error, 'replication_user matches repmgr user but passwords differ. Use a distinct replication_user or the same password.'
|
|
116
116
|
end
|
|
117
|
+
|
|
117
118
|
effective_replication_password = replication_user == repmgr_user ? repmgr_password : replication_password
|
|
118
119
|
|
|
119
120
|
# Variables used in ERB templates via binding
|
|
@@ -174,13 +175,13 @@ module ActivePostgres
|
|
|
174
175
|
info 'Creating repmgr database and user...'
|
|
175
176
|
repmgr_sql = repmgr_component.send(:build_repmgr_setup_sql, repmgr_user, repmgr_db, repmgr_password)
|
|
176
177
|
executor.run_sql_on_backend(self, repmgr_sql, postgres_user: 'postgres', port: 5432, tuples_only: false,
|
|
177
|
-
|
|
178
|
+
capture: false)
|
|
178
179
|
|
|
179
180
|
if replication_user != repmgr_user
|
|
180
181
|
info 'Ensuring replication user exists...'
|
|
181
182
|
repl_sql = repmgr_component.send(:build_replication_user_sql, replication_user, effective_replication_password)
|
|
182
183
|
executor.run_sql_on_backend(self, repl_sql, postgres_user: 'postgres', port: 5432, tuples_only: false,
|
|
183
|
-
|
|
184
|
+
capture: false)
|
|
184
185
|
end
|
|
185
186
|
|
|
186
187
|
info 'Reloading PostgreSQL configuration to apply pg_hba.conf changes...'
|
|
@@ -202,14 +203,14 @@ module ActivePostgres
|
|
|
202
203
|
cluster_show = nil
|
|
203
204
|
5.times do |attempt|
|
|
204
205
|
cluster_show = capture(:sudo, '-u', 'postgres', 'bash', '-lc',
|
|
205
|
-
|
|
206
|
+
'repmgr cluster show -f /etc/repmgr.conf 2>&1', raise_on_non_zero_exit: false).to_s
|
|
206
207
|
|
|
207
208
|
break if cluster_show.match?(/primary/i)
|
|
208
209
|
|
|
209
210
|
sleep 2 if attempt < 4
|
|
210
211
|
end
|
|
211
212
|
|
|
212
|
-
unless cluster_show
|
|
213
|
+
unless cluster_show&.match?(/primary/i)
|
|
213
214
|
# Fallback: verify via repmgr metadata in the repmgr database
|
|
214
215
|
db_check = executor.run_sql_on_backend(self,
|
|
215
216
|
'SELECT type FROM repmgr.nodes WHERE node_id = 1;',
|
|
@@ -255,13 +256,14 @@ module ActivePostgres
|
|
|
255
256
|
if replication_user == repmgr_user && replication_password != repmgr_password
|
|
256
257
|
raise Error, 'replication_user matches repmgr user but passwords differ. Use a distinct replication_user or the same password.'
|
|
257
258
|
end
|
|
259
|
+
|
|
258
260
|
effective_replication_password = replication_user == repmgr_user ? repmgr_password : replication_password
|
|
259
261
|
|
|
260
262
|
ensure_primary_registered
|
|
261
263
|
ensure_primary_replication_ready(repmgr_password, effective_replication_password)
|
|
262
264
|
|
|
263
265
|
setup_pgpass_file(standby_host, repmgr_password, replication_password: effective_replication_password,
|
|
264
|
-
|
|
266
|
+
primary_ip: primary_replication_host)
|
|
265
267
|
|
|
266
268
|
if standby_already_configured?(standby_host)
|
|
267
269
|
puts ' Standby already configured, updating configs...'
|
|
@@ -418,9 +420,10 @@ module ActivePostgres
|
|
|
418
420
|
|
|
419
421
|
def setup_inter_node_ssh
|
|
420
422
|
dns_config = dns_failover_config
|
|
421
|
-
key_path = dns_config && dns_config[:ssh_key_path] || '/var/lib/postgresql/.ssh/active_postgres_dns'
|
|
423
|
+
key_path = (dns_config && dns_config[:ssh_key_path]) || '/var/lib/postgresql/.ssh/active_postgres_dns'
|
|
422
424
|
postgres_user = config.postgres_user
|
|
423
425
|
all_hosts = config.all_hosts
|
|
426
|
+
replication_hosts = all_hosts.to_h { |host| [host, config.replication_host_for(host)] }
|
|
424
427
|
pub_keys = {}
|
|
425
428
|
|
|
426
429
|
all_hosts.each do |host|
|
|
@@ -430,18 +433,35 @@ module ActivePostgres
|
|
|
430
433
|
all_hosts.each do |host|
|
|
431
434
|
ssh_executor.execute_on_host(host) do
|
|
432
435
|
other_keys = pub_keys.reject { |h, _| h == host }.values
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
436
|
+
authorized_keys_path = '/var/lib/postgresql/.ssh/authorized_keys'
|
|
437
|
+
existing_keys = if test(:sudo, 'test', '-f', authorized_keys_path)
|
|
438
|
+
capture(:sudo, 'cat', authorized_keys_path).lines.map(&:strip)
|
|
439
|
+
else
|
|
440
|
+
[]
|
|
441
|
+
end
|
|
442
|
+
authorized_keys = (existing_keys + other_keys.map(&:to_s).map(&:strip)).reject(&:empty?).uniq.join("\n")
|
|
443
|
+
unless authorized_keys.empty?
|
|
444
|
+
upload! StringIO.new("#{authorized_keys}\n"), '/tmp/pg_authorized_keys'
|
|
445
|
+
execute :sudo, 'mv', '/tmp/pg_authorized_keys', authorized_keys_path
|
|
446
|
+
execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", authorized_keys_path
|
|
447
|
+
execute :sudo, 'chmod', '600', authorized_keys_path
|
|
440
448
|
end
|
|
441
449
|
|
|
442
|
-
peer_ips = all_hosts.reject { |h| h == host }.map { |h|
|
|
443
|
-
|
|
444
|
-
|
|
450
|
+
peer_ips = all_hosts.reject { |h| h == host }.map { |h| replication_hosts[h] }
|
|
451
|
+
known_hosts_path = '/var/lib/postgresql/.ssh/known_hosts'
|
|
452
|
+
existing_hosts = if test(:sudo, 'test', '-f', known_hosts_path)
|
|
453
|
+
capture(:sudo, 'cat', known_hosts_path).lines.map(&:strip)
|
|
454
|
+
else
|
|
455
|
+
[]
|
|
456
|
+
end
|
|
457
|
+
scanned_hosts = capture(:sudo, '-u', postgres_user, 'ssh-keyscan', *peer_ips, raise_on_non_zero_exit: false)
|
|
458
|
+
known_hosts = (existing_hosts + scanned_hosts.lines.map(&:strip)).reject(&:empty?).uniq.join("\n")
|
|
459
|
+
unless known_hosts.empty?
|
|
460
|
+
upload! StringIO.new("#{known_hosts}\n"), '/tmp/pg_known_hosts'
|
|
461
|
+
execute :sudo, 'mv', '/tmp/pg_known_hosts', known_hosts_path
|
|
462
|
+
execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", known_hosts_path
|
|
463
|
+
execute :sudo, 'chmod', '600', known_hosts_path
|
|
464
|
+
end
|
|
445
465
|
end
|
|
446
466
|
end
|
|
447
467
|
end
|
|
@@ -518,10 +538,21 @@ module ActivePostgres
|
|
|
518
538
|
public_key = capture(:sudo, '-u', postgres_user, 'cat', "#{key_path}.pub").strip
|
|
519
539
|
|
|
520
540
|
unless dns_servers.empty?
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
541
|
+
known_hosts_path = '/var/lib/postgresql/.ssh/known_hosts'
|
|
542
|
+
existing_hosts = if test(:sudo, 'test', '-f', known_hosts_path)
|
|
543
|
+
capture(:sudo, 'cat', known_hosts_path).lines.map(&:strip)
|
|
544
|
+
else
|
|
545
|
+
[]
|
|
546
|
+
end
|
|
547
|
+
scanned_hosts = capture(:sudo, '-u', postgres_user, 'ssh-keyscan', '-H', *dns_servers,
|
|
548
|
+
raise_on_non_zero_exit: false)
|
|
549
|
+
known_hosts = (existing_hosts + scanned_hosts.lines.map(&:strip)).reject(&:empty?).uniq.join("\n")
|
|
550
|
+
unless known_hosts.empty?
|
|
551
|
+
upload! StringIO.new("#{known_hosts}\n"), '/tmp/pg_known_hosts'
|
|
552
|
+
execute :sudo, 'mv', '/tmp/pg_known_hosts', known_hosts_path
|
|
553
|
+
execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", known_hosts_path
|
|
554
|
+
execute :sudo, 'chmod', '600', known_hosts_path
|
|
555
|
+
end
|
|
525
556
|
end
|
|
526
557
|
end
|
|
527
558
|
|
|
@@ -542,8 +573,8 @@ module ActivePostgres
|
|
|
542
573
|
|
|
543
574
|
upload! StringIO.new("#{key}\n"), '/tmp/active_postgres_dns_key.pub'
|
|
544
575
|
execute :bash, '-c',
|
|
545
|
-
|
|
546
|
-
|
|
576
|
+
'grep -qxF -f /tmp/active_postgres_dns_key.pub ~/.ssh/authorized_keys || ' \
|
|
577
|
+
'cat /tmp/active_postgres_dns_key.pub >> ~/.ssh/authorized_keys'
|
|
547
578
|
execute :rm, '-f', '/tmp/active_postgres_dns_key.pub'
|
|
548
579
|
end
|
|
549
580
|
end
|
|
@@ -560,11 +591,11 @@ module ActivePostgres
|
|
|
560
591
|
|
|
561
592
|
domains = normalize_dns_domains(dns_config)
|
|
562
593
|
primary_records = normalize_dns_records(dns_config[:primary_records] || dns_config[:primary_record],
|
|
563
|
-
|
|
564
|
-
|
|
594
|
+
default_prefix: 'db-primary',
|
|
595
|
+
domains: domains)
|
|
565
596
|
replica_records = normalize_dns_records(dns_config[:replica_records] || dns_config[:replica_record],
|
|
566
|
-
|
|
567
|
-
|
|
597
|
+
default_prefix: 'db-replica',
|
|
598
|
+
domains: domains)
|
|
568
599
|
|
|
569
600
|
_ = primary_records
|
|
570
601
|
_ = replica_records
|
|
@@ -732,10 +763,10 @@ module ActivePostgres
|
|
|
732
763
|
# Check replication status
|
|
733
764
|
begin
|
|
734
765
|
rep_status = executor.run_sql_on_backend(self,
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
766
|
+
'SELECT pg_is_in_recovery();',
|
|
767
|
+
postgres_user: postgres_user,
|
|
768
|
+
tuples_only: true,
|
|
769
|
+
capture: true).to_s
|
|
739
770
|
if rep_status.include?('t')
|
|
740
771
|
info '✓ Standby is in recovery mode (receiving replication)'
|
|
741
772
|
else
|
|
@@ -750,10 +781,10 @@ module ActivePostgres
|
|
|
750
781
|
# Check lag
|
|
751
782
|
begin
|
|
752
783
|
lag_result = executor.run_sql_on_backend(self,
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
784
|
+
'SELECT EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))::int AS lag;',
|
|
785
|
+
postgres_user: postgres_user,
|
|
786
|
+
tuples_only: true,
|
|
787
|
+
capture: true).to_s.strip
|
|
757
788
|
info "Replication lag: #{lag_result}"
|
|
758
789
|
rescue StandardError => e
|
|
759
790
|
error "Failed to get replication lag: #{e.message}"
|
|
@@ -785,7 +816,7 @@ module ActivePostgres
|
|
|
785
816
|
# Create .pgpass file for postgres user to avoid password exposure in logs
|
|
786
817
|
# Format: hostname:port:database:username:password
|
|
787
818
|
pgpass_content = build_pgpass_content(host, repmgr_password, replication_password: replication_password,
|
|
788
|
-
|
|
819
|
+
primary_ip: primary_ip)
|
|
789
820
|
|
|
790
821
|
ssh_executor.execute_on_host(host) do
|
|
791
822
|
# Create .pgpass in postgres user's home directory
|
|
@@ -854,8 +885,13 @@ module ActivePostgres
|
|
|
854
885
|
def escape_pgpass_value(value)
|
|
855
886
|
return '' if value.nil?
|
|
856
887
|
|
|
857
|
-
|
|
858
|
-
|
|
888
|
+
value.to_s.each_char.map do |char|
|
|
889
|
+
case char
|
|
890
|
+
when '\\' then '\\\\'
|
|
891
|
+
when ':' then '\\:'
|
|
892
|
+
else char
|
|
893
|
+
end
|
|
894
|
+
end.join
|
|
859
895
|
end
|
|
860
896
|
|
|
861
897
|
def build_primary_conninfo(standby_label)
|
|
@@ -935,12 +971,12 @@ module ActivePostgres
|
|
|
935
971
|
ssh_executor.execute_on_host(host) do
|
|
936
972
|
repmgr_sql = repmgr_component.send(:build_repmgr_setup_sql, repmgr_user, repmgr_db, repmgr_password)
|
|
937
973
|
executor.run_sql_on_backend(self, repmgr_sql, postgres_user: 'postgres', port: 5432, tuples_only: false,
|
|
938
|
-
|
|
974
|
+
capture: false)
|
|
939
975
|
|
|
940
976
|
if replication_user != repmgr_user
|
|
941
977
|
repl_sql = repmgr_component.send(:build_replication_user_sql, replication_user, effective_replication_password)
|
|
942
978
|
executor.run_sql_on_backend(self, repl_sql, postgres_user: 'postgres', port: 5432, tuples_only: false,
|
|
943
|
-
|
|
979
|
+
capture: false)
|
|
944
980
|
end
|
|
945
981
|
|
|
946
982
|
info '✓ Primary replication user is ready'
|
|
@@ -125,11 +125,10 @@ module ActivePostgres
|
|
|
125
125
|
# Validate required secrets if components are enabled
|
|
126
126
|
raise Error, 'Missing replication_password secret' if component_enabled?(:repmgr) && !secrets_config['replication_password']
|
|
127
127
|
raise Error, 'Missing monitoring_password secret' if component_enabled?(:monitoring) && !secrets_config['monitoring_password']
|
|
128
|
+
|
|
128
129
|
if component_enabled?(:monitoring)
|
|
129
130
|
grafana_config = component_config(:monitoring)[:grafana] || {}
|
|
130
|
-
if grafana_config[:enabled] && !secrets_config['grafana_admin_password']
|
|
131
|
-
raise Error, 'Missing grafana_admin_password secret'
|
|
132
|
-
end
|
|
131
|
+
raise Error, 'Missing grafana_admin_password secret' if grafana_config[:enabled] && !secrets_config['grafana_admin_password']
|
|
133
132
|
if grafana_config[:enabled] && grafana_config[:host].to_s.strip.empty?
|
|
134
133
|
raise Error, 'monitoring.grafana.host is required when grafana is enabled'
|
|
135
134
|
end
|
|
@@ -150,27 +149,27 @@ module ActivePostgres
|
|
|
150
149
|
end
|
|
151
150
|
end
|
|
152
151
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
end
|
|
152
|
+
if component_enabled?(:repmgr)
|
|
153
|
+
dns_failover = component_config(:repmgr)[:dns_failover]
|
|
154
|
+
if dns_failover && dns_failover[:enabled]
|
|
155
|
+
domains = Array(dns_failover[:domains] || dns_failover[:domain]).map(&:to_s).map(&:strip).reject(&:empty?)
|
|
156
|
+
servers = Array(dns_failover[:dns_servers])
|
|
157
|
+
provider = (dns_failover[:provider] || 'dnsmasq').to_s.strip
|
|
158
|
+
|
|
159
|
+
raise Error, 'dns_failover.domain or dns_failover.domains is required when enabled' if domains.empty?
|
|
160
|
+
raise Error, 'dns_failover.dns_servers is required when enabled' if servers.empty?
|
|
161
|
+
raise Error, "Unsupported dns_failover provider '#{provider}'" unless provider == 'dnsmasq'
|
|
162
|
+
|
|
163
|
+
servers.each do |server|
|
|
164
|
+
next unless server.is_a?(Hash)
|
|
165
|
+
|
|
166
|
+
ssh_host = server['ssh_host'] || server[:ssh_host] || server['host'] || server[:host]
|
|
167
|
+
private_ip = server['private_ip'] || server[:private_ip] || server['ip'] || server[:ip]
|
|
168
|
+
raise Error, 'dns_failover.dns_servers entries must include host/ssh_host or private_ip' if
|
|
169
|
+
(ssh_host.nil? || ssh_host.to_s.strip.empty?) && (private_ip.nil? || private_ip.to_s.strip.empty?)
|
|
172
170
|
end
|
|
173
171
|
end
|
|
172
|
+
end
|
|
174
173
|
|
|
175
174
|
true
|
|
176
175
|
end
|
|
@@ -62,19 +62,13 @@ module ActivePostgres
|
|
|
62
62
|
return nil if value.nil?
|
|
63
63
|
|
|
64
64
|
# Handle Rails credentials
|
|
65
|
-
if value.start_with?('rails_credentials:')
|
|
66
|
-
return resolve_rails_credentials(value)
|
|
67
|
-
end
|
|
65
|
+
return resolve_rails_credentials(value) if value.start_with?('rails_credentials:')
|
|
68
66
|
|
|
69
67
|
# Handle environment variables
|
|
70
|
-
if value.start_with?('$')
|
|
71
|
-
return ENV[value[1..]]
|
|
72
|
-
end
|
|
68
|
+
return ENV.fetch(value[1..], nil) if value.start_with?('$')
|
|
73
69
|
|
|
74
70
|
# Handle shell commands
|
|
75
|
-
if value.start_with?('$(') && value.end_with?(')')
|
|
76
|
-
return `#{value[2..-2]}`.strip
|
|
77
|
-
end
|
|
71
|
+
return `#{value[2..-2]}`.strip if value.start_with?('$(') && value.end_with?(')')
|
|
78
72
|
|
|
79
73
|
value
|
|
80
74
|
end
|
|
@@ -127,24 +127,24 @@ module ActivePostgres
|
|
|
127
127
|
|
|
128
128
|
# Identify error type from error message
|
|
129
129
|
def identify_error_type(message)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
130
|
+
normalized = message.to_s.downcase
|
|
131
|
+
|
|
132
|
+
return :ssh_connection if contains_any?(normalized, 'ssh', 'connection refused', 'network unreachable')
|
|
133
|
+
return :private_network_connectivity if contains_any?(normalized, 'private network', 'vpn', 'wireguard network')
|
|
134
|
+
return :postgresql_not_starting if normalized.include?('postgresql') && normalized.include?('not') && normalized.include?('start')
|
|
135
|
+
return :postgresql_not_starting if normalized.include?('cluster') && normalized.include?('not') && normalized.include?('running')
|
|
136
|
+
return :repmgr_clone_failed if normalized.include?('repmgr') && normalized.include?('clone')
|
|
137
|
+
return :repmgr_clone_failed if normalized.include?('data directory')
|
|
138
|
+
return :repmgr_register_failed if normalized.include?('repmgr') && normalized.include?('register')
|
|
139
|
+
return :repmgr_register_failed if normalized.include?('unable to connect to') && normalized.include?('primary')
|
|
140
|
+
return :ssl_certificate_error if contains_any?(normalized, 'ssl', 'certificate', 'tls')
|
|
141
|
+
return :disk_space_error if contains_any?(normalized, 'no space', 'disk full')
|
|
142
|
+
|
|
143
|
+
:authentication_failed if contains_any?(normalized, 'authentication', 'password', 'pg_hba')
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def contains_any?(value, *needles)
|
|
147
|
+
needles.any? { |needle| value.include?(needle) }
|
|
148
148
|
end
|
|
149
149
|
|
|
150
150
|
# Show troubleshooting guide for a specific error type
|
|
@@ -12,14 +12,6 @@ module ActivePostgres
|
|
|
12
12
|
@executor
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
private def create_executor
|
|
16
|
-
if use_ssh_executor?
|
|
17
|
-
SSHExecutor.new(config, quiet: true)
|
|
18
|
-
else
|
|
19
|
-
DirectExecutor.new(config, quiet: true)
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
15
|
def use_ssh_executor?
|
|
24
16
|
mode = ENV['ACTIVE_POSTGRES_STATUS_MODE'].to_s.strip.downcase
|
|
25
17
|
return true if mode == 'ssh'
|
|
@@ -131,6 +123,14 @@ module ActivePostgres
|
|
|
131
123
|
|
|
132
124
|
private
|
|
133
125
|
|
|
126
|
+
def create_executor
|
|
127
|
+
if use_ssh_executor?
|
|
128
|
+
SSHExecutor.new(config, quiet: true)
|
|
129
|
+
else
|
|
130
|
+
DirectExecutor.new(config, quiet: true)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
134
|
def collect_node_status
|
|
135
135
|
nodes = []
|
|
136
136
|
|
|
@@ -184,9 +184,7 @@ module ActivePostgres
|
|
|
184
184
|
lag: [3, nodes.map { |n| n[:lag].to_s.length }.max].max
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
if config.component_enabled?(:pgbouncer)
|
|
188
|
-
cols[:pgbouncer] = [9, nodes.map { |n| n[:pgbouncer].to_s.length }.max].max
|
|
189
|
-
end
|
|
187
|
+
cols[:pgbouncer] = [9, nodes.map { |n| n[:pgbouncer].to_s.length }.max].max if config.component_enabled?(:pgbouncer)
|
|
190
188
|
|
|
191
189
|
cols
|
|
192
190
|
end
|
|
@@ -194,7 +192,7 @@ module ActivePostgres
|
|
|
194
192
|
def print_table_header(cols)
|
|
195
193
|
fmt = "%-#{cols[:role]}s %-#{cols[:host]}s %-#{cols[:private_ip]}s " \
|
|
196
194
|
"%-#{cols[:label]}s %-#{cols[:status]}s %#{cols[:conn]}s %#{cols[:lag]}s"
|
|
197
|
-
headers =
|
|
195
|
+
headers = ['Role', 'Host', 'Private IP', 'Label', 'Status', 'Conn', 'Lag']
|
|
198
196
|
|
|
199
197
|
if cols[:pgbouncer]
|
|
200
198
|
fmt += " %-#{cols[:pgbouncer]}s"
|
|
@@ -61,14 +61,14 @@ module ActivePostgres
|
|
|
61
61
|
label = config.node_label_for(host)
|
|
62
62
|
stats = fetch_system_stats(host, paths)
|
|
63
63
|
if stats.nil?
|
|
64
|
-
puts " #{host}#{
|
|
64
|
+
puts " #{host}#{" (#{label})" if label}: unavailable"
|
|
65
65
|
next
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
mem_used_kb = stats[:mem_total_kb] - stats[:mem_avail_kb]
|
|
69
69
|
mem_pct = stats[:mem_total_kb].positive? ? (mem_used_kb.to_f / stats[:mem_total_kb] * 100).round : 0
|
|
70
70
|
|
|
71
|
-
puts " #{host}#{
|
|
71
|
+
puts " #{host}#{" (#{label})" if label}: load #{stats[:loadavg]} | cpu #{stats[:cpu]}% | " \
|
|
72
72
|
"mem #{format_kb(mem_used_kb)}/#{format_kb(stats[:mem_total_kb])} (#{mem_pct}%)"
|
|
73
73
|
|
|
74
74
|
stats[:disks].each do |disk|
|
|
@@ -227,7 +227,7 @@ module ActivePostgres
|
|
|
227
227
|
with_timeout("system stats #{host}") do
|
|
228
228
|
ssh_executor = executor
|
|
229
229
|
safe_paths = paths.map { |p| Shellwords.escape(p) }.join(' ')
|
|
230
|
-
script = <<~
|
|
230
|
+
script = <<~BASH
|
|
231
231
|
set -e
|
|
232
232
|
loadavg=$(cut -d' ' -f1-3 /proc/loadavg)
|
|
233
233
|
|
|
@@ -256,7 +256,7 @@ module ActivePostgres
|
|
|
256
256
|
|
|
257
257
|
ssh_executor.execute_on_host(host) do
|
|
258
258
|
output = capture(:bash, '-lc', "#{script}\n df -kP #{safe_paths} 2>/dev/null | tail -n +2 | " \
|
|
259
|
-
|
|
259
|
+
"awk '{print \"disk=\" $6 \"|\" $2 \"|\" $3 \"|\" $4 \"|\" $5}'",
|
|
260
260
|
raise_on_non_zero_exit: false).to_s
|
|
261
261
|
end
|
|
262
262
|
end
|
|
@@ -299,12 +299,12 @@ module ActivePostgres
|
|
|
299
299
|
stats
|
|
300
300
|
end
|
|
301
301
|
|
|
302
|
-
def format_kb(
|
|
303
|
-
|
|
304
|
-
gb =
|
|
302
|
+
def format_kb(kilobytes)
|
|
303
|
+
kilobytes = kilobytes.to_f
|
|
304
|
+
gb = kilobytes / 1024 / 1024
|
|
305
305
|
return format('%.1fG', gb) if gb >= 1
|
|
306
306
|
|
|
307
|
-
mb =
|
|
307
|
+
mb = kilobytes / 1024
|
|
308
308
|
format('%.0fM', mb)
|
|
309
309
|
end
|
|
310
310
|
|
|
@@ -342,8 +342,8 @@ module ActivePostgres
|
|
|
342
342
|
value.positive? ? value : DEFAULT_TIMEOUT
|
|
343
343
|
end
|
|
344
344
|
|
|
345
|
-
def with_timeout(label)
|
|
346
|
-
Timeout.timeout(overview_timeout)
|
|
345
|
+
def with_timeout(label, &)
|
|
346
|
+
Timeout.timeout(overview_timeout, &)
|
|
347
347
|
rescue Timeout::Error
|
|
348
348
|
raise StandardError, "#{label} timed out after #{overview_timeout}s"
|
|
349
349
|
end
|
|
@@ -20,8 +20,8 @@ module ActivePostgres
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def resolve_all
|
|
23
|
-
config.secrets_config.keys.
|
|
24
|
-
|
|
23
|
+
config.secrets_config.keys.to_h do |key|
|
|
24
|
+
[key, resolve(key)]
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
@@ -47,8 +47,9 @@ module ActivePostgres
|
|
|
47
47
|
|
|
48
48
|
resolve_all.each do |key, value|
|
|
49
49
|
file_path = File.join(directory, key)
|
|
50
|
-
File.
|
|
51
|
-
|
|
50
|
+
File.open(file_path, File::WRONLY | File::CREAT | File::TRUNC, 0o600) do |file|
|
|
51
|
+
file.write(value)
|
|
52
|
+
end
|
|
52
53
|
puts "Cached #{key} to #{file_path}"
|
|
53
54
|
end
|
|
54
55
|
|
|
@@ -101,9 +102,7 @@ module ActivePostgres
|
|
|
101
102
|
|
|
102
103
|
# Try loading just the Rails application (without full initialization)
|
|
103
104
|
# This makes Rails.application available for credential access
|
|
104
|
-
if File.exist?('./config/application.rb')
|
|
105
|
-
require './config/application'
|
|
106
|
-
end
|
|
105
|
+
require './config/application' if File.exist?('./config/application.rb')
|
|
107
106
|
rescue StandardError
|
|
108
107
|
# Rails boot failed — CLI running outside a Rails project
|
|
109
108
|
nil
|
|
@@ -215,11 +215,11 @@ module ActivePostgres
|
|
|
215
215
|
execute_on_host(host) do
|
|
216
216
|
backend = self
|
|
217
217
|
result = executor.run_sql_on_backend(backend, sql,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
218
|
+
postgres_user: postgres_user,
|
|
219
|
+
port: port,
|
|
220
|
+
database: database,
|
|
221
|
+
tuples_only: tuples_only,
|
|
222
|
+
capture: capture)
|
|
223
223
|
end
|
|
224
224
|
result
|
|
225
225
|
end
|
data/lib/tasks/postgres.rake
CHANGED
|
@@ -287,39 +287,39 @@ namespace :postgres do
|
|
|
287
287
|
puts "\nTip: set RAILS_ENV=production for production targets"
|
|
288
288
|
|
|
289
289
|
puts "\nSetup & Maintenance"
|
|
290
|
-
puts
|
|
291
|
-
puts
|
|
292
|
-
puts
|
|
293
|
-
puts
|
|
294
|
-
puts
|
|
295
|
-
puts
|
|
296
|
-
puts
|
|
297
|
-
puts
|
|
290
|
+
puts ' rake postgres:setup # Deploy HA cluster (CLEAN=true for fresh install)'
|
|
291
|
+
puts ' rake postgres:purge # Destroy cluster (DESTRUCTIVE)'
|
|
292
|
+
puts ' rake postgres:setup:core # PostgreSQL config (postgresql.conf, pg_hba.conf)'
|
|
293
|
+
puts ' rake postgres:setup:repmgr # HA + failover (repmgr)'
|
|
294
|
+
puts ' rake postgres:setup:pgbouncer # PgBouncer pooling'
|
|
295
|
+
puts ' rake postgres:setup:pgbackrest # Backups (pgBackRest)'
|
|
296
|
+
puts ' rake postgres:setup:monitoring # postgres_exporter'
|
|
297
|
+
puts ' rake postgres:setup:ssl # SSL certs'
|
|
298
298
|
|
|
299
299
|
puts "\nStatus & Health"
|
|
300
|
-
puts
|
|
301
|
-
puts
|
|
302
|
-
puts
|
|
303
|
-
puts
|
|
304
|
-
puts
|
|
300
|
+
puts ' rake postgres:status # Cluster status (SSH by default)'
|
|
301
|
+
puts ' rake postgres:overview # Control tower overview'
|
|
302
|
+
puts ' rake postgres:nodes # Topology view'
|
|
303
|
+
puts ' rake postgres:verify # Comprehensive checklist'
|
|
304
|
+
puts ' ACTIVE_POSTGRES_STATUS_MODE=ssh|direct rake postgres:status'
|
|
305
305
|
|
|
306
306
|
puts "\nBackups"
|
|
307
|
-
puts
|
|
308
|
-
puts
|
|
309
|
-
puts
|
|
310
|
-
puts
|
|
311
|
-
puts
|
|
307
|
+
puts ' rake postgres:backup:full # Full backup'
|
|
308
|
+
puts ' rake postgres:backup:incremental # Incremental backup'
|
|
309
|
+
puts ' rake postgres:backup:list # List backups'
|
|
310
|
+
puts ' rake postgres:backup:restore[ID] # Restore backup set'
|
|
311
|
+
puts ' rake postgres:backup:restore_at["YYYY-MM-DD HH:MM:SS",promote] # PITR'
|
|
312
312
|
|
|
313
313
|
puts "\nMigrations"
|
|
314
|
-
puts
|
|
314
|
+
puts ' rake postgres:migrate # Run migrations on primary only'
|
|
315
315
|
|
|
316
316
|
puts "\nPgBouncer"
|
|
317
|
-
puts
|
|
318
|
-
puts
|
|
317
|
+
puts ' rake postgres:pgbouncer:update_userlist[users] # Update userlist'
|
|
318
|
+
puts ' rake postgres:pgbouncer:stats # Service status + stats'
|
|
319
319
|
|
|
320
320
|
puts "\nTests"
|
|
321
|
-
puts
|
|
322
|
-
puts
|
|
321
|
+
puts ' rake postgres:test:replication[rows] # Replication stress test'
|
|
322
|
+
puts ' rake postgres:test:pgbouncer[connections] # PgBouncer load test'
|
|
323
323
|
|
|
324
324
|
puts
|
|
325
325
|
end
|
|
@@ -380,7 +380,7 @@ namespace :postgres do
|
|
|
380
380
|
end
|
|
381
381
|
|
|
382
382
|
desc 'Restore to a point in time (PITR)'
|
|
383
|
-
task :restore_at, [
|
|
383
|
+
task :restore_at, %i[target_time target_action] => :environment do |_t, args|
|
|
384
384
|
require 'active_postgres'
|
|
385
385
|
|
|
386
386
|
unless args[:target_time]
|
|
@@ -607,8 +607,8 @@ namespace :postgres do
|
|
|
607
607
|
nil
|
|
608
608
|
end
|
|
609
609
|
if cert_issuer
|
|
610
|
-
issuer_o = cert_issuer.match(
|
|
611
|
-
issuer_cn = cert_issuer.match(
|
|
610
|
+
issuer_o = cert_issuer.match(%r{O\s*=\s*"?([^",/]+)"?})&.captures&.first
|
|
611
|
+
issuer_cn = cert_issuer.match(%r{CN\s*=\s*([^,/]+)})&.captures&.first
|
|
612
612
|
issuer_name = issuer_o || issuer_cn || cert_issuer.sub('issuer=', '')
|
|
613
613
|
info " Issuer: #{issuer_name.strip}"
|
|
614
614
|
end
|
|
@@ -52,6 +52,11 @@ archive_command = '<%= pg_config[:archive_command] %>'
|
|
|
52
52
|
<% elsif config.component_enabled?(:pgbackrest) %>
|
|
53
53
|
archive_command = 'pgbackrest --stanza=main archive-push %p'
|
|
54
54
|
<% end %>
|
|
55
|
+
<% if pg_config[:restore_command] %>
|
|
56
|
+
restore_command = '<%= pg_config[:restore_command] %>'
|
|
57
|
+
<% elsif config.component_enabled?(:pgbackrest) %>
|
|
58
|
+
restore_command = 'pgbackrest --pg1-path=/var/lib/postgresql/<%= config.version %>/main --stanza=main archive-get %f "%p"'
|
|
59
|
+
<% end %>
|
|
55
60
|
<% if pg_config[:shared_memory_type] %>
|
|
56
61
|
shared_memory_type = <%= pg_config[:shared_memory_type] %>
|
|
57
62
|
<% end %>
|
|
@@ -17,13 +17,13 @@ REPMGR_DB=$(grep -oP "dbname=\K[^ ']+" "$REPMGR_CONF" | head -1)
|
|
|
17
17
|
REPMGR_USER=$(grep -oP "user=\K[^ ']+" "$REPMGR_CONF" 2>/dev/null | head -1)
|
|
18
18
|
REPMGR_USER="${REPMGR_USER:-repmgr}"
|
|
19
19
|
|
|
20
|
-
cluster_data=$(psql -h /var/run/postgresql -d "$REPMGR_DB" -tAF',' -c "SELECT type, conninfo FROM repmgr.nodes WHERE active = true" 2>/dev/null || true)
|
|
20
|
+
cluster_data=$(psql -h /var/run/postgresql -d "$REPMGR_DB" -tAF',' -c "SELECT type, conninfo FROM repmgr.nodes WHERE active = true ORDER BY node_id" 2>/dev/null || true)
|
|
21
21
|
if [[ -z "$cluster_data" ]]; then
|
|
22
22
|
exit 0
|
|
23
23
|
fi
|
|
24
24
|
|
|
25
25
|
primary_host=$(printf "%s\n" "$cluster_data" | awk -F',' '$1 == "primary" {print $2; exit}' | sed -n 's/.*host=\([^ ]*\).*/\1/p')
|
|
26
|
-
standby_hosts=$(printf "%s\n" "$cluster_data" | awk -F',' '$1 == "standby" {print $2}' | sed -n 's/.*host=\([^ ]*\).*/\1/p'
|
|
26
|
+
standby_hosts=$(printf "%s\n" "$cluster_data" | awk -F',' '$1 == "standby" {print $2; exit}' | sed -n 's/.*host=\([^ ]*\).*/\1/p')
|
|
27
27
|
|
|
28
28
|
if [[ -z "$primary_host" ]]; then
|
|
29
29
|
exit 0
|
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.9.
|
|
4
|
+
version: 0.9.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- BoringCache
|
|
@@ -29,35 +29,35 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '1.
|
|
32
|
+
version: '1.4'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '1.
|
|
39
|
+
version: '1.4'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: pg
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '1.
|
|
46
|
+
version: '1.6'
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '1.
|
|
53
|
+
version: '1.6'
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
55
|
name: railties
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
58
|
- - ">="
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: '
|
|
60
|
+
version: '8.1'
|
|
61
61
|
- - "<"
|
|
62
62
|
- !ruby/object:Gem::Version
|
|
63
63
|
version: '9.0'
|
|
@@ -67,7 +67,7 @@ dependencies:
|
|
|
67
67
|
requirements:
|
|
68
68
|
- - ">="
|
|
69
69
|
- !ruby/object:Gem::Version
|
|
70
|
-
version: '
|
|
70
|
+
version: '8.1'
|
|
71
71
|
- - "<"
|
|
72
72
|
- !ruby/object:Gem::Version
|
|
73
73
|
version: '9.0'
|
|
@@ -77,70 +77,28 @@ dependencies:
|
|
|
77
77
|
requirements:
|
|
78
78
|
- - "~>"
|
|
79
79
|
- !ruby/object:Gem::Version
|
|
80
|
-
version: '1.
|
|
80
|
+
version: '1.25'
|
|
81
81
|
type: :runtime
|
|
82
82
|
prerelease: false
|
|
83
83
|
version_requirements: !ruby/object:Gem::Requirement
|
|
84
84
|
requirements:
|
|
85
85
|
- - "~>"
|
|
86
86
|
- !ruby/object:Gem::Version
|
|
87
|
-
version: '1.
|
|
87
|
+
version: '1.25'
|
|
88
88
|
- !ruby/object:Gem::Dependency
|
|
89
89
|
name: thor
|
|
90
90
|
requirement: !ruby/object:Gem::Requirement
|
|
91
91
|
requirements:
|
|
92
92
|
- - "~>"
|
|
93
93
|
- !ruby/object:Gem::Version
|
|
94
|
-
version: '1.
|
|
94
|
+
version: '1.5'
|
|
95
95
|
type: :runtime
|
|
96
96
|
prerelease: false
|
|
97
97
|
version_requirements: !ruby/object:Gem::Requirement
|
|
98
98
|
requirements:
|
|
99
99
|
- - "~>"
|
|
100
100
|
- !ruby/object:Gem::Version
|
|
101
|
-
version: '1.
|
|
102
|
-
- !ruby/object:Gem::Dependency
|
|
103
|
-
name: minitest
|
|
104
|
-
requirement: !ruby/object:Gem::Requirement
|
|
105
|
-
requirements:
|
|
106
|
-
- - "~>"
|
|
107
|
-
- !ruby/object:Gem::Version
|
|
108
|
-
version: '5.0'
|
|
109
|
-
type: :development
|
|
110
|
-
prerelease: false
|
|
111
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
112
|
-
requirements:
|
|
113
|
-
- - "~>"
|
|
114
|
-
- !ruby/object:Gem::Version
|
|
115
|
-
version: '5.0'
|
|
116
|
-
- !ruby/object:Gem::Dependency
|
|
117
|
-
name: rubocop
|
|
118
|
-
requirement: !ruby/object:Gem::Requirement
|
|
119
|
-
requirements:
|
|
120
|
-
- - "~>"
|
|
121
|
-
- !ruby/object:Gem::Version
|
|
122
|
-
version: '1.50'
|
|
123
|
-
type: :development
|
|
124
|
-
prerelease: false
|
|
125
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
126
|
-
requirements:
|
|
127
|
-
- - "~>"
|
|
128
|
-
- !ruby/object:Gem::Version
|
|
129
|
-
version: '1.50'
|
|
130
|
-
- !ruby/object:Gem::Dependency
|
|
131
|
-
name: simplecov
|
|
132
|
-
requirement: !ruby/object:Gem::Requirement
|
|
133
|
-
requirements:
|
|
134
|
-
- - "~>"
|
|
135
|
-
- !ruby/object:Gem::Version
|
|
136
|
-
version: '0.22'
|
|
137
|
-
type: :development
|
|
138
|
-
prerelease: false
|
|
139
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
140
|
-
requirements:
|
|
141
|
-
- - "~>"
|
|
142
|
-
- !ruby/object:Gem::Version
|
|
143
|
-
version: '0.22'
|
|
101
|
+
version: '1.5'
|
|
144
102
|
description: Production-grade PostgreSQL HA with deep Rails integration. Automated
|
|
145
103
|
deployment, replication, failover, and monitoring.
|
|
146
104
|
email:
|
|
@@ -150,8 +108,10 @@ executables:
|
|
|
150
108
|
extensions: []
|
|
151
109
|
extra_rdoc_files: []
|
|
152
110
|
files:
|
|
111
|
+
- CHANGELOG.md
|
|
153
112
|
- LICENSE
|
|
154
113
|
- README.md
|
|
114
|
+
- SECURITY.md
|
|
155
115
|
- exe/activepostgres
|
|
156
116
|
- lib/active_postgres.rb
|
|
157
117
|
- lib/active_postgres/cli.rb
|
|
@@ -209,10 +169,10 @@ homepage: https://github.com/boringcache/active_postgres
|
|
|
209
169
|
licenses:
|
|
210
170
|
- MIT
|
|
211
171
|
metadata:
|
|
212
|
-
homepage_uri: https://github.com/boringcache/active_postgres
|
|
213
172
|
source_code_uri: https://github.com/boringcache/active_postgres
|
|
214
173
|
documentation_uri: https://github.com/boringcache/active_postgres/blob/main/README.md
|
|
215
174
|
changelog_uri: https://github.com/boringcache/active_postgres/blob/main/CHANGELOG.md
|
|
175
|
+
bug_tracker_uri: https://github.com/boringcache/active_postgres/issues
|
|
216
176
|
rubygems_mfa_required: 'true'
|
|
217
177
|
rdoc_options: []
|
|
218
178
|
require_paths:
|
|
@@ -221,14 +181,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
221
181
|
requirements:
|
|
222
182
|
- - ">="
|
|
223
183
|
- !ruby/object:Gem::Version
|
|
224
|
-
version:
|
|
184
|
+
version: 4.0.0
|
|
225
185
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
226
186
|
requirements:
|
|
227
187
|
- - ">="
|
|
228
188
|
- !ruby/object:Gem::Version
|
|
229
189
|
version: '0'
|
|
230
190
|
requirements: []
|
|
231
|
-
rubygems_version:
|
|
191
|
+
rubygems_version: 4.0.16
|
|
232
192
|
specification_version: 4
|
|
233
193
|
summary: PostgreSQL High Availability for Rails, made simple
|
|
234
194
|
test_files: []
|