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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +23 -0
  3. data/README.md +158 -0
  4. data/exe/activepostgres +5 -0
  5. data/lib/active_postgres/cli.rb +157 -0
  6. data/lib/active_postgres/cluster_deployment_flow.rb +85 -0
  7. data/lib/active_postgres/component_resolver.rb +24 -0
  8. data/lib/active_postgres/components/base.rb +38 -0
  9. data/lib/active_postgres/components/core.rb +158 -0
  10. data/lib/active_postgres/components/extensions.rb +99 -0
  11. data/lib/active_postgres/components/monitoring.rb +55 -0
  12. data/lib/active_postgres/components/pgbackrest.rb +94 -0
  13. data/lib/active_postgres/components/pgbouncer.rb +137 -0
  14. data/lib/active_postgres/components/repmgr.rb +651 -0
  15. data/lib/active_postgres/components/ssl.rb +86 -0
  16. data/lib/active_postgres/configuration.rb +190 -0
  17. data/lib/active_postgres/connection_pooler.rb +429 -0
  18. data/lib/active_postgres/credentials.rb +17 -0
  19. data/lib/active_postgres/deployment_flow.rb +154 -0
  20. data/lib/active_postgres/error_handler.rb +185 -0
  21. data/lib/active_postgres/failover.rb +83 -0
  22. data/lib/active_postgres/generators/active_postgres/install_generator.rb +186 -0
  23. data/lib/active_postgres/health_checker.rb +244 -0
  24. data/lib/active_postgres/installer.rb +114 -0
  25. data/lib/active_postgres/log_sanitizer.rb +67 -0
  26. data/lib/active_postgres/logger.rb +125 -0
  27. data/lib/active_postgres/performance_tuner.rb +246 -0
  28. data/lib/active_postgres/rails/database_config.rb +174 -0
  29. data/lib/active_postgres/rails/migration_guard.rb +25 -0
  30. data/lib/active_postgres/railtie.rb +28 -0
  31. data/lib/active_postgres/retry_helper.rb +80 -0
  32. data/lib/active_postgres/rollback_manager.rb +140 -0
  33. data/lib/active_postgres/secrets.rb +86 -0
  34. data/lib/active_postgres/ssh_executor.rb +288 -0
  35. data/lib/active_postgres/standby_deployment_flow.rb +122 -0
  36. data/lib/active_postgres/validator.rb +143 -0
  37. data/lib/active_postgres/version.rb +3 -0
  38. data/lib/active_postgres.rb +67 -0
  39. data/lib/tasks/postgres.rake +855 -0
  40. data/lib/tasks/rolling_update.rake +258 -0
  41. data/lib/tasks/rotate_credentials.rake +193 -0
  42. data/templates/pg_hba.conf.erb +47 -0
  43. data/templates/pgbackrest.conf.erb +43 -0
  44. data/templates/pgbouncer.ini.erb +55 -0
  45. data/templates/postgresql.conf.erb +157 -0
  46. data/templates/repmgr.conf.erb +40 -0
  47. metadata +224 -0
