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 +7 -0
- data/LICENSE +23 -0
- data/README.md +296 -0
- data/exe/messhy +6 -0
- data/lib/messhy/cli.rb +280 -0
- data/lib/messhy/configuration.rb +87 -0
- data/lib/messhy/generators/messhy/install_generator.rb +47 -0
- data/lib/messhy/health_checker.rb +203 -0
- data/lib/messhy/host_trust_manager.rb +139 -0
- data/lib/messhy/installer.rb +334 -0
- data/lib/messhy/mesh_builder.rb +70 -0
- data/lib/messhy/railtie.rb +18 -0
- data/lib/messhy/ssh_executor.rb +305 -0
- data/lib/messhy/version.rb +5 -0
- data/lib/messhy/wireguard_status_parser.rb +54 -0
- data/lib/messhy.rb +21 -0
- data/lib/tasks/messhy.rake +32 -0
- data/templates/wg0.conf.erb +24 -0
- metadata +162 -0
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
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
|