active_postgres 0.9.2 → 0.9.4

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: e51d5f1214cedf497a9b060e5c667c43711f9ff5e36024bfd8dc926b50fe272f
4
- data.tar.gz: 618fa70d39a43e1c74ba2ed449b75138670bca92cb6bf418606f772de34c6a7d
3
+ metadata.gz: b0966aed96fa6f906e7b048ef7804a0b9994c54f841f8dc9f00425f57f565a71
4
+ data.tar.gz: 86582950fc4afa733cba7052682dbaf0f3e76275c9397559f2d3398c8f92da86
5
5
  SHA512:
6
- metadata.gz: 254b6fb4342d4e510076d05ebfbeaa0e7990b463df13ad6ddb7d6eb5a70562c23cc4f502ce6c435c6372fc3dd5f695285ee3fdfed522cc188f1b1cc21c6cc90d
7
- data.tar.gz: d9147a4679529344ccb0ffd3d5c2ffacc3072464b9cf4e1c6e11150a66c142ef0d0b45f9b9cd9d34eceba6f4d0e50baa7c2f227a5b2b1719502972e2b4b39ee4
6
+ metadata.gz: 18644b19453727a1b14918f9bc7d055fdf2c8cbaab962d94acdb058be4fa6b969e72e569ea5e1a2f0848cc45433e35c47dfa69af699370cb267b64c0a61114d5
7
+ data.tar.gz: '08b112f2121747a9b970809bf0c662dfbed99166edf6e3b18e88daab4fff0a65fcae13821c37fa69af218497b9ac05978d2bf6cd157c197d0f719f968088f407'
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
@@ -43,6 +43,14 @@ module ActivePostgres
43
43
  content = render_template(template_name, binding_context)
44
44
  ssh_executor.upload_file(host, content, remote_path, mode: mode, owner: owner)
45
45
  end
46
+
47
+ def install_apt_packages(host, *packages)
48
+ ssh_executor.execute_on_host(host) do
49
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get',
50
+ '-o', 'DPkg::Lock::Timeout=300',
51
+ 'install', '-y', '-qq', *packages
52
+ end
53
+ end
46
54
  end
47
55
  end
48
56
  end
@@ -65,10 +65,9 @@ module ActivePostgres
65
65
  return if packages_to_install.empty?
66
66
 
67
67
  ssh_executor.execute_on_host(host) do
68
- execute :sudo, 'apt-get', 'update', '-qq'
69
- execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq',
70
- *packages_to_install
68
+ execute :sudo, 'apt-get', '-o', 'DPkg::Lock::Timeout=300', 'update', '-qq'
71
69
  end
70
+ install_apt_packages(host, *packages_to_install)
72
71
  end
73
72
 
74
73
  def install_on_primary(extensions)
@@ -60,10 +60,7 @@ module ActivePostgres
60
60
  exporter_port = monitoring_config[:exporter_port] || 9187
61
61
 
62
62
  # Download and install postgres_exporter
63
- ssh_executor.execute_on_host(host) do
64
- # Install via package manager or download binary
65
- execute :sudo, 'apt-get', 'install', '-y', '-qq', 'prometheus-postgres-exporter'
66
- end
63
+ install_apt_packages(host, 'prometheus-postgres-exporter')
67
64
 
68
65
  configure_exporter_service(host, monitoring_config, exporter_port)
69
66
 
@@ -157,9 +154,7 @@ module ActivePostgres
157
154
  listen_address = monitoring_config[:node_exporter_listen_address].to_s.strip
158
155
  puts " Installing node_exporter on #{host}..."
159
156
 
160
- ssh_executor.execute_on_host(host) do
161
- execute :sudo, 'apt-get', 'install', '-y', '-qq', 'prometheus-node-exporter'
162
- end
157
+ install_apt_packages(host, 'prometheus-node-exporter')
163
158
 
164
159
  configure_node_exporter_service(host, port, listen_address)
165
160
 
@@ -213,12 +208,15 @@ module ActivePostgres
213
208
  puts "Installing Grafana on #{host}..."
214
209
 
215
210
  ssh_executor.execute_on_host(host) do
216
- execute :sudo, 'apt-get', 'install', '-y', '-qq', 'apt-transport-https', 'software-properties-common', 'wget'
211
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get',
212
+ '-o', 'DPkg::Lock::Timeout=300', 'install', '-y', '-qq',
213
+ 'apt-transport-https', 'software-properties-common', 'wget'
217
214
  execute :sudo, 'wget', '-q', '-O', '/usr/share/keyrings/grafana.gpg', 'https://apt.grafana.com/gpg.key'
218
215
  execute :sudo, 'sh', '-c',
219
216
  'echo "deb [signed-by=/usr/share/keyrings/grafana.gpg] https://apt.grafana.com stable main" > /etc/apt/sources.list.d/grafana.list'