@@ -0,0 +1,86 @@
1
+ require 'English'
2
+ module ActivePostgres
3
+ class Secrets
4
+ attr_reader :config
5
+
6
+ def initialize(config)
7
+ @config = config
8
+ @cache = {}
9
+ end
10
+
11
+ def resolve(secret_key)
12
+ return @cache[secret_key] if @cache.key?(secret_key)
13
+
14
+ secret_value = config.secrets_config[secret_key]
15
+ return nil unless secret_value
16
+
17
+ resolved = resolve_secret_value(secret_value)
18
+ @cache[secret_key] = resolved
19
+ resolved
20
+ end
21
+
22
+ def resolve_all
23
+ config.secrets_config.keys.each_with_object({}) do |key, result|
24
+ result[key] = resolve(key)
25
+ end
26
+ end
27
+
28
+ def cache_to_files(directory = '.secrets')
29
+ require 'fileutils'
30
+
31
+ FileUtils.mkdir_p(directory)
32
+
33
+ resolve_all.each do |key, value|
34
+ file_path = File.join(directory, key)
35
+ File.write(file_path, value)
36
+ File.chmod(0o600, file_path)
37
+ puts "Cached #{key} to #{file_path}"
38
+ end
39
+
40
+ puts "\n✓ Secrets cached to #{directory}/"
41
+ puts "Add to .gitignore: #{directory}/"
42
+ end
43
+
44
+ private
45
+
46
+ def resolve_secret_value(value)
47
+ case value
48
+ when /^rails_credentials:(.+)$/
49
+ # Rails credentials: rails_credentials:postgres.superuser_password
50
+ key_path = ::Regexp.last_match(1)
51
+ fetch_from_rails_credentials(key_path)
52
+ when /^\$\((.+)\)$/
53
+ # Command execution: $(op read "op://...")
54
+ execute_command(::Regexp.last_match(1))
55
+ when /^\$([A-Z_][A-Z0-9_]*)$/
56
+ # Environment variable: $POSTGRES_PASSWORD
57
+ ENV.fetch(::Regexp.last_match(1), nil)
58
+ when /^env:(.+)$/
59
+ # Explicit env var: env:DATABASE_PASSWORD
60
+ ENV.fetch(::Regexp.last_match(1), nil)
61
+ else
62
+ # Literal value
63
+ value
64
+ end
65
+ end
66
+
67
+ def fetch_from_rails_credentials(key_path)
68
+ return nil unless Credentials.available?
69
+
70
+ keys = key_path.split('.').map(&:to_sym)
71
+ Rails.application.credentials.dig(*keys)
72
+ end
73
+
74
+ def execute_command(command)
75
+ # Preserve RAILS_ENV if set
76
+ env_prefix = ENV['RAILS_ENV'] ? "RAILS_ENV=#{ENV['RAILS_ENV']} " : ''
77
+ full_command = "#{env_prefix}#{command}"
78
+
79
+ result = `#{full_command}`.strip
80
+
81
+ raise Error, "Failed to execute secret command: #{command} (exit status: #{$CHILD_STATUS.exitstatus})" unless $CHILD_STATUS.success?
82
+
83
+ result
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,288 @@
1
+ require 'sshkit'
2
+ require 'sshkit/dsl'
3
+ require 'securerandom'
4
+
5
+ module ActivePostgres
6
+ class SSHExecutor
7
+ include SSHKit::DSL
8
+
9
+ attr_reader :config
10
+
11
+ def initialize(config, quiet: false)
12
+ @config = config
13
+ @quiet = quiet
14
+ setup_sshkit
15
+ end
16
+
17
+ def quiet?
18
+ @quiet
19
+ end
20
+
21
+ def execute_on_host(host, &)
22
+ on("#{config.user}@#{host}", &)
23
+ end
24
+
25
+ def execute_on_primary(&)
26
+ execute_on_host(config.primary_host, &)
27
+ end
28
+
29
+ def execute_on_standbys(&)
30
+ hosts = config.standby_hosts.map { |h| "#{config.user}@#{h}" }
31
+ on(hosts, in: :parallel, &)
32
+ end
33
+
34
+ def execute_on_all_hosts(&)
35
+ hosts = config.all_hosts.map { |h| "#{config.user}@#{h}" }
36
+ on(hosts, in: :parallel, &)
37
+ end
38
+
39
+ def install_postgres(host, version = 18)
40
+ execute_on_host(host) do
41
+ info "Installing PostgreSQL #{version}..."
42
+
43
+ if test('[ -f /etc/apt/sources.list.d/pgdg.list ]')
44
+ execute :sudo, 'rm', '-f',
45
+ '/etc/apt/sources.list.d/pgdg.list'
46
+ end
47
+
48
+ execute :sudo, 'apt-get', 'update', '-qq'
49
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq', 'gnupg', 'wget',
50
+ 'lsb-release', 'locales'
51
+
52
+ info 'Generating locales...'
53
+ execute :sudo, 'locale-gen', 'en_US.UTF-8'
54
+ execute :sudo, 'update-locale', 'LANG=en_US.UTF-8'
55
+
56
+ # Check for any installed PostgreSQL server packages
57
+ if test('command -v pg_lsclusters')
58
+ existing_clusters = capture(:pg_lsclusters, '-h').split("\n")
59
+ installed_versions = existing_clusters.map { |line| line.split[0].to_i }.uniq.sort
60
+
61
+ # Check if we have different versions installed
62
+ other_versions = installed_versions - [version]
63
+
64
+ if other_versions.any?
65
+ info "Found PostgreSQL version(s) #{other_versions.join(', ')}, cleaning up for fresh PostgreSQL #{version} install..."
66
+
67
+ # Stop all PostgreSQL services
68
+ execute :sudo, 'systemctl', 'stop', 'postgresql' if test('systemctl is-active postgresql')
69
+
70
+ # Remove all PostgreSQL packages
71
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'remove', '--purge', '-y', '-qq', 'postgresql*'
72
+ execute :sudo, 'apt-get', 'autoremove', '-y', '-qq'
73
+
74
+ # Clean up OLD version directories only (preserve target version SSL certs, etc.)
75
+ other_versions.each do |old_version|
76
+ execute :sudo, 'rm', '-rf', "/etc/postgresql/#{old_version}"
77
+ execute :sudo, 'rm', '-rf', "/var/lib/postgresql/#{old_version}"
78
+ end
79
+
80
+ execute :sudo, 'rm', '-f', '/etc/apt/sources.list.d/pgdg.list'
81
+ execute :sudo, 'rm', '-f', '/usr/share/keyrings/postgresql-archive-keyring.gpg'
82
+
83
+ info 'Cleanup complete'
84
+ elsif installed_versions.include?(version)
85
+ info "PostgreSQL #{version} already installed, skipping cleanup"
86
+ end
87
+ end
88
+
89
+ info 'Ensuring PostgreSQL GPG key is present...'
90
+ execute :wget, '--quiet', '-O', '/tmp/pgdg.asc', 'https://www.postgresql.org/media/keys/ACCC4CF8.asc'
91
+ execute :sudo, 'gpg', '--dearmor', '--yes', '-o', '/usr/share/keyrings/postgresql-archive-keyring.gpg',
92
+ '/tmp/pgdg.asc'
93
+ execute :rm, '/tmp/pgdg.asc'
94
+
95
+ info 'Configuring PostgreSQL apt repository...'
96
+ pgdg_repo = "'echo \"deb [signed-by=/usr/share/keyrings/postgresql-archive-keyring.gpg] " \
97
+ 'http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > ' \
98
+ "/etc/apt/sources.list.d/pgdg.list'"
99
+ execute :sudo, 'sh', '-c', pgdg_repo
100
+
101
+ execute :sudo, 'apt-get', 'update', '-qq'
102
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq', "postgresql-#{version}",
103
+ "postgresql-client-#{version}"
104
+
105
+ execute :sudo, 'systemctl', 'enable', 'postgresql'
106
+ execute :sudo, 'systemctl', 'start', 'postgresql' unless test('systemctl is-active postgresql')
107
+ end
108
+ end
109
+
110
+ def upload_file(host, content, remote_path, mode: '644', owner: nil)
111
+ execute_on_host(host) do
112
+ temp_file = "/tmp/#{File.basename(remote_path)}"
113
+ upload! StringIO.new(content), temp_file
114
+
115
+ execute :sudo, 'mv', temp_file, remote_path
116
+ execute :sudo, 'chown', owner, remote_path if owner
117
+ execute :sudo, 'chmod', mode, remote_path
118
+ end
119
+ end
120
+
121
+ def ensure_postgres_user(host)
122
+ postgres_user = config.postgres_user
123
+
124
+ execute_on_host(host) do
125
+ execute :sudo, 'groupadd', '--system', postgres_user unless test(:getent, 'group', postgres_user)
126
+
127
+ unless test(:id, postgres_user)
128
+ execute :sudo, 'useradd', '--system', '--home', '/var/lib/postgresql',
129
+ '--shell', '/bin/bash', '--gid', postgres_user, '--create-home', postgres_user
130
+ end
131
+ end
132
+ end
133
+
134
+ def postgres_running?(host)
135
+ result = false
136
+ execute_on_host(host) do
137
+ # Check if any PostgreSQL cluster is online using pg_lsclusters
138
+ # This works regardless of whether it's the generic postgresql service
139
+ # or a specific postgresql@version-main service
140
+ clusters = begin
141
+ capture(:sudo, 'pg_lsclusters', '2>/dev/null')
142
+ rescue StandardError
143
+ ''
144
+ end
145
+ result = clusters.include?('online')
146
+ end
147
+ result
148
+ end
149
+
150
+ def restart_postgres(host, version = nil)
151
+ execute_on_host(host) do
152
+ if version
153
+ begin
154
+ execute :sudo, 'pg_ctlcluster', version.to_s, 'main', 'restart'
155
+ rescue StandardError => e
156
+ error "Failed to restart PostgreSQL cluster #{version}/main"
157
+ info 'Checking systemd logs...'
158
+ logs = begin
159
+ capture(:sudo, 'journalctl', '-xeu', "postgresql@#{version}-main", '-n', '50',
160
+ '--no-pager')
161
+ rescue StandardError
162
+ 'Could not get systemd logs'
163
+ end
164
+ info logs
165
+ info 'Checking PostgreSQL logs...'
166
+ pg_logs = begin
167
+ capture(:sudo, 'tail', '-100',
168
+ "/var/log/postgresql/postgresql-#{version}-main.log")
169
+ rescue StandardError
170
+ 'Could not get PostgreSQL logs'
171
+ end
172
+ info pg_logs
173
+ info 'Checking cluster status...'
174
+ cluster_status = begin
175
+ capture(:sudo, 'pg_lsclusters')
176
+ rescue StandardError
177
+ 'Could not get cluster status'
178
+ end
179
+ info cluster_status
180
+ raise e
181
+ end
182
+ else
183
+ execute :sudo, 'systemctl', 'restart', 'postgresql'
184
+ end
185
+ end
186
+ end
187
+
188
+ def stop_postgres(host)
189
+ execute_on_host(host) do
190
+ execute :sudo, 'systemctl', 'stop', 'postgresql'
191
+ end
192
+ end
193
+
194
+ def get_postgres_status(host)
195
+ result = nil
196
+ postgres_user = config.postgres_user
197
+ execute_on_host(host) do
198
+ result = capture(:sudo, '-u', postgres_user, 'psql', '-c', 'SELECT version();')
199
+ end
200
+ result
201
+ end
202
+
203
+ def run_sql(host, sql)
204
+ result = nil
205
+ postgres_user = config.postgres_user
206
+ execute_on_host(host) do
207
+ # Use a temporary file to avoid shell escaping issues with special characters
208
+ temp_file = "/tmp/query_#{SecureRandom.hex(8)}.sql"
209
+ upload! StringIO.new(sql), temp_file
210
+ execute :chmod, '644', temp_file
211
+
212
+ begin
213
+ result = capture(:sudo, '-u', postgres_user, 'psql', '-t', '-f', temp_file)
214
+ ensure
215
+ execute :rm, '-f', temp_file
216
+ end
217
+ end
218
+ result
219
+ end
220
+
221
+ def ensure_cluster_exists(host, version)
222
+ execute_on_host(host) do
223
+ data_dir = "/var/lib/postgresql/#{version}/main"
224
+
225
+ if test(:sudo, 'test', '-d', data_dir)
226
+ info "PostgreSQL #{version}/main cluster already exists, skipping creation"
227
+ else
228
+ info 'Creating PostgreSQL cluster...'
229
+ execute :sudo, 'pg_createcluster', version.to_s, 'main', '--start'
230
+ end
231
+ end
232
+ end
233
+
234
+ def recreate_cluster(host, version)
235
+ execute_on_host(host) do
236
+ info 'Ensuring clean cluster state...'
237
+ begin
238
+ execute :sudo, 'systemctl', 'stop', 'postgresql'
239
+ rescue StandardError
240
+ nil
241
+ end
242
+ begin
243
+ execute :sudo, 'pg_dropcluster', '--stop', version.to_s, 'main'
244
+ rescue StandardError
245
+ nil
246
+ end
247
+ begin
248
+ execute :sudo, 'rm', '-rf', "/etc/postgresql/#{version}/main"
249
+ rescue StandardError
250
+ nil
251
+ end
252
+ begin
253
+ execute :sudo, 'rm', '-rf', "/var/lib/postgresql/#{version}/main"
254
+ rescue StandardError
255
+ nil
256
+ end
257
+
258
+ info 'Creating fresh PostgreSQL cluster...'
259
+ execute :sudo, 'pg_createcluster', version.to_s, 'main'
260
+ execute :sudo, 'systemctl', 'start', 'postgresql'
261
+ end
262
+ end
263
+
264
+ private
265
+
266
+ def setup_sshkit
267
+ if @quiet
268
+ SSHKit.config.output_verbosity = ::Logger::FATAL
269
+ SSHKit.config.format = :blackhole
270
+ else
271
+ SSHKit.config.output_verbosity = ::Logger::INFO
272
+ SSHKit.config.format = :pretty
273
+ end
274
+
275
+ return unless File.exist?(config.ssh_key)
276
+
277
+ SSHKit::Backend::Netssh.configure do |ssh|
278
+ ssh.ssh_options = {
279
+ keys: [config.ssh_key],
280
+ keys_only: true,
281
+ forward_agent: false,
282
+ auth_methods: ['publickey'],
283
+ verify_host_key: :never
284
+ }
285
+ end
286
+ end
287
+ end
288
+ end
@@ -0,0 +1,122 @@
1
+ module ActivePostgres
2
+ class StandbyDeploymentFlow < DeploymentFlow
3
+ attr_reader :standby_host
4
+
5
+ def initialize(config, standby_host:, **)
6
+ super(config, **)
7
+ @standby_host = standby_host
8
+ end
9
+
10
+ private
11
+
12
+ def operation_name
13
+ "Standby Setup: #{standby_host}"
14
+ end
15
+
16
+ def print_targets
17
+ logger.info "Primary: #{config.primary_host}"
18
+ logger.info "Standby: #{standby_host}"
19
+ end
20
+
21
+ def validate_specific_requirements
22
+ abort "❌ #{standby_host} is not configured as a standby in config/postgres.yml" unless config.standby_hosts.include?(standby_host)
23
+
24
+ return if config.component_enabled?(:repmgr)
25
+
26
+ abort '❌ repmgr component must be enabled to setup standbys'
27
+ end
28
+
29
+ def run_preflight_checks
30
+ super
31
+
32
+ check_if_standby_already_deployed
33
+ end
34
+
35
+ def check_if_standby_already_deployed
36
+ logger.info "\nChecking if standby is already deployed..."
37
+
38
+ postgres_running = ssh_executor.postgres_running?(standby_host)
39
+
40
+ return unless postgres_running
41
+
42
+ logger.warn "⚠️ PostgreSQL is already running on #{standby_host}"
43
+ logger.warn ' This deployment will DROP and RECREATE the database cluster'
44
+ logger.warn ' All data on this standby will be LOST and re-cloned from primary'
45
+ puts ''
46
+ rescue StandardError => e
47
+ logger.warn "Could not check if standby is deployed: #{e.message}"
48
+ end
49
+
50
+ def list_deployment_steps
51
+ logger.info " • Install PostgreSQL #{config.version} packages on #{standby_host}"
52
+ logger.info " • Clone data from primary: #{config.primary_host}"
53
+ logger.info ' • Register standby with repmgr cluster'
54
+ end
55
+
56
+ def list_warnings
57
+ logger.info "\n⚠️ Primary database will NOT be touched"
58
+ end
59
+
60
+ def deploy_components
61
+ deploy_ssl if config.component_enabled?(:ssl)
62
+ deploy_core
63
+ deploy_repmgr
64
+ deploy_optional_components
65
+ end
66
+
67
+ def deploy_ssl
68
+ logger.task('Setting up SSL on standby') do
69
+ component = Components::SSL.new(config, ssh_executor, secrets)
70
+ register_rollback('SSL', component)
71
+ component.install_on_standby(standby_host)
72
+ end
73
+ end
74
+
75
+ def deploy_core
76
+ logger.task('Installing PostgreSQL packages on standby') do
77
+ component = Components::Core.new(config, ssh_executor, secrets)
78
+ component.install_packages_only(standby_host)
79
+ end
80
+ end
81
+
82
+ def deploy_repmgr
83
+ logger.task('Setting up repmgr and cloning from primary') do
84
+ component = Components::Repmgr.new(config, ssh_executor, secrets)
85
+ register_rollback('repmgr', component)
86
+ component.setup_standby_only(standby_host)
87
+ end
88
+ end
89
+
90
+ def deploy_optional_components
91
+ deploy_pgbouncer if config.component_enabled?(:pgbouncer)
92
+ deploy_monitoring if config.component_enabled?(:monitoring)
93
+ end
94
+
95
+ def deploy_pgbouncer
96
+ logger.task('Setting up pgbouncer on standby') do
97
+ component = Components::PgBouncer.new(config, ssh_executor, secrets)
98
+ component.install_on_standby(standby_host) if component.respond_to?(:install_on_standby)
99
+ end
100
+ end
101
+
102
+ def deploy_monitoring
103
+ logger.task('Setting up monitoring on standby') do
104
+ component = Components::Monitoring.new(config, ssh_executor, secrets)
105
+ component.install_on_standby(standby_host) if component.respond_to?(:install_on_standby)
106
+ end
107
+ end
108
+
109
+ def register_rollback(component_name, component)
110
+ rollback_manager.register("Uninstall #{component_name} on #{standby_host}", host: standby_host) do
111
+ component.uninstall
112
+ rescue StandardError => e
113
+ logger.warn "Failed to uninstall #{component_name} on #{standby_host}: #{e.message}"
114
+ end
115
+ end
116
+
117
+ def list_next_steps
118
+ logger.info ' 1. Check cluster status: active_postgres status'
119
+ logger.info ' 2. Verify replication: Check repmgr cluster show'
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,143 @@
1
+ module ActivePostgres
2
+ class Validator
3
+ attr_reader :config, :ssh_executor, :errors, :warnings
4
+
5
+ def initialize(config, ssh_executor)
6
+ @config = config
7
+ @ssh_executor = ssh_executor
8
+ @errors = []
9
+ @warnings = []
10
+ end
11
+
12
+ # Run all validation checks before installation
13
+ def validate_all
14
+ puts 'Running pre-flight validation checks...'
15
+
16
+ validate_configuration
17
+ validate_ssh_connectivity
18
+ validate_network_connectivity
19
+ validate_system_requirements
20
+
21
+ if errors.any?
22
+ puts "\n❌ Validation failed with #{errors.count} error(s):"
23
+ errors.each { |error| puts " - #{error}" }
24
+ return false
25
+ end
26
+
27
+ if warnings.any?
28
+ puts "\n⚠️ Found #{warnings.count} warning(s):"
29
+ warnings.each { |warning| puts " - #{warning}" }
30
+ end
31
+
32
+ puts "✅ All validation checks passed!\n"
33
+ true
34
+ end
35
+
36
+ private
37
+
38
+ def validate_configuration
39
+ puts ' Checking configuration...'
40
+
41
+ # Validate primary host
42
+ errors << 'Primary host not configured' unless config.primary_host
43
+
44
+ # Validate PostgreSQL version
45
+ errors << "PostgreSQL version must be 12 or higher (got: #{config.version})" unless config.version && config.version >= 12
46
+
47
+ # Validate repmgr setup
48
+ if config.component_enabled?(:repmgr)
49
+ errors << 'repmgr enabled but no standby hosts configured' unless config.standby_hosts&.any?
50
+
51
+ replication_host = config.primary_replication_host
52
+ if replication_host == config.primary_host
53
+ warnings << "Primary is using '#{config.primary_host}' for replication traffic. Set private_ip for isolated networks."
54
+ end
55
+
56
+ config.standby_hosts.each do |host|
57
+ standby_replication_host = config.replication_host_for(host)
58
+ next unless standby_replication_host == host
59
+
60
+ warnings << "Standby #{host} is using its SSH host for replication. Provide private_ip if it differs."
61
+ end
62
+ end
63
+
64
+ # Validate SSL configuration
65
+ return unless config.component_enabled?(:ssl)
66
+
67
+ ssl_config = config.component_config(:ssl)
68
+ return unless ssl_config['certificate_mode'] == 'custom'
69
+
70
+ warnings << 'Custom SSL certificates configured - ensure they are available'
71
+ end
72
+
73
+ def validate_ssh_connectivity
74
+ puts ' Checking SSH connectivity...'
75
+
76
+ all_hosts = [config.primary_host] + config.standby_hosts
77
+
78
+ all_hosts.each do |host|
79
+ ssh_executor.execute_on_host(host) do
80
+ test(:echo, 'SSH connection test')
81
+ end
82
+ rescue StandardError => e
83
+ errors << "Cannot connect to #{host} via SSH: #{e.message}"
84
+ end
85
+ end
86
+
87
+ def validate_network_connectivity
88
+ puts ' Checking network connectivity...'
89
+
90
+ return unless config.component_enabled?(:repmgr)
91
+
92
+ primary_replication_host = config.primary_replication_host
93
+ return unless primary_replication_host
94
+
95
+ # Capture errors and warnings before entering SSH block
96
+ validator_errors = errors
97
+ validator_warnings = warnings
98
+
99
+ # Check if standbys can reach primary via the preferred replication network
100
+ config.standby_hosts.each do |host|
101
+ ssh_executor.execute_on_host(host) do
102
+ can_ping = test(:ping, '-c', '1', '-W', '2', primary_replication_host)
103
+ validator_errors << "Standby #{host} cannot reach the primary over #{primary_replication_host}" unless can_ping
104
+ end
105
+ rescue StandardError => e
106
+ validator_warnings << "Could not test private network connectivity from #{host}: #{e.message}"
107
+ end
108
+ end
109
+
110
+ def validate_system_requirements
111
+ puts ' Checking system requirements...'
112
+
113
+ all_hosts = [config.primary_host] + config.standby_hosts
114
+
115
+ # Capture errors and warnings before entering SSH block
116
+ validator_errors = errors
117
+ validator_warnings = warnings
118
+
119
+ all_hosts.each do |host|
120
+ ssh_executor.execute_on_host(host) do
121
+ # Check if running Debian/Ubuntu
122
+ unless test('[ -f /etc/debian_version ]')
123
+ validator_errors << "#{host} is not running Debian/Ubuntu"
124
+ next
125
+ end
126
+
127
+ # Check available disk space (require at least 10GB)
128
+ # Check /var or root filesystem (since /var/lib/postgresql may not exist yet)
129
+ disk_space_output = capture(:df, '-BG', '/var', '|', :tail, '-1', '|', :awk, "'{print $4}'")
130
+ disk_space = disk_space_output.gsub('G', '').to_i
131
+ validator_warnings << "#{host} has less than 10GB free disk space (#{disk_space}GB available)" if disk_space < 10
132
+
133
+ # Check if postgres user already exists
134
+ if test(:id, 'postgres')
135
+ validator_warnings << "#{host} already has a 'postgres' user - this is expected if PostgreSQL was previously installed"
136
+ end
137
+ end
138
+ rescue StandardError => e
139
+ validator_warnings << "Could not check system requirements on #{host}: #{e.message}"
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,3 @@
1
+ module ActivePostgres
2
+ VERSION = '0.4.0'.freeze
3
+ end