messhy 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b9b5c1546fc154030d0b9b39299e315d13988eefdf5437b2410b07fe1b2fe268
4
+ data.tar.gz: f0e209b1a65ac6adbb1d15b3e17fdcf634b165ba07526b1ac1a5dfcdf1a3fcdb
5
+ SHA512:
6
+ metadata.gz: f7aeb904c83b9323c12bb26ea665597e1528e862e8910bd7c918256bc9ac52c9f580040c01462f3c3a6f8a693b86a4839e990efebef12fa44ea6d26db4b75f4b
7
+ data.tar.gz: 5562d98883977f7aa0312faec1ec966d0d26d4027e119253eeff66abdbbc26371471a898210e110ded737960f4d1359a1c15be9a912cd5a7a04e08171b27a257
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Gaurav
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+
data/README.md ADDED
@@ -0,0 +1,296 @@
1
+ # messhy
2
+
3
+ > WireGuard VPN mesh for secure private networking. Simple, fast, zero-config.
4
+
5
+ A Ruby gem that sets up a full WireGuard VPN mesh across any VMs. Every node connects directly to every other node, creating a secure private network.
6
+
7
+ ## Why messhy?
8
+
9
+ ### Problems It Solves
10
+
11
+ ❌ **Without messhy:**
12
+ - Database replication over public IPs (insecure)
13
+ - Complex VPN configurations
14
+ - NAT traversal issues
15
+ - Manual key management
16
+ - Cloud-specific networking (VPC, subnet, security groups)
17
+
18
+ ✅ **With messhy:**
19
+ - Secure encrypted connections (WireGuard)
20
+ - Automatic key generation and distribution
21
+ - Works across any cloud/datacenter
22
+ - Zero application changes (just use 10.8.0.x IPs)
23
+ - Simple configuration
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ gem install messhy
29
+ ```
30
+
31
+ Or add to your Gemfile:
32
+
33
+ ```ruby
34
+ gem 'messhy'
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ 1. **Create config file**:
40
+
41
+ ```yaml
42
+ # config/mesh.yml
43
+ production:
44
+ network: 10.8.0.0/24
45
+ user: ubuntu
46
+ ssh_key: ~/.ssh/id_rsa
47
+ verify_host_key: true
48
+
49
+ nodes:
50
+ db-primary:
51
+ host: 34.12.234.81
52
+ private_ip: 10.8.0.10
53
+
54
+ db-standby:
55
+ host: 52.23.45.67
56
+ private_ip: 10.8.0.11
57
+
58
+ app-1:
59
+ host: 18.156.78.90
60
+ private_ip: 10.8.0.20
61
+ ```
62
+
63
+ 2. **Setup mesh**:
64
+
65
+ ```bash
66
+ messhy setup --environment=production
67
+ ```
68
+
69
+ 3. **Verify**:
70
+
71
+ ```bash
72
+ messhy status
73
+ ```
74
+
75
+ ## Secret Management
76
+
77
+ `messhy setup` stores generated WireGuard key pairs inside `.secrets/wireguard/*.yml` with `0600` permissions. Each node gets its own YAML file (`.secrets/wireguard/<node>.yml`) and all peer pre‑shared keys live in `.secrets/wireguard/psks.yml`. The directory is gitignored by default, and the Rails generator ensures the ignore rules are present in your application. After provisioning, copy the YAML files into 1Password (or another vault) and remove them from disk if you do not want long‑lived local copies.
78
+
79
+ If you want to pre-generate keys before rolling out configs, run:
80
+
81
+ ```bash
82
+ messhy keygen --environment=production
83
+ ```
84
+
85
+ ## Trusting SSH Host Keys
86
+
87
+ Before running `messhy setup`, fetch each server's SSH fingerprint and add it to your local `known_hosts` file:
88
+
89
+ ```bash
90
+ # Adds Ed25519/ECDSA/RSA host keys for every node defined in config/mesh.yml
91
+ bundle exec messhy trust-hosts --environment=production
92
+ ```
93
+
94
+ If a server rotated keys or you resized an instance, clear the old entry as part of the same command:
95
+
96
+ ```bash
97
+ bundle exec messhy trust-hosts --environment=production --force
98
+ ```
99
+
100
+ This command uses `ssh-keyscan` under the hood and skips entries that already exist. If a host cannot be scanned (firewall / DNS issue), it will be listed at the end so you can add it manually. You can also call the Rails task `rails messhy:trust_hosts`.
101
+
102
+ ## Rails Integration
103
+
104
+ 1. Add the gem to your Rails application and run `bundle install`.
105
+ 2. Generate the config stub and gitignore entries:
106
+
107
+ ```bash
108
+ rails generate messhy:install
109
+ ```
110
+
111
+ 3. Use the provided rake tasks from your app (they automatically use `RAILS_ENV`/`MESSHY_ENVIRONMENT`):
112
+
113
+ ```bash
114
+ rails messhy:trust_hosts # ssh-keyscan every node
115
+ rails messhy:setup # deploy WireGuard configs
116
+ rails messhy:status # show current mesh status
117
+ rails messhy:keygen # pre-generate keys only
118
+ ```
119
+
120
+ This keeps Rails + SSHKit conventions intact: tasks shell out to the Thor CLI, SSH host key verification is enforced by default, and WireGuard secrets stay outside of Git.
121
+
122
+ ## CLI Commands
123
+
124
+ ### Setup
125
+
126
+ ```bash
127
+ # Initial setup (all nodes)
128
+ messhy setup
129
+ messhy setup --environment=production
130
+
131
+ # Setup with options
132
+ messhy setup --dry-run # Show what would be done
133
+ messhy setup --skip-node=app-1 # Skip specific node
134
+ messhy setup --only-node=db-primary # Setup single node
135
+ ```
136
+
137
+ ### Status & Monitoring
138
+
139
+ ```bash
140
+ # Show all connections
141
+ messhy status
142
+
143
+ # Ping specific node
144
+ messhy ping app-1
145
+ messhy ping 10.8.0.20
146
+
147
+ # Test connectivity
148
+ messhy test-connectivity
149
+
150
+ # Show traffic statistics
151
+ messhy stats
152
+ messhy stats --node=db-primary
153
+ ```
154
+
155
+ ### Key & Access Management
156
+
157
+ ```bash
158
+ # Generate WireGuard keys without touching configs
159
+ messhy keygen --environment=production
160
+ messhy keygen --skip-node=app-1
161
+
162
+ # Trust SSH host keys (uses ssh-keyscan)
163
+ messhy trust-hosts
164
+ messhy trust-hosts --force # replace existing entries
165
+ messhy trust-hosts --known-hosts=/tmp/known_hosts
166
+ ```
167
+
168
+ ### Info
169
+
170
+ ```bash
171
+ # List all nodes
172
+ messhy list
173
+
174
+ # Show node details
175
+ messhy show db-primary
176
+ ```
177
+
178
+ ## Configuration
179
+
180
+ See `config/mesh.example.yml` for a complete example.
181
+
182
+ ### Basic Options
183
+
184
+ - `network`: CIDR network for VPN (default: `10.8.0.0/24`)
185
+ - `user`: SSH user (default: `ubuntu`)
186
+ - `ssh_key`: Path to SSH private key
187
+ - `mtu`: MTU size (default: `1280` for reliability)
188
+ - `listen_port`: WireGuard port (default: `51820`)
189
+ - `keepalive`: Keepalive interval in seconds (default: `25`)
190
+
191
+ ### Node Configuration
192
+
193
+ Each node requires:
194
+ - `host`: Public IP or hostname
195
+ - `private_ip`: Private VPN IP (must be within network range)
196
+
197
+ Optional per-node overrides:
198
+ - `ssh_user` / `ssh_port`: Override SSH access details (defaults to top-level `user` and port 22)
199
+ - `ssh_key`: Override SSH key for a specific node
200
+ - `listen_port`: WireGuard UDP port (defaults to top-level `listen_port`)
201
+ - `region`: Documentation / metadata field
202
+
203
+ ## Firewall Requirements
204
+
205
+ Only one port needs to be opened on each node:
206
+
207
+ ```bash
208
+ # UFW
209
+ ufw allow 51820/udp
210
+
211
+ # iptables
212
+ iptables -A INPUT -p udp --dport 51820 -j ACCEPT
213
+ ```
214
+
215
+ ## Architecture
216
+
217
+ ### Full Mesh Topology
218
+
219
+ ```
220
+ Node A
221
+ / | \
222
+ / | \
223
+ / | \
224
+ Node B - + - Node C
225
+ \ | /
226
+ \ | /
227
+ \ | /
228
+ Node D
229
+ ```
230
+
231
+ **Every node connects directly to every other node:**
232
+ - No central point of failure
233
+ - Optimal routing (direct connections)
234
+ - Scales to ~50 nodes
235
+
236
+ ## Performance
237
+
238
+ ### Benchmarks (compared to no VPN)
239
+
240
+ | Metric | No VPN | WireGuard | Overhead |
241
+ |--------|--------|-----------|----------|
242
+ | Throughput | 1000 Mbps | 950 Mbps | 5% |
243
+ | Latency | 10ms | 10.5ms | +0.5ms |
244
+ | CPU usage | 2% | 3% | +1% |
245
+
246
+ **WireGuard is FAST!** Negligible overhead for most workloads.
247
+
248
+ ## Troubleshooting
249
+
250
+ ### Node can't connect
251
+
252
+ ```bash
253
+ # Check WireGuard is running
254
+ systemctl status wg-quick@wg0
255
+
256
+ # Check interface
257
+ wg show wg0
258
+
259
+ # Check firewall
260
+ ufw status | grep 51820
261
+
262
+ # Test connectivity
263
+ ping 10.8.0.x
264
+
265
+ # Check logs
266
+ journalctl -u wg-quick@wg0 -f
267
+ ```
268
+
269
+ ### High latency
270
+
271
+ ```bash
272
+ # Try lower MTU (in mesh.yml)
273
+ mtu: 1280 # instead of 1420
274
+
275
+ # Redeploy
276
+ messhy setup
277
+ ```
278
+
279
+ ## Requirements
280
+
281
+ - Ruby 3.0+
282
+ - WireGuard tools (`wg` command)
283
+ - Target servers with Linux kernel 5.6+ (WireGuard built-in)
284
+ - SSH key-based authentication
285
+
286
+ ## Contributing
287
+
288
+ Bug reports and pull requests are welcome on GitHub.
289
+
290
+ ## License
291
+
292
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
293
+
294
+ ---
295
+
296
+ Built with ❤️ for the Rails community by [Gaurav](https://github.com/yourusername)
data/exe/messhy ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/messhy'
5
+
6
+ Messhy::CLI.start(ARGV)
data/lib/messhy/cli.rb ADDED
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module Messhy
6
+ class CLI < Thor
7
+ class_option :environment, aliases: '-e', default: ENV['MESSHY_ENVIRONMENT'] || ENV['RAILS_ENV'] || 'development'
8
+ class_option :config, aliases: '-c', default: 'config/mesh.yml'
9
+
10
+ desc 'setup', 'Setup WireGuard mesh network on all nodes'
11
+ option :dry_run, type: :boolean, default: false
12
+ option :skip_node, type: :string
13
+ option :only_node, type: :string
14
+ def setup
15
+ config = load_config
16
+ installer = Installer.new(config, dry_run: options[:dry_run])
17
+
18
+ if options[:only_node]
19
+ installer.setup_node(options[:only_node])
20
+ elsif options[:skip_node]
21
+ installer.setup(skip: options[:skip_node])
22
+ else
23
+ installer.setup
24
+ end
25
+ rescue SSHKit::Runner::ExecuteError => e
26
+ handle_ssh_error(e, config)
27
+ end
28
+
29
+ desc 'keygen', 'Generate WireGuard keys without deploying configs'
30
+ option :skip_node, type: :string
31
+ def keygen
32
+ config = load_config
33
+ installer = Installer.new(config)
34
+ installer.generate_keys(skip: options[:skip_node])
35
+ rescue SSHKit::Runner::ExecuteError => e
36
+ handle_ssh_error(e, config)
37
+ end
38
+
39
+ desc 'status', 'Show mesh network status'
40
+ def status
41
+ config = load_config
42
+ health_checker = HealthChecker.new(config)
43
+ health_checker.show_status
44
+ rescue SSHKit::Runner::ExecuteError => e
45
+ handle_ssh_error(e, config)
46
+ end
47
+
48
+ desc 'health', 'Alias for status'
49
+ def health
50
+ status
51
+ end
52
+
53
+ desc 'ping NODE', 'Ping a specific node'
54
+ def ping(node)
55
+ config = load_config
56
+ health_checker = HealthChecker.new(config)
57
+ health_checker.ping_node(node)
58
+ rescue SSHKit::Runner::ExecuteError => e
59
+ handle_ssh_error(e, config)
60
+ end
61
+
62
+ desc 'test-connectivity', 'Test connectivity between all nodes'
63
+ def test_connectivity
64
+ config = load_config
65
+ health_checker = HealthChecker.new(config)
66
+ health_checker.test_all
67
+ rescue SSHKit::Runner::ExecuteError => e
68
+ handle_ssh_error(e, config)
69
+ end
70
+
71
+ desc 'stats', 'Show traffic statistics'
72
+ option :node, type: :string
73
+ def stats
74
+ config = load_config
75
+ health_checker = HealthChecker.new(config)
76
+ health_checker.show_stats(node: options[:node])
77
+ rescue SSHKit::Runner::ExecuteError => e
78
+ handle_ssh_error(e, config)
79
+ end
80
+
81
+ desc 'trust-hosts', 'Add each node host key to known_hosts using ssh-keyscan'
82
+ option :known_hosts, type: :string, desc: 'Override path to known_hosts'
83
+ option :force, type: :boolean, default: false, desc: 'Remove existing entries before scanning'
84
+ option :hash_hosts, type: :boolean, default: false, desc: 'Hash hostnames in known_hosts'
85
+ option :timeout, type: :numeric, default: HostTrustManager::DEFAULT_TIMEOUT, desc: 'ssh-keyscan timeout (seconds)'
86
+ def trust_hosts
87
+ config = load_config
88
+ timeout = (options[:timeout] || HostTrustManager::DEFAULT_TIMEOUT).to_i
89
+ manager = HostTrustManager.new(
90
+ config,
91
+ known_hosts_path: options[:known_hosts] || File.expand_path('~/.ssh/known_hosts'),
92
+ timeout: timeout,
93
+ hash_hosts: options[:hash_hosts],
94
+ replace_existing: options[:force]
95
+ )
96
+
97
+ success = manager.trust_all_hosts
98
+ exit 1 unless success
99
+ end
100
+
101
+ desc 'list', 'List all nodes'
102
+ def list
103
+ config = load_config
104
+ config.each_node do |name, node_config|
105
+ puts "#{name}: #{node_config['host']} (#{node_config['private_ip']})"
106
+ end
107
+ end
108
+
109
+ desc 'show NAME', 'Show node details'
110
+ def show(name)
111
+ config = load_config
112
+ node_config = config.node_config(name)
113
+
114
+ unless node_config
115
+ puts "Node not found: #{name}"
116
+ exit 1
117
+ end
118
+
119
+ puts "Node: #{name}"
120
+ puts "Host: #{node_config['host']}"
121
+ puts "Private IP: #{node_config['private_ip']}"
122
+ puts "Region: #{node_config['region']}" if node_config['region']
123
+
124
+ health_checker = HealthChecker.new(config)
125
+ health_checker.show_node_status(name)
126
+ rescue SSHKit::Runner::ExecuteError => e
127
+ handle_ssh_error(e, config)
128
+ end
129
+
130
+ desc 'version', 'Show version'
131
+ def version
132
+ puts "messhy #{Messhy::VERSION}"
133
+ end
134
+
135
+ private
136
+
137
+ def load_config
138
+ Configuration.load(options[:config], options[:environment])
139
+ rescue StandardError => e
140
+ puts "Error loading config: #{e.message}"
141
+ exit 1
142
+ end
143
+
144
+ def handle_ssh_error(error, config)
145
+ error_msg = error.message
146
+
147
+ # Check if it's a host key mismatch error
148
+ if error_msg.include?('fingerprint') && error_msg.include?('does not match')
149
+ handle_host_key_mismatch_error(error_msg, config)
150
+ elsif error_msg.include?('Authentication failed') || error_msg.include?('Permission denied')
151
+ handle_authentication_error(error_msg, config)
152
+ elsif error_msg.include?('Connection refused') || error_msg.include?('Connection timed out')
153
+ handle_connection_error(error_msg, config)
154
+ else
155
+ handle_generic_ssh_error(error_msg, config)
156
+ end
157
+
158
+ exit 1
159
+ end
160
+
161
+ def handle_host_key_mismatch_error(error_msg, config)
162
+ # Extract the host IP from the error message
163
+ host_match = error_msg.match(/for "([^"]+)"/)
164
+ host_ip = host_match[1] if host_match
165
+
166
+ puts "\n❌ SSH Host Key Verification Failed"
167
+ puts '=' * 60
168
+ puts
169
+ puts 'The SSH fingerprint for one or more hosts has changed.'
170
+ puts 'This typically happens when a server is rebuilt or reinstalled.'
171
+ puts
172
+
173
+ if host_ip
174
+ puts "Problematic host: #{host_ip}"
175
+ puts
176
+ end
177
+
178
+ puts '🔧 How to fix this:'
179
+ puts
180
+ puts ' 1. Update all host keys automatically (recommended):'
181
+ puts " #{environment_prefix(config)}messhy trust-hosts --force"
182
+ puts
183
+ puts ' 2. Or manually remove just the problematic host:'
184
+ if host_ip
185
+ puts " ssh-keygen -R #{host_ip}"
186
+ else
187
+ puts ' ssh-keygen -R <host_ip>'
188
+ end
189
+ puts
190
+ puts ' 3. Then retry the setup:'
191
+ puts " #{environment_prefix(config)}messhy setup"
192
+ puts
193
+ puts '=' * 60
194
+ end
195
+
196
+ def handle_authentication_error(_error_msg, config)
197
+ puts "\n❌ SSH Authentication Failed"
198
+ puts '=' * 60
199
+ puts
200
+ puts 'Could not authenticate to one or more hosts.'
201
+ puts
202
+ puts '🔧 Troubleshooting steps:'
203
+ puts
204
+ puts ' 1. Verify the SSH key path is correct in your config:'
205
+ puts " Config file: #{options[:config]}"
206
+ puts " Environment: #{config.environment}"
207
+ puts " SSH key: #{config.ssh_key}"
208
+ puts
209
+ puts ' 2. Check that the SSH key exists:'
210
+ puts " ls -la #{config.ssh_key}"
211
+ puts
212
+ puts ' 3. Verify the SSH key is authorized on the remote hosts:'
213
+ puts " ssh -i #{config.ssh_key} #{config.user}@<host> 'cat ~/.ssh/authorized_keys'"
214
+ puts
215
+ puts ' 4. Check file permissions (should be 600 for private key):'
216
+ puts " chmod 600 #{config.ssh_key}"
217
+ puts
218
+ puts ' 5. Test manual SSH connection:'
219
+ puts " ssh -i #{config.ssh_key} #{config.user}@<host>"
220
+ puts
221
+ puts '=' * 60
222
+ end
223
+
224
+ def handle_connection_error(error_msg, _config)
225
+ puts "\n❌ SSH Connection Failed"
226
+ puts '=' * 60
227
+ puts
228
+ puts 'Could not connect to one or more hosts.'
229
+ puts
230
+ puts "Error: #{error_msg.lines.first&.strip}"
231
+ puts
232
+ puts '🔧 Troubleshooting steps:'
233
+ puts
234
+ puts ' 1. Verify the hosts are online and reachable:'
235
+ puts ' ping <host>'
236
+ puts
237
+ puts ' 2. Check that SSH port is open (default: 22):'
238
+ puts ' nc -zv <host> 22'
239
+ puts
240
+ puts ' 3. Verify firewall rules allow SSH connections'
241
+ puts
242
+ puts ' 4. Check that SSH service is running on remote hosts:'
243
+ puts ' systemctl status sshd'
244
+ puts
245
+ puts ' 5. Review host configuration in:'
246
+ puts " #{options[:config]}"
247
+ puts
248
+ puts '=' * 60
249
+ end
250
+
251
+ def handle_generic_ssh_error(error_msg, config)
252
+ puts "\n❌ SSH Error"
253
+ puts '=' * 60
254
+ puts
255
+ puts "Error: #{error_msg}"
256
+ puts
257
+ puts '🔧 Troubleshooting steps:'
258
+ puts
259
+ puts ' 1. Trust SSH host keys for all nodes:'
260
+ puts " #{environment_prefix(config)}messhy trust-hosts"
261
+ puts
262
+ puts ' 2. Verify configuration:'
263
+ puts " Config file: #{options[:config]}"
264
+ puts " Environment: #{config.environment}"
265
+ puts
266
+ puts ' 3. Test manual SSH connection:'
267
+ puts " ssh -i #{config.ssh_key} #{config.user}@<host>"
268
+ puts
269
+ puts ' 4. Check the detailed error message above for specific issues'
270
+ puts
271
+ puts '=' * 60
272
+ end
273
+
274
+ def environment_prefix(config)
275
+ return '' if config.environment == 'development'
276
+
277
+ "RAILS_ENV=#{config.environment} "
278
+ end
279
+ end
280
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Messhy
6
+ class Configuration
7
+ attr_reader :environment,
8
+ :network,
9
+ :nodes,
10
+ :user,
11
+ :ssh_key,
12
+ :mtu,
13
+ :listen_port,
14
+ :keepalive,
15
+ :verify_host_key
16
+
17
+ def initialize(config_hash, environment = 'development')
18
+ @environment = environment
19
+ env_config = config_hash[environment] || {}
20
+
21
+ @network = env_config['network'] || '10.8.0.0/24'
22
+ @nodes = env_config['nodes'] || {}
23
+ @user = env_config['user'] || 'ubuntu'
24
+ @ssh_key = File.expand_path(env_config['ssh_key'] || '~/.ssh/id_rsa')
25
+ @mtu = env_config['mtu'] || 1280
26
+ @listen_port = env_config['listen_port'] || 51_820
27
+ @keepalive = env_config['keepalive'] || 25
28
+ @verify_host_key = env_config.key?('verify_host_key') ? env_config['verify_host_key'] : true
29
+ end
30
+
31
+ def self.load(config_path = 'config/mesh.yml', environment = nil)
32
+ environment ||= ENV['MESSHY_ENVIRONMENT'] || ENV['RAILS_ENV'] || 'development'
33
+
34
+ raise Error, "Config file not found: #{config_path}" unless File.exist?(config_path)
35
+
36
+ config_hash = YAML.load_file(config_path, aliases: true)
37
+ new(config_hash, environment)
38
+ end
39
+
40
+ def node_names
41
+ @nodes.keys
42
+ end
43
+
44
+ def node_config(name)
45
+ @nodes[name]
46
+ end
47
+
48
+ def each_node(&)
49
+ @nodes.each(&)
50
+ end
51
+
52
+ def network_prefix_length
53
+ return 24 unless @network
54
+
55
+ parts = @network.split('/')
56
+ return 24 if parts.length < 2
57
+
58
+ Integer(parts.last)
59
+ rescue ArgumentError
60
+ 24
61
+ end
62
+
63
+ def validate!
64
+ raise Error, 'No nodes defined' if @nodes.empty?
65
+
66
+ @nodes.each do |name, config|
67
+ raise Error, "Node #{name} missing 'host'" unless config['host']
68
+ raise Error, "Node #{name} missing 'private_ip'" unless config['private_ip']
69
+ end
70
+
71
+ true
72
+ end
73
+
74
+ def verify_host_key_mode
75
+ case @verify_host_key
76
+ when true, 'always', :always
77
+ :always
78
+ when 'accept_new', :accept_new
79
+ :accept_new
80
+ when 'never', :never, false
81
+ :never
82
+ else
83
+ :always
84
+ end
85
+ end
86
+ end
87
+ end