220
- execute :sudo, 'apt-get', 'update', '-qq'
221
- execute :sudo, 'apt-get', 'install', '-y', '-qq', 'grafana'
217
+ execute :sudo, 'apt-get', '-o', 'DPkg::Lock::Timeout=300', 'update', '-qq'
218
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get',
219
+ '-o', 'DPkg::Lock::Timeout=300', 'install', '-y', '-qq', 'grafana'
222
220
  execute :sudo, 'systemctl', 'enable', '--now', 'grafana-server'
223
221
  execute :sudo, 'grafana-cli', 'admin', 'reset-admin-password', admin_password
224
222
  end
@@ -1,6 +1,12 @@
1
+ require 'shellwords'
2
+
1
3
  module ActivePostgres
2
4
  module Components
3
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'
9
+
4
10
  def install
5
11
  puts 'Installing pgBackRest for backups...'
6
12
 
@@ -21,6 +27,9 @@ module ActivePostgres
21
27
  execute :sudo, 'apt-get', 'remove', '-y', 'pgbackrest'
22
28
  execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup'
23
29
  execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-incremental'
30
+ execute :sudo, 'rm', '-f', LOG_ARCHIVE_CRON_FILE
31
+ execute :sudo, 'rm', '-f', LOG_ARCHIVE_ENV_FILE
32
+ execute :sudo, 'rm', '-f', LOG_ARCHIVE_SCRIPT
24
33
  end
25
34
  end
26
35
  end
@@ -105,10 +114,7 @@ module ActivePostgres
105
114
  _ = pgbackrest_config # Used in ERB template
106
115
  _ = secrets_obj # Used in ERB template
107
116
 
108
- # Install package
109
- ssh_executor.execute_on_host(host) do
110
- execute :sudo, 'apt-get', 'install', '-y', '-qq', 'pgbackrest'
111
- end
117
+ install_apt_packages(host, 'pgbackrest')
112
118
 
113
119
  # Upload configuration
