active_postgres 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +23 -0
- data/README.md +158 -0
- data/exe/activepostgres +5 -0
- data/lib/active_postgres/cli.rb +157 -0
- data/lib/active_postgres/cluster_deployment_flow.rb +85 -0
- data/lib/active_postgres/component_resolver.rb +24 -0
- data/lib/active_postgres/components/base.rb +38 -0
- data/lib/active_postgres/components/core.rb +158 -0
- data/lib/active_postgres/components/extensions.rb +99 -0
- data/lib/active_postgres/components/monitoring.rb +55 -0
- data/lib/active_postgres/components/pgbackrest.rb +94 -0
- data/lib/active_postgres/components/pgbouncer.rb +137 -0
- data/lib/active_postgres/components/repmgr.rb +651 -0
- data/lib/active_postgres/components/ssl.rb +86 -0
- data/lib/active_postgres/configuration.rb +190 -0
- data/lib/active_postgres/connection_pooler.rb +429 -0
- data/lib/active_postgres/credentials.rb +17 -0
- data/lib/active_postgres/deployment_flow.rb +154 -0
- data/lib/active_postgres/error_handler.rb +185 -0
- data/lib/active_postgres/failover.rb +83 -0
- data/lib/active_postgres/generators/active_postgres/install_generator.rb +186 -0
- data/lib/active_postgres/health_checker.rb +244 -0
- data/lib/active_postgres/installer.rb +114 -0
- data/lib/active_postgres/log_sanitizer.rb +67 -0
- data/lib/active_postgres/logger.rb +125 -0
- data/lib/active_postgres/performance_tuner.rb +246 -0
- data/lib/active_postgres/rails/database_config.rb +174 -0
- data/lib/active_postgres/rails/migration_guard.rb +25 -0
- data/lib/active_postgres/railtie.rb +28 -0
- data/lib/active_postgres/retry_helper.rb +80 -0
- data/lib/active_postgres/rollback_manager.rb +140 -0
- data/lib/active_postgres/secrets.rb +86 -0
- data/lib/active_postgres/ssh_executor.rb +288 -0
- data/lib/active_postgres/standby_deployment_flow.rb +122 -0
- data/lib/active_postgres/validator.rb +143 -0
- data/lib/active_postgres/version.rb +3 -0
- data/lib/active_postgres.rb +67 -0
- data/lib/tasks/postgres.rake +855 -0
- data/lib/tasks/rolling_update.rake +258 -0
- data/lib/tasks/rotate_credentials.rake +193 -0
- data/templates/pg_hba.conf.erb +47 -0
- data/templates/pgbackrest.conf.erb +43 -0
- data/templates/pgbouncer.ini.erb +55 -0
- data/templates/postgresql.conf.erb +157 -0
- data/templates/repmgr.conf.erb +40 -0
- metadata +224 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module ActivePostgres
|
|
2
|
+
module Components
|
|
3
|
+
class Extensions < Base
|
|
4
|
+
EXTENSION_PACKAGES = {
|
|
5
|
+
'pgvector' => 'postgresql-{version}-pgvector',
|
|
6
|
+
'postgis' => 'postgresql-{version}-postgis-3',
|
|
7
|
+
'pg_trgm' => nil, # Built-in, no package needed
|
|
8
|
+
'hstore' => nil, # Built-in
|
|
9
|
+
'uuid-ossp' => nil, # Built-in
|
|
10
|
+
'ltree' => nil, # Built-in
|
|
11
|
+
'citext' => nil, # Built-in
|
|
12
|
+
'unaccent' => nil, # Built-in
|
|
13
|
+
'pg_stat_statements' => nil, # Built-in
|
|
14
|
+
'timescaledb' => 'timescaledb-2-postgresql-{version}',
|
|
15
|
+
'citus' => 'postgresql-{version}-citus-12.1',
|
|
16
|
+
'pg_partman' => 'postgresql-{version}-partman'
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def install
|
|
20
|
+
extensions_config = config.component_config(:extensions)
|
|
21
|
+
return unless extensions_config[:enabled]
|
|
22
|
+
|
|
23
|
+
extensions = extensions_config[:list] || []
|
|
24
|
+
return if extensions.empty?
|
|
25
|
+
|
|
26
|
+
puts 'Installing PostgreSQL extensions...'
|
|
27
|
+
|
|
28
|
+
install_on_primary(extensions)
|
|
29
|
+
install_on_standbys(extensions) if config.standbys.any?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def uninstall
|
|
33
|
+
puts 'Extensions uninstall not implemented (extensions remain in database)'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def install_on_primary(extensions)
|
|
39
|
+
host = config.primary_host
|
|
40
|
+
version = config.version
|
|
41
|
+
db_name = config.secrets_config['database_name'] || 'postgres'
|
|
42
|
+
postgres_user = config.postgres_user
|
|
43
|
+
|
|
44
|
+
puts " Installing extensions on primary (#{host})..."
|
|
45
|
+
|
|
46
|
+
packages_to_install = []
|
|
47
|
+
extensions.each do |ext_name|
|
|
48
|
+
package = EXTENSION_PACKAGES[ext_name]
|
|
49
|
+
next unless package
|
|
50
|
+
|
|
51
|
+
package = package.gsub('{version}', version.to_s)
|
|
52
|
+
packages_to_install << package
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
ssh_executor.execute_on_host(host) do
|
|
56
|
+
unless packages_to_install.empty?
|
|
57
|
+
execute :sudo, 'apt-get', 'update', '-qq'
|
|
58
|
+
execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq',
|
|
59
|
+
*packages_to_install
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
extensions.each do |ext_name|
|
|
63
|
+
sql = "CREATE EXTENSION IF NOT EXISTS #{ext_name};"
|
|
64
|
+
begin
|
|
65
|
+
execute :sudo, '-u', postgres_user, 'psql', '-d', db_name, '-c', sql
|
|
66
|
+
rescue StandardError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def install_on_standbys(extensions)
|
|
74
|
+
version = config.version
|
|
75
|
+
|
|
76
|
+
config.standby_hosts.each do |host|
|
|
77
|
+
puts " Installing extensions on standby (#{host})..."
|
|
78
|
+
|
|
79
|
+
packages_to_install = []
|
|
80
|
+
extensions.each do |ext_name|
|
|
81
|
+
package = EXTENSION_PACKAGES[ext_name]
|
|
82
|
+
next unless package
|
|
83
|
+
|
|
84
|
+
package = package.gsub('{version}', version.to_s)
|
|
85
|
+
packages_to_install << package
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
ssh_executor.execute_on_host(host) do
|
|
89
|
+
unless packages_to_install.empty?
|
|
90
|
+
execute :sudo, 'apt-get', 'update', '-qq'
|
|
91
|
+
execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq',
|
|
92
|
+
*packages_to_install
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module ActivePostgres
|
|
2
|
+
module Components
|
|
3
|
+
class Monitoring < Base
|
|
4
|
+
def install
|
|
5
|
+
puts 'Installing postgres_exporter for monitoring...'
|
|
6
|
+
|
|
7
|
+
config.all_hosts.each do |host|
|
|
8
|
+
install_on_host(host)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def uninstall
|
|
13
|
+
puts 'Uninstalling postgres_exporter...'
|
|
14
|
+
|
|
15
|
+
config.all_hosts.each do |host|
|
|
16
|
+
ssh_executor.execute_on_host(host) do
|
|
17
|
+
execute :sudo, 'systemctl', 'stop', 'postgres_exporter'
|
|
18
|
+
execute :sudo, 'systemctl', 'disable', 'postgres_exporter'
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def restart
|
|
24
|
+
puts 'Restarting postgres_exporter...'
|
|
25
|
+
|
|
26
|
+
config.all_hosts.each do |host|
|
|
27
|
+
ssh_executor.execute_on_host(host) do
|
|
28
|
+
execute :sudo, 'systemctl', 'restart', 'postgres_exporter'
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def install_on_host(host)
|
|
36
|
+
puts " Installing postgres_exporter on #{host}..."
|
|
37
|
+
|
|
38
|
+
monitoring_config = config.component_config(:monitoring)
|
|
39
|
+
exporter_port = monitoring_config[:exporter_port] || 9187
|
|
40
|
+
|
|
41
|
+
# Download and install postgres_exporter
|
|
42
|
+
ssh_executor.execute_on_host(host) do
|
|
43
|
+
# Install via package manager or download binary
|
|
44
|
+
execute :sudo, 'apt-get', 'install', '-y', '-qq', 'prometheus-postgres-exporter'
|
|
45
|
+
|
|
46
|
+
# Enable and start
|
|
47
|
+
execute :sudo, 'systemctl', 'enable', 'prometheus-postgres-exporter'
|
|
48
|
+
execute :sudo, 'systemctl', 'start', 'prometheus-postgres-exporter'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
puts " Metrics available at: http://#{host}:#{exporter_port}/metrics"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
module ActivePostgres
|
|
2
|
+
module Components
|
|
3
|
+
class PgBackRest < Base
|
|
4
|
+
def install
|
|
5
|
+
puts 'Installing pgBackRest for backups...'
|
|
6
|
+
|
|
7
|
+
install_on_host(config.primary_host)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def uninstall
|
|
11
|
+
puts 'Uninstalling pgBackRest...'
|
|
12
|
+
|
|
13
|
+
ssh_executor.execute_on_host(config.primary_host) do
|
|
14
|
+
execute :sudo, 'apt-get', 'remove', '-y', 'pgbackrest'
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def restart
|
|
19
|
+
puts "pgBackRest is a backup tool and doesn't run as a service."
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run_backup(type = 'full')
|
|
23
|
+
puts "Running #{type} backup..."
|
|
24
|
+
postgres_user = config.postgres_user
|
|
25
|
+
|
|
26
|
+
ssh_executor.execute_on_host(config.primary_host) do
|
|
27
|
+
execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', "--type=#{type}", 'backup'
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def run_restore(backup_id)
|
|
32
|
+
puts "Restoring from backup #{backup_id}..."
|
|
33
|
+
postgres_user = config.postgres_user
|
|
34
|
+
|
|
35
|
+
ssh_executor.execute_on_host(config.primary_host) do
|
|
36
|
+
# Stop PostgreSQL
|
|
37
|
+
execute :sudo, 'systemctl', 'stop', 'postgresql'
|
|
38
|
+
|
|
39
|
+
# Restore
|
|
40
|
+
execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', "--set=#{backup_id}", 'restore'
|
|
41
|
+
|
|
42
|
+
# Start PostgreSQL
|
|
43
|
+
execute :sudo, 'systemctl', 'start', 'postgresql'
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def list_backups
|
|
48
|
+
puts 'Available backups:'
|
|
49
|
+
postgres_user = config.postgres_user
|
|
50
|
+
|
|
51
|
+
ssh_executor.execute_on_host(config.primary_host) do
|
|
52
|
+
execute :sudo, '-u', postgres_user, 'pgbackrest', 'info'
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def install_on_host(host)
|
|
59
|
+
puts " Installing pgBackRest on #{host}..."
|
|
60
|
+
|
|
61
|
+
pgbackrest_config = config.component_config(:pgbackrest)
|
|
62
|
+
postgres_user = config.postgres_user
|
|
63
|
+
secrets_obj = secrets
|
|
64
|
+
_ = pgbackrest_config # Used in ERB template
|
|
65
|
+
_ = secrets_obj # Used in ERB template
|
|
66
|
+
|
|
67
|
+
# Install package
|
|
68
|
+
ssh_executor.execute_on_host(host) do
|
|
69
|
+
execute :sudo, 'apt-get', 'install', '-y', '-qq', 'pgbackrest'
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Upload configuration
|
|
73
|
+
upload_template(host, 'pgbackrest.conf.erb', '/etc/pgbackrest.conf', binding, mode: '644')
|
|
74
|
+
|
|
75
|
+
ssh_executor.execute_on_host(host) do
|
|
76
|
+
execute :sudo, 'rm', '-rf', '/var/lib/pgbackrest', '||', 'true'
|
|
77
|
+
execute :sudo, 'mkdir', '-p', '/var/lib/pgbackrest'
|
|
78
|
+
execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", '/var/lib/pgbackrest'
|
|
79
|
+
execute :sudo, 'chmod', '750', '/var/lib/pgbackrest'
|
|
80
|
+
|
|
81
|
+
execute :sudo, 'rm', '-rf', '/var/log/pgbackrest', '||', 'true'
|
|
82
|
+
execute :sudo, 'mkdir', '-p', '/var/log/pgbackrest'
|
|
83
|
+
execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", '/var/log/pgbackrest'
|
|
84
|
+
|
|
85
|
+
execute :sudo, 'rm', '-rf', '/var/spool/pgbackrest', '||', 'true'
|
|
86
|
+
execute :sudo, 'mkdir', '-p', '/var/spool/pgbackrest'
|
|
87
|
+
execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", '/var/spool/pgbackrest'
|
|
88
|
+
|
|
89
|
+
execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', 'stanza-create'
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
module ActivePostgres
|
|
2
|
+
module Components
|
|
3
|
+
class PgBouncer < Base
|
|
4
|
+
def install
|
|
5
|
+
puts 'Installing PgBouncer for connection pooling...'
|
|
6
|
+
|
|
7
|
+
# Install on primary (can also install on standbys if needed)
|
|
8
|
+
install_on_host(config.primary_host)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def uninstall
|
|
12
|
+
puts 'Uninstalling PgBouncer...'
|
|
13
|
+
|
|
14
|
+
ssh_executor.execute_on_host(config.primary_host) do
|
|
15
|
+
execute :sudo, 'systemctl', 'stop', 'pgbouncer'
|
|
16
|
+
execute :sudo, 'apt-get', 'remove', '-y', 'pgbouncer'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def restart
|
|
21
|
+
puts 'Restarting PgBouncer...'
|
|
22
|
+
|
|
23
|
+
ssh_executor.execute_on_host(config.primary_host) do
|
|
24
|
+
execute :sudo, 'systemctl', 'restart', 'pgbouncer'
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def install_on_host(host)
|
|
31
|
+
puts " Installing PgBouncer on #{host}..."
|
|
32
|
+
|
|
33
|
+
# Get user config
|
|
34
|
+
user_config = config.component_config(:pgbouncer)
|
|
35
|
+
|
|
36
|
+
# Calculate optimal pool settings based on PostgreSQL max_connections
|
|
37
|
+
max_connections = get_postgres_max_connections(host)
|
|
38
|
+
optimal_pool = ConnectionPooler.calculate_optimal_pool_sizes(max_connections)
|
|
39
|
+
|
|
40
|
+
# Merge: user config overrides calculated settings
|
|
41
|
+
pgbouncer_config = optimal_pool.merge(user_config)
|
|
42
|
+
_ = pgbouncer_config # Used in ERB template
|
|
43
|
+
|
|
44
|
+
puts " Calculated pool settings for max_connections=#{max_connections}"
|
|
45
|
+
|
|
46
|
+
# Install package
|
|
47
|
+
ssh_executor.execute_on_host(host) do
|
|
48
|
+
execute :sudo, 'apt-get', 'install', '-y', '-qq', 'pgbouncer'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Upload configuration
|
|
52
|
+
upload_template(host, 'pgbouncer.ini.erb', '/etc/pgbouncer/pgbouncer.ini', binding, mode: '644')
|
|
53
|
+
|
|
54
|
+
# Create userlist with postgres superuser and app user
|
|
55
|
+
create_userlist(host)
|
|
56
|
+
|
|
57
|
+
# Enable and start
|
|
58
|
+
ssh_executor.execute_on_host(host) do
|
|
59
|
+
execute :sudo, 'systemctl', 'enable', 'pgbouncer'
|
|
60
|
+
execute :sudo, 'systemctl', 'restart', 'pgbouncer'
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def get_postgres_max_connections(host)
|
|
65
|
+
# Try to get max_connections from running PostgreSQL
|
|
66
|
+
postgres_user = config.postgres_user
|
|
67
|
+
max_conn = nil
|
|
68
|
+
|
|
69
|
+
ssh_executor.execute_on_host(host) do
|
|
70
|
+
result = capture(:sudo, '-u', postgres_user, 'psql', '-t', '-c', "'SHOW max_connections;'").strip
|
|
71
|
+
max_conn = result.to_i if result && !result.empty?
|
|
72
|
+
rescue StandardError
|
|
73
|
+
# PostgreSQL might not be running yet
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Fall back to config value or default
|
|
77
|
+
max_conn || config.component_config(:core).dig(:postgresql, :max_connections) || 100
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def create_userlist(host)
|
|
81
|
+
puts ' Creating PgBouncer userlist with database users...'
|
|
82
|
+
|
|
83
|
+
postgres_user = config.postgres_user
|
|
84
|
+
app_user = config.app_user
|
|
85
|
+
users_to_add = [postgres_user, (app_user if app_user != postgres_user)].compact
|
|
86
|
+
pgbouncer = self
|
|
87
|
+
|
|
88
|
+
ssh_executor.execute_on_host(host) do
|
|
89
|
+
backend = self
|
|
90
|
+
userlist_entries = users_to_add.filter_map { |user| pgbouncer.send(:fetch_user_hash, backend, user, postgres_user) }
|
|
91
|
+
pgbouncer.send(:write_userlist_file, backend, userlist_entries)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def fetch_user_hash(backend, user, postgres_user)
|
|
96
|
+
sql = build_user_hash_sql(user)
|
|
97
|
+
|
|
98
|
+
backend.upload! StringIO.new(sql), '/tmp/get_user_hash.sql'
|
|
99
|
+
backend.execute :chmod, '644', '/tmp/get_user_hash.sql'
|
|
100
|
+
user_hash = backend.capture(:sudo, '-u', postgres_user, 'psql', '-t', '-f', '/tmp/get_user_hash.sql').strip
|
|
101
|
+
backend.execute :rm, '-f', '/tmp/get_user_hash.sql'
|
|
102
|
+
|
|
103
|
+
if user_hash && !user_hash.empty?
|
|
104
|
+
puts " ✓ Added #{user} to PgBouncer userlist"
|
|
105
|
+
user_hash
|
|
106
|
+
else
|
|
107
|
+
warn " ⚠ User #{user} not found in PostgreSQL - create it first"
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
warn " ⚠ Warning: Could not get password hash for #{user}: #{e.message}"
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def build_user_hash_sql(user)
|
|
116
|
+
<<~SQL.strip
|
|
117
|
+
SELECT concat('"', rolname, '" "', rolpassword, '"')
|
|
118
|
+
FROM pg_authid
|
|
119
|
+
WHERE rolname = '#{user}'
|
|
120
|
+
SQL
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def write_userlist_file(backend, userlist_entries)
|
|
124
|
+
if userlist_entries.any?
|
|
125
|
+
userlist_content = "#{userlist_entries.join("\n")}\n"
|
|
126
|
+
backend.upload! StringIO.new(userlist_content), '/tmp/userlist.txt'
|
|
127
|
+
backend.execute :sudo, 'mv', '/tmp/userlist.txt', '/etc/pgbouncer/userlist.txt'
|
|
128
|
+
backend.execute :sudo, 'chmod', '640', '/etc/pgbouncer/userlist.txt'
|
|
129
|
+
backend.execute :sudo, 'chown', 'postgres:postgres', '/etc/pgbouncer/userlist.txt'
|
|
130
|
+
puts " ✓ Created userlist with #{userlist_entries.size} user(s)"
|
|
131
|
+
else
|
|
132
|
+
warn ' Warning: No users added to userlist - connections may fail'
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|