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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c89909c556964abfe7dbede0550c7fa1fbd60fc332e667fce5de858114ba2146
4
- data.tar.gz: e715719391d53f89cb394203018a5e2951f2602018ef97a417ee369d443e410c
3
+ metadata.gz: 12db7ee31476f31cae2fa436ade1c6fe3a94367b5013765603442df5fdc90feb
4
+ data.tar.gz: e31aa6b40e45b90b2c985e5347078e4886385e2a26c359c87ce5c951adf04eb9
5
5
  SHA512:
6
- metadata.gz: 603d5b28ef5730d595b24bf36b517148a882c5889d339675545fc98ffda547d8c1ead15ff8ee6a048fd6b117b5e257309460e7b5e25a9ee5eb4e55c730050ba4
7
- data.tar.gz: 50f632e74185dc90d7da5e6ece125fc625b04665cc4859ca7fd3d136e82df6e295b5fde10d8a2239675ce67a1a527f24ef8ec00cfeff79c57d84601a11168105
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
- enable pg_rewind support in repmgr:
190
+ use `repmgr node rejoin --force-rewind` once the node is ready to reattach:
191
191
 
192
- ```yaml
193
- components:
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
  '',
@@ -79,7 +79,7 @@ module ActivePostgres
79
79
  default: &default
80
80
  adapter: postgresql
81
81
  encoding: unicode
82
- pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
82
+ pool: <%= ENV.fetch("DB_POOL") { ENV.fetch("RAILS_MAX_THREADS") { 5 } } %>
83
83
 
84
84
  development:
85
85
  <<: *default
@@ -82,7 +82,7 @@ module ActivePostgres
82
82
  {
83
83
  'adapter' => 'postgresql',
84
84
  'encoding' => 'unicode',
85
- 'pool' => env_value('RAILS_MAX_THREADS', fallback: 5),
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']} " : ''
@@ -1,6 +1,7 @@
1
1
  require 'sshkit'
2
2
  require 'sshkit/dsl'
3
3
  require 'securerandom'
4
+ require 'stringio'
4
5
 
5
6
  module ActivePostgres
6
7
  class SSHExecutor
@@ -1,3 +1,3 @@
1
1
  module ActivePostgres
2
- VERSION = '0.9.1'.freeze
2
+ VERSION = '0.9.3'.freeze
3
3
  end
@@ -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
- <% if repmgr_config.key?(:use_rewind) %>
34
- use_rewind=<%= repmgr_config[:use_rewind] ? 'yes' : 'no' %>
35
- <% end %>
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
- cluster_csv=$(repmgr -f "$REPMGR_CONF" cluster show --csv 2>/dev/null || true)
16
- if [[ -z "$cluster_csv" ]]; then
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" "$cluster_csv" | awk -F',' 'NR>1 && tolower($3) ~ /primary/ {print $NF; exit}' | sed -n 's/.*host=\\([^ ]*\\).*/\\1/p')
21
- standby_hosts=$(printf "%s\n" "$cluster_csv" | awk -F',' 'NR>1 && tolower($3) ~ /standby/ {print $NF}' | sed -n 's/.*host=\\([^ ]*\\).*/\\1/p' | sort -u)
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' && (sudo systemctl reload dnsmasq || sudo systemctl restart dnsmasq)"; then
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
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.1
4
+ version: 0.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - BoringCache