114
120
  upload_template(host, 'pgbackrest.conf.erb', '/etc/pgbackrest.conf', binding,
@@ -134,13 +140,22 @@ module ActivePostgres
134
140
  end
135
141
  end
136
142
 
143
+ if log_archive_enabled?(pgbackrest_config)
144
+ setup_log_archive(host, pgbackrest_config)
145
+ else
146
+ remove_log_archive(host)
147
+ end
148
+
137
149
  # Set up scheduled backups on primary only
138
150
  if create_stanza
139
151
  setup_backup_schedules(host, pgbackrest_config)
152
+ else
153
+ remove_backup_schedule(host)
140
154
  end
141
155
  end
142
156
 
143
157
  def setup_backup_schedules(host, pgbackrest_config)
158
+ remove_backup_schedule(host)
144
159
  schedules = backup_schedules(pgbackrest_config)
145
160
  schedules.each do |entry|
146
161
  setup_backup_schedule(host, entry[:schedule], entry[:type], entry[:file])
@@ -186,6 +201,75 @@ module ActivePostgres
186
201
  execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-incremental'
187
202
  end
188
203
  end
204
+
205
+ def setup_log_archive(host, pgbackrest_config)
206
+ 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
210
+
211
+ postgres_user = config.postgres_user
212
+ env_content = log_archive_env(pgbackrest_config, log_archive_config, host)
213
+ cron_content = log_archive_cron(log_archive_config)
214
+
215
+ install_apt_packages(host, 'awscli')
216
+
217
+ ssh_executor.upload_file(host, env_content, LOG_ARCHIVE_ENV_FILE, mode: '640', owner: "root:#{postgres_user}")
218
+
219
+ upload_template(host, 'postgres_log_archive.sh.erb', LOG_ARCHIVE_SCRIPT, binding,
220
+ mode: '750', owner: "root:#{postgres_user}")
221
+ ssh_executor.upload_file(host, cron_content, LOG_ARCHIVE_CRON_FILE, mode: '644', owner: 'root:root')
222
+ end
223
+
224
+ def remove_log_archive(host)
225
+ ssh_executor.execute_on_host(host) do
226
+ execute :sudo, 'rm', '-f', LOG_ARCHIVE_CRON_FILE
227
+ execute :sudo, 'rm', '-f', LOG_ARCHIVE_ENV_FILE
228
+ execute :sudo, 'rm', '-f', LOG_ARCHIVE_SCRIPT
229
+ end
230
+ end
231
+
232
+ def log_archive_enabled?(pgbackrest_config)
233
+ (pgbackrest_config[:log_archive] || {})[:enabled] == true
234
+ end
235
+
236
+ def log_archive_cron(log_archive_config)
237
+ schedule = log_archive_config[:schedule] || '17 3 * * *'
238
+ <<~CRON
239
+ # PostgreSQL text log archive (managed by active_postgres)
240
+ SHELL=/bin/bash
241
+ PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
242
+ #{schedule} root #{LOG_ARCHIVE_SCRIPT}
243
+ CRON
244
+ end
245
+
246
+ def log_archive_env(pgbackrest_config, log_archive_config, host)
247
+ access_key = secrets.resolve('s3_access_key')
248
+ secret_key = secrets.resolve('s3_secret_key')
249
+ raise Error, 's3_access_key secret is required for pgbackrest.log_archive' if access_key.to_s.empty?
250
+ raise Error, 's3_secret_key secret is required for pgbackrest.log_archive' if secret_key.to_s.empty?
251
+
252
+ env = {
253
+ 'AWS_ACCESS_KEY_ID' => access_key,
254
+ 'AWS_SECRET_ACCESS_KEY' => secret_key,
255
+ 'AWS_DEFAULT_REGION' => pgbackrest_config[:s3_region] || 'us-east-1',
256
+ 'AWS_BUCKET' => pgbackrest_config[:s3_bucket],
257
+ 'AWS_ENDPOINT_URL' => s3_endpoint_url(pgbackrest_config),
258
+ 'POSTGRES_LOG_ARCHIVE_LOG_DIR' => log_archive_config[:log_directory] || '/var/log/postgresql',
259
+ 'POSTGRES_LOG_ARCHIVE_PREFIX' => log_archive_config[:prefix] || 'postgres-logs',
260
+ 'POSTGRES_LOG_ARCHIVE_NODE' => config.node_label_for(host) || host
261
+ }.compact
262
+
263
+ env.map { |key, value| "#{key}=#{Shellwords.escape(value.to_s)}" }.join("\n") + "\n"
264
+ end
265
+
266
+ def s3_endpoint_url(pgbackrest_config)
267
+ endpoint = pgbackrest_config[:s3_endpoint]
268
+ return nil if endpoint.to_s.empty?
269
+ return endpoint if endpoint.match?(/\Ahttps?:\/\//)
270
+
271
+ "https://#{endpoint}"
272
+ end
189
273
  end
190
274
  end
191
275
  end
@@ -60,15 +60,7 @@ module ActivePostgres
60
60
 
61
61
  user_config = config.component_config(:pgbouncer)
62
62
 
63
- # Determine follow_primary behavior:
64
- # - Primary: use global follow_primary setting
65
- # - Standby: check per-standby pgbouncer_follow_primary, default false (use localhost for read replicas)
66
- if is_standby
67
- standby_config = config.standby_config_for(host) || {}
68
- follow_primary = standby_config['pgbouncer_follow_primary'] == true
69
- else
70
- follow_primary = user_config[:follow_primary] == true
71
- end
63
+ follow_primary = follow_primary_for?(host, is_standby: is_standby, user_config: user_config)
72
64
 
73
65
  if follow_primary && !config.component_enabled?(:repmgr)
74
66
  raise Error, 'PgBouncer follow_primary requires repmgr to be enabled'
@@ -89,9 +81,7 @@ module ActivePostgres
89
81
 
90
82
  puts " Calculated pool settings for max_connections=#{max_connections}"
91
83
 
92
- ssh_executor.execute_on_host(host) do
93
- execute :sudo, 'apt-get', 'install', '-y', '-qq', 'pgbouncer'
94
- end
84
+ install_apt_packages(host, 'pgbouncer')
95
85
 
96
86
  upload_template(host, 'pgbouncer.ini.erb', '/etc/pgbouncer/pgbouncer.ini', binding, mode: '644')
97
87
 
@@ -99,14 +89,34 @@ module ActivePostgres
99
89
 
100
90
  create_userlist(host)
101
91
 
92
+ ensure_firewall_port_open(host, pgbouncer_config[:listen_port] || 6432)
93
+
102
94
  ssh_executor.execute_on_host(host) do
103
95
  execute :sudo, 'systemctl', 'enable', 'pgbouncer'
104
96
  execute :sudo, 'systemctl', 'restart', 'pgbouncer'
97
+ unless follow_primary
98
+ execute :sudo, 'systemctl', 'disable', '--now', 'pgbouncer-follow-primary.timer', '||', 'true'
99
+ execute :sudo, 'rm', '-f', '/usr/local/bin/pgbouncer-follow-primary',
100
+ '/etc/systemd/system/pgbouncer-follow-primary.service',
101
+ '/etc/systemd/system/pgbouncer-follow-primary.timer'
102
+ execute :sudo, 'systemctl', 'daemon-reload'
103
+ end
105
104
  end
106
105
 
107
106
  install_follow_primary(host, pgbouncer_config) if follow_primary
108
107
  end
109
108
 
109
+ def follow_primary_for?(host, is_standby:, user_config:)
110
+ if is_standby
111
+ standby_config = config.standby_config_for(host) || {}
112
+ standby_override = standby_config['pgbouncer_follow_primary']
113
+ standby_override = standby_config[:pgbouncer_follow_primary] if standby_override.nil?
114
+ return standby_override == true unless standby_override.nil?
115
+ end
116
+
117
+ user_config[:follow_primary] == true || user_config['follow_primary'] == true
118
+ end
119
+
110
120
  def setup_ssl_certs(host)
111
121
  puts ' Setting up SSL certificates for PgBouncer...'
112
122
  version = config.version
@@ -194,15 +204,28 @@ module ActivePostgres
194
204
  end
195
205
  end
196
206
 
207
+ def ensure_firewall_port_open(host, port)
208
+ ssh_executor.execute_on_host(host) do
209
+ has_reject = test(:sudo, 'iptables', '-C', 'INPUT', '-j', 'REJECT', '2>/dev/null')
210
+ if has_reject
211
+ execute :sudo, 'iptables', '-I', 'INPUT', '-p', 'tcp', '--dport', port.to_s, '-j', 'ACCEPT'
212
+ execute :sudo, 'sh', '-c', "'iptables-save > /etc/iptables/rules.v4 2>/dev/null || true'"
213
+ puts " ✓ Opened port #{port} in iptables"
214
+ end
215
+ end
216
+ end
217
+
197
218
  def install_follow_primary(host, pgbouncer_config)
198
219
  interval = pgbouncer_config[:follow_primary_interval] || 5
199
220
  interval = interval.to_i
200
221
  interval = 5 if interval <= 0
201
222
 
202
223
  repmgr_conf = '/etc/repmgr.conf'
224
+ repmgr_database = config.repmgr_database
203
225
  postgres_user = config.postgres_user
204
226
  _ = interval
205
227
  _ = repmgr_conf
228
+ _ = repmgr_database
206
229
  _ = postgres_user
207
230
 
208
231
  upload_template(host, 'pgbouncer_follow_primary.sh.erb', '/usr/local/bin/pgbouncer-follow-primary', binding,
@@ -11,6 +11,7 @@ module ActivePostgres
11
11
  setup_standby(host)
12
12
  end
13
13
 
14
+ setup_inter_node_ssh if config.standby_hosts.any?
14
15
  setup_dns_failover if dns_failover_enabled?
15
16
 
16
17
  # Verify the entire cluster is healthy after setup
@@ -64,10 +65,7 @@ module ActivePostgres
64
65
  puts "Setting up standby #{standby_host} (primary will not be touched)..."
65
66
 
66
67
  version = config.version
67
- ssh_executor.execute_on_host(standby_host) do
68
- execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq',
69
- "postgresql-#{version}-repmgr"
70
- end
68
+ install_apt_packages(standby_host, "postgresql-#{version}-repmgr")
71
69
 
72
70
  setup_standby(standby_host)
73
71
 
@@ -81,10 +79,7 @@ module ActivePostgres
81
79
 
82
80
  version = config.version
83
81
  config.all_hosts.each do |host|
84
- ssh_executor.execute_on_host(host) do
85
- execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq',
86
- "postgresql-#{version}-repmgr"
87
- end
82
+ install_apt_packages(host, "postgresql-#{version}-repmgr")
88
83
  install_postgres_sudoers(host)
89
84
  end
90
85
  end
@@ -275,6 +270,7 @@ module ActivePostgres
275
270
  update_postgres_configs_on_standby(standby_host, version)
276
271
  ensure_ssl_certs(standby_host, version) if config.component_enabled?(:ssl)
277
272
  register_standby_with_primary(standby_host)
273
+ setup_inter_node_ssh
278
274
  enable_repmgrd_if_configured(standby_host, repmgr_config)
279
275
  return
280
276
  end
@@ -428,9 +424,7 @@ module ActivePostgres
428
424
  pub_keys = {}
429
425
 
430
426
  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
427
+ pub_keys[host] = ensure_dns_ssh_key(host, key_path, [])
434
428
  end
435
429
 
436
430
  all_hosts.each do |host|
@@ -1073,13 +1067,23 @@ module ActivePostgres
1073
1067
  def enable_repmgrd_if_configured(host, repmgr_config)
1074
1068
  return if repmgr_config[:auto_failover] == false
1075
1069
 
1070
+ default_config = <<~CONF
1071
+ REPMGRD_ENABLED=yes
1072
+ REPMGRD_CONF="/etc/repmgr.conf"
1073
+ REPMGRD_OPTS=""
1074
+ REPMGRD_USER=postgres
1075
+ REPMGRD_BIN=/usr/bin/repmgrd
1076
+ REPMGRD_PIDFILE=/var/run/repmgrd.pid
1077
+ CONF
1078
+
1076
1079
  ssh_executor.execute_on_host(host) do
1077
- begin
1078
- execute :sudo, 'systemctl', 'enable', 'repmgrd'
1079
- execute :sudo, 'systemctl', 'restart', 'repmgrd'
1080
- rescue StandardError
1081
- nil
1082
- end
1080
+ upload! StringIO.new(default_config), '/tmp/repmgrd-default'
1081
+ execute :sudo, 'mv', '/tmp/repmgrd-default', '/etc/default/repmgrd'
1082
+ execute :sudo, 'chown', 'root:root', '/etc/default/repmgrd'
1083
+ execute :sudo, 'chmod', '644', '/etc/default/repmgrd'
1084
+ execute :sudo, 'systemctl', 'enable', 'repmgrd'
1085
+ execute :sudo, 'systemctl', 'restart', 'repmgrd'
1086
+ execute :pgrep, '-x', 'repmgrd'
1083
1087
  end
1084
1088
  end
1085
1089
 
@@ -240,8 +240,8 @@ module ActivePostgres
240
240
 
241
241
  @ssh_executor.execute_on_host(host) do
242
242
  # Install PgBouncer package
243
- execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install',
244
- '-y', '-qq', 'pgbouncer'
243
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get',
244
+ '-o', 'DPkg::Lock::Timeout=300', 'install', '-y', '-qq', 'pgbouncer'
245
245
 
246
246
  # Create required directories
247
247
  execute :sudo, 'mkdir', '-p', '/var/log/pgbouncer'
@@ -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
@@ -50,6 +50,8 @@ shared: &shared
50
50
  enabled: false
51
51
  # Optional: Override default pgbouncer user
52
52
  # user: pgbouncer
53
+ # Keep PgBouncer on every node pointed at the current primary.
54
+ # Set standby.pgbouncer_follow_primary: false on a specific standby to keep it local.
53
55
  # follow_primary: true
54
56
  # follow_primary_interval: 5
55
57
 
@@ -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('- ', '_')
@@ -50,9 +50,10 @@ module ActivePostgres
50
50
  '/etc/apt/sources.list.d/pgdg.list'
51
51
  end
52
52
 
53
- execute :sudo, 'apt-get', 'update', '-qq'
54
- execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq', 'gnupg', 'wget',
55
- 'lsb-release', 'locales'
53
+ execute :sudo, 'apt-get', '-o', 'DPkg::Lock::Timeout=300', 'update', '-qq'
54
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get',
55
+ '-o', 'DPkg::Lock::Timeout=300', 'install', '-y', '-qq',
56
+ 'gnupg', 'wget', 'lsb-release', 'locales'
56
57
 
57
58
  info 'Generating locales...'
58
59
  execute :sudo, 'locale-gen', 'en_US.UTF-8'
@@ -73,8 +74,9 @@ module ActivePostgres
73
74
  execute :sudo, 'systemctl', 'stop', 'postgresql' if test('systemctl is-active postgresql')
74
75
 
75
76
  # Remove all PostgreSQL packages
76
- execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'remove', '--purge', '-y', '-qq', 'postgresql*'
77
- execute :sudo, 'apt-get', 'autoremove', '-y', '-qq'
77
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get',
78
+ '-o', 'DPkg::Lock::Timeout=300', 'remove', '--purge', '-y', '-qq', 'postgresql*'
79
+ execute :sudo, 'apt-get', '-o', 'DPkg::Lock::Timeout=300', 'autoremove', '-y', '-qq'
78
80
 
79
81
  # Clean up OLD version directories only (preserve target version SSL certs, etc.)
80
82
  other_versions.each do |old_version|
@@ -103,9 +105,10 @@ module ActivePostgres
103
105
  "/etc/apt/sources.list.d/pgdg.list'"
104
106
  execute :sudo, 'sh', '-c', pgdg_repo
105
107
 
106
- execute :sudo, 'apt-get', 'update', '-qq'
107
- execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq', "postgresql-#{version}",
108
- "postgresql-client-#{version}"
108
+ execute :sudo, 'apt-get', '-o', 'DPkg::Lock::Timeout=300', 'update', '-qq'
109
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get',
110
+ '-o', 'DPkg::Lock::Timeout=300', 'install', '-y', '-qq',
111
+ "postgresql-#{version}", "postgresql-client-#{version}"
109
112
 
110
113
  execute :sudo, 'systemctl', 'enable', 'postgresql'
111
114
  execute :sudo, 'systemctl', 'start', 'postgresql' unless test('systemctl is-active postgresql')
@@ -1,3 +1,3 @@
1
1
  module ActivePostgres
2
- VERSION = '0.9.2'.freeze
2
+ VERSION = '0.9.4'.freeze
3
3
  end
@@ -97,13 +97,14 @@ namespace :postgres do
97
97
 
98
98
  # Remove packages
99
99
  begin
100
- execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'remove', '--purge', '-y',
100
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get',
101
+ '-o', 'DPkg::Lock::Timeout=300', 'remove', '--purge', '-y',
101
102
  'postgresql*', 'pgbouncer', 'repmgr', 'prometheus-postgres-exporter'
102
103
  rescue StandardError
103
104
  nil
104
105
  end
105
106
  begin
106
- execute :sudo, 'apt-get', 'autoremove', '-y'
107
+ execute :sudo, 'apt-get', '-o', 'DPkg::Lock::Timeout=300', 'autoremove', '-y'
107
108
  rescue StandardError
108
109
  nil
109
110
  end
@@ -56,8 +56,10 @@ namespace :postgres do
56
56
  puts "\nUpdating #{standby}..."
57
57
  ssh_executor.execute_on_host(standby) do
58
58
  execute :sudo, 'systemctl', 'stop', "postgresql@#{current_version}-main"
59
- execute :sudo, 'apt-get', 'update'
60
- execute :sudo, 'apt-get', 'install', '-y', "postgresql-#{new_version}", "postgresql-contrib-#{new_version}"
59
+ execute :sudo, 'apt-get', '-o', 'DPkg::Lock::Timeout=300', 'update'
60
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get',
61
+ '-o', 'DPkg::Lock::Timeout=300', 'install', '-y',
62
+ "postgresql-#{new_version}", "postgresql-contrib-#{new_version}"
61
63
 
62
64
  puts "Upgrading cluster from #{current_version} to #{new_version}..."
63
65
  execute :sudo, 'pg_upgradecluster', current_version.to_s, 'main'
@@ -99,8 +101,10 @@ namespace :postgres do
99
101
  puts "\nUpdating #{primary}..."
100
102
  ssh_executor.execute_on_host(primary) do
101
103
  execute :sudo, 'systemctl', 'stop', "postgresql@#{current_version}-main"
102
- execute :sudo, 'apt-get', 'update'
103
- execute :sudo, 'apt-get', 'install', '-y', "postgresql-#{new_version}", "postgresql-contrib-#{new_version}"
104
+ execute :sudo, 'apt-get', '-o', 'DPkg::Lock::Timeout=300', 'update'
105
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get',
106
+ '-o', 'DPkg::Lock::Timeout=300', 'install', '-y',
107
+ "postgresql-#{new_version}", "postgresql-contrib-#{new_version}"
104
108
 
105
109
  puts "Upgrading cluster from #{current_version} to #{new_version}..."
106
110
  execute :sudo, 'pg_upgradecluster', current_version.to_s, 'main'
@@ -156,8 +160,9 @@ namespace :postgres do
156
160
 
157
161
  puts "\n📋 Step 1: Patch standby #{standby}"
158
162
  ssh_executor.execute_on_host(standby) do
159
- execute :sudo, 'apt-get', 'update'
160
- execute :sudo, 'apt-get', 'install', '--only-upgrade', '-y', "postgresql-#{version}"
163
+ execute :sudo, 'apt-get', '-o', 'DPkg::Lock::Timeout=300', 'update'
164
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get',
165
+ '-o', 'DPkg::Lock::Timeout=300', 'install', '--only-upgrade', '-y', "postgresql-#{version}"
161
166
  execute :sudo, 'systemctl', 'restart', "postgresql@#{version}-main"
162
167
  end
163
168
  puts '✓ Standby patched and restarted'
@@ -169,8 +174,9 @@ namespace :postgres do
169
174
 
170
175
  puts "\n📋 Step 3: Patch old primary #{primary}"
171
176
  ssh_executor.execute_on_host(primary) do
172
- execute :sudo, 'apt-get', 'update'
173
- execute :sudo, 'apt-get', 'install', '--only-upgrade', '-y', "postgresql-#{version}"
177
+ execute :sudo, 'apt-get', '-o', 'DPkg::Lock::Timeout=300', 'update'
178
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get',
179
+ '-o', 'DPkg::Lock::Timeout=300', 'install', '--only-upgrade', '-y', "postgresql-#{version}"
174
180
  execute :sudo, 'systemctl', 'restart', "postgresql@#{version}-main"
175
181
  end
176
182
  puts '✓ Old primary patched'
@@ -211,8 +217,9 @@ namespace :postgres do
211
217
  puts "\n🔄 Starting in-place upgrade..."
212
218
  ssh_executor.execute_on_host(host) do
213
219
  execute :sudo, 'systemctl', 'stop', "postgresql@#{current_version}-main"
214
- execute :sudo, 'apt-get', 'update'
215
- execute :sudo, 'apt-get', 'install', '-y', "postgresql-#{new_version}"
220
+ execute :sudo, 'apt-get', '-o', 'DPkg::Lock::Timeout=300', 'update'
221
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get',
222
+ '-o', 'DPkg::Lock::Timeout=300', 'install', '-y', "postgresql-#{new_version}"
216
223
 
217
224
  execute :sudo, 'pg_upgradecluster', current_version.to_s, 'main'
218
225
  execute :sudo, 'pg_dropcluster', '--stop', current_version.to_s, 'main'
@@ -2,17 +2,19 @@
2
2
  set -euo pipefail
3
3
 
4
4
  REPMGR_CONF="<%= repmgr_conf %>"
5
+ REPMGR_DB="<%= repmgr_database %>"
5
6
  PGBOUNCER_INI="/etc/pgbouncer/pgbouncer.ini"
6
7
  PGBOUNCER_SERVICE="pgbouncer"
7
8
  LOG_TAG="pgbouncer-follow-primary"
8
9
 
9
- cluster_csv=$(/usr/bin/sudo -u <%= postgres_user %> env HOME=/var/lib/postgresql \
10
- repmgr -f "$REPMGR_CONF" cluster show --csv 2>/dev/null || true)
11
- if [[ -z "$cluster_csv" ]]; then
10
+ cluster_nodes=$(/usr/bin/sudo -u <%= postgres_user %> env HOME=/var/lib/postgresql \
11
+ psql -h /var/run/postgresql -d "$REPMGR_DB" -tAF',' \
12
+ -c "SELECT type, conninfo FROM repmgr.nodes WHERE active = true" 2>/dev/null || true)
13
+ if [[ -z "$cluster_nodes" ]]; then
12
14
  exit 0
13
15
  fi
14
16
 
15
- primary_conninfo=$(printf "%s\n" "$cluster_csv" | awk -F',' 'NR>1 && tolower($3) ~ /primary/ {print $NF; exit}')
17
+ primary_conninfo=$(printf "%s\n" "$cluster_nodes" | awk -F',' '$1 == "primary" {print $2; exit}')
16
18
  if [[ -z "$primary_conninfo" ]]; then
17
19
  exit 0
18
20
  fi
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ umask 077
5
+
6
+ ENV_FILE="/etc/active-postgres-log-archive.env"
7
+ if [[ ! -r "$ENV_FILE" ]]; then
8
+ echo "missing $ENV_FILE" >&2
9
+ exit 1
10
+ fi
11
+
12
+ set -a
13
+ source "$ENV_FILE"
14
+ set +a
15
+
16
+ LOG_DIR="${POSTGRES_LOG_ARCHIVE_LOG_DIR:-/var/log/postgresql}"
17
+ WORK_DIR="${POSTGRES_LOG_ARCHIVE_WORK_DIR:-/var/tmp/active-postgres-log-archive}"
18
+ PREFIX="${POSTGRES_LOG_ARCHIVE_PREFIX:-postgres-logs}"
19
+ NODE="${POSTGRES_LOG_ARCHIVE_NODE:-$(hostname -f 2>/dev/null || hostname)}"
20
+ DATE_PATH="$(date -u +%Y/%m/%d)"
21
+ STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
22
+ LOCK_FILE="/var/run/active-postgres-log-archive.lock"
23
+
24
+ mkdir -p "$WORK_DIR"
25
+ exec 9>"$LOCK_FILE"
26
+ flock -n 9 || exit 0
27
+
28
+ shopt -s nullglob
29
+ log_files=("$LOG_DIR"/postgresql-*.log "$LOG_DIR"/repmgr.log)
30
+ if [[ ${#log_files[@]} -eq 0 ]]; then
31
+ exit 0
32
+ fi
33
+
34
+ archive="$WORK_DIR/postgresql-logs-${NODE}-${STAMP}.tar.gz"
35
+ cleanup() {
36
+ rm -f "$archive"
37
+ }
38
+ trap cleanup EXIT
39
+
40
+ tar_paths=()
41
+ for file in "${log_files[@]}"; do
42
+ [[ -f "$file" ]] && tar_paths+=("${file#/}")
43
+ done
44
+
45
+ if [[ ${#tar_paths[@]} -eq 0 ]]; then
46
+ exit 0
47
+ fi
48
+
49
+ tar -czf "$archive" -C / "${tar_paths[@]}"
50
+
51
+ destination="s3://${AWS_BUCKET}/${PREFIX%/}/${NODE}/${DATE_PATH}/$(basename "$archive")"
52
+ aws_args=(s3 cp "$archive" "$destination" --only-show-errors)
53
+ if [[ -n "${AWS_ENDPOINT_URL:-}" ]]; then
54
+ aws_args+=(--endpoint-url "$AWS_ENDPOINT_URL")
55
+ fi
56
+
57
+ aws "${aws_args[@]}"
@@ -95,8 +95,12 @@ max_parallel_maintenance_workers = <%= pg_config[:max_parallel_maintenance_worke
95
95
  logging_collector = <%= pg_config[:logging_collector] || 'on' %>
96
96
  log_directory = '<%= pg_config[:log_directory] || 'log' %>'
97
97
  log_filename = '<%= pg_config[:log_filename] || 'postgresql-%Y-%m-%d_%H%M%S.log' %>'
98
+ log_file_mode = <%= pg_config[:log_file_mode] || '0600' %>
98
99
  log_rotation_age = <%= pg_config[:log_rotation_age] || '1d' %>
99
100
  log_rotation_size = <%= pg_config[:log_rotation_size] || '100MB' %>
101
+ <% if pg_config[:log_truncate_on_rotation] %>
102
+ log_truncate_on_rotation = <%= pg_config[:log_truncate_on_rotation] %>
103
+ <% end %>
100
104
  log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
101
105
  log_timezone = 'UTC'
102
106
  <% if pg_config[:log_checkpoints] %>
@@ -143,7 +147,7 @@ jit = <%= pg_config[:jit] %>
143
147
  <% end %>
144
148
 
145
149
  # Additional settings
146
- shared_preload_libraries = '<%= pg_config[:shared_preload_libraries] || 'pg_stat_statements' %>'
150
+ shared_preload_libraries = '<%= pg_config[:shared_preload_libraries] || (config.component_enabled?(:repmgr) ? 'pg_stat_statements, repmgr' : 'pg_stat_statements') %>'
147
151
  <% if pg_config[:'pg_stat_statements.max'] %>
148
152
  pg_stat_statements.max = <%= pg_config[:'pg_stat_statements.max'] %>
149
153
  <% end %>
@@ -157,5 +161,3 @@ pg_stat_statements.track = <%= pg_config[:'pg_stat_statements.track'] %>
157
161
  <%= key %> = <%= value %>
158
162
  <% end %>
159
163
  <% end %>
160
-
161
-
@@ -36,10 +36,7 @@ service_stop_command='sudo systemctl stop postgresql@<%= config.version %>-main'
36
36
  service_restart_command='sudo systemctl restart postgresql@<%= config.version %>-main'
37
37
  service_reload_command='sudo systemctl reload postgresql@<%= config.version %>-main'
38
38
 
39
- ssh_options='-i /var/lib/postgresql/.ssh/active_postgres_dns -o StrictHostKeyChecking=no'
40
- <% if repmgr_config.key?(:use_rewind) %>
41
- use_rewind=<%= repmgr_config[:use_rewind] ? 'yes' : 'no' %>
42
- <% end %>
39
+ ssh_options='-l <%= repmgr_config[:ssh_user] || config.postgres_user %> -i /var/lib/postgresql/.ssh/active_postgres_dns -o IdentitiesOnly=yes -o StrictHostKeyChecking=no'
43
40
 
44
41
  log_level=INFO
45
42
  log_facility=STDERR
@@ -6,6 +6,7 @@ DNS_USER="<%= dns_user %>"
6
6
  DNS_SERVERS=(<%= dns_servers.map(&:to_s).join(' ') %>)
7
7
  DNS_SSH_KEY="<%= dns_ssh_key_path %>"
8
8
  DNSMASQ_FILE="/etc/dnsmasq.d/active_postgres.conf"
9
+ DNSMASQ_STATIC_FILE="/etc/dnsmasq.d/messhy.conf"
9
10
  PRIMARY_RECORDS=(<%= primary_records.map(&:to_s).join(' ') %>)
10
11
  REPLICA_RECORDS=(<%= replica_records.map(&:to_s).join(' ') %>)
11
12
  SSH_STRICT_HOST_KEY="<%= ssh_strict_host_key %>"
@@ -16,7 +17,7 @@ REPMGR_DB=$(grep -oP "dbname=\K[^ ']+" "$REPMGR_CONF" | head -1)
16
17
  REPMGR_USER=$(grep -oP "user=\K[^ ']+" "$REPMGR_CONF" 2>/dev/null | head -1)
17
18
  REPMGR_USER="${REPMGR_USER:-repmgr}"
18
19
 
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
+ 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
21
  if [[ -z "$cluster_data" ]]; then
21
22
  exit 0
22
23
  fi
@@ -48,14 +49,20 @@ if [[ -n "$standby_hosts" ]]; then
48
49
  fi
49
50
 
50
51
  ssh_opts=(-i "$DNS_SSH_KEY" -o BatchMode=yes -o StrictHostKeyChecking="$SSH_STRICT_HOST_KEY" -o UserKnownHostsFile="$SSH_KNOWN_HOSTS")
52
+ cleanup_cmd=""
53
+ for record in "${PRIMARY_RECORDS[@]}" "${REPLICA_RECORDS[@]}"; do
54
+ if [[ -n "$record" ]]; then
55
+ cleanup_cmd="${cleanup_cmd}sudo sed -i '\\#^address=/${record}/#d' ${DNSMASQ_STATIC_FILE} 2>/dev/null || true; "
56
+ fi
57
+ done
51
58
 
52
59
  for server in "${DNS_SERVERS[@]}"; do
53
60
  if [[ -z "$server" ]]; then
54
61
  continue
55
62
  fi
56
63
 
57
- if ! /usr/bin/ssh "${ssh_opts[@]}" "${DNS_USER}@${server}" \
58
- "sudo bash -c 'cat > ${DNSMASQ_FILE} << \"EOF\"\\n${content}EOF\\n' && sudo systemctl restart dnsmasq"; then
64
+ if ! printf "%s" "$content" | /usr/bin/ssh "${ssh_opts[@]}" "${DNS_USER}@${server}" \
65
+ "${cleanup_cmd}sudo tee ${DNSMASQ_FILE} >/dev/null && sudo systemctl restart dnsmasq"; then
59
66
  /usr/bin/logger -t "$LOG_TAG" "Failed updating dnsmasq on ${server}"
60
67
  fi
61
68
  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.2
4
+ version: 0.9.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - BoringCache
@@ -200,6 +200,7 @@ files:
200
200
  - templates/pgbouncer-follow-primary.timer.erb
201
201
  - templates/pgbouncer.ini.erb
202
202
  - templates/pgbouncer_follow_primary.sh.erb
203
+ - templates/postgres_log_archive.sh.erb
203
204
  - templates/postgresql.conf.erb
204
205
  - templates/repmgr.conf.erb
205
206
  - templates/repmgr_dns_failover.sh.erb