boring_services 0.2.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 +22 -0
- data/README.md +461 -0
- data/exe/boringservices +5 -0
- data/lib/boring_services/cli.rb +100 -0
- data/lib/boring_services/configuration.rb +80 -0
- data/lib/boring_services/generators/boring_services/install_generator.rb +25 -0
- data/lib/boring_services/health_checker.rb +47 -0
- data/lib/boring_services/installer.rb +142 -0
- data/lib/boring_services/railtie.rb +19 -0
- data/lib/boring_services/secrets.rb +55 -0
- data/lib/boring_services/services/base.rb +168 -0
- data/lib/boring_services/services/haproxy.rb +272 -0
- data/lib/boring_services/services/memcached.rb +113 -0
- data/lib/boring_services/services/nginx.rb +96 -0
- data/lib/boring_services/services/redis.rb +55 -0
- data/lib/boring_services/ssh_executor.rb +206 -0
- data/lib/boring_services/version.rb +3 -0
- data/lib/boring_services.rb +32 -0
- data/lib/tasks/services.rake +47 -0
- metadata +164 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module BoringServices
|
|
2
|
+
module Services
|
|
3
|
+
class Memcached < Base
|
|
4
|
+
def install
|
|
5
|
+
execute_on_host do
|
|
6
|
+
puts " Installing Memcached on #{label || host}..."
|
|
7
|
+
ssh_executor.install_package('memcached')
|
|
8
|
+
configure_memcached
|
|
9
|
+
ssh_executor.systemd_enable('memcached')
|
|
10
|
+
ssh_executor.systemd_start('memcached')
|
|
11
|
+
end
|
|
12
|
+
rails_credentials_entry
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def uninstall
|
|
16
|
+
execute_on_host do
|
|
17
|
+
puts " Uninstalling Memcached from #{label || host}..."
|
|
18
|
+
ssh_executor.systemd_stop('memcached')
|
|
19
|
+
ssh_executor.systemd_disable('memcached')
|
|
20
|
+
ssh_executor.uninstall_package('memcached')
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def restart
|
|
25
|
+
execute_on_host do
|
|
26
|
+
puts " Restarting Memcached on #{label || host}..."
|
|
27
|
+
ssh_executor.systemd_restart('memcached')
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def reconfigure
|
|
32
|
+
execute_on_host do
|
|
33
|
+
puts " Reconfiguring Memcached on #{label || host}..."
|
|
34
|
+
configure_memcached
|
|
35
|
+
ssh_executor.systemd_restart('memcached')
|
|
36
|
+
end
|
|
37
|
+
rails_credentials_entry
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def configure_memcached
|
|
43
|
+
# Check if custom config file is provided
|
|
44
|
+
if service_config['custom_config_template'] && File.exist?(service_config['custom_config_template'])
|
|
45
|
+
puts " Using custom Memcached config template: #{service_config['custom_config_template']}"
|
|
46
|
+
config_content = File.read(service_config['custom_config_template'])
|
|
47
|
+
upload! StringIO.new(config_content), '/tmp/memcached.conf'
|
|
48
|
+
execute :sudo, :mv, '/tmp/memcached.conf', '/etc/memcached.conf'
|
|
49
|
+
execute :sudo, :chown, 'root:root', '/etc/memcached.conf'
|
|
50
|
+
execute :sudo, :chmod, '644', '/etc/memcached.conf'
|
|
51
|
+
return
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
memory = memory_mb || 64
|
|
55
|
+
listen_port = port || 11_211
|
|
56
|
+
|
|
57
|
+
# Get custom overrides or use defaults
|
|
58
|
+
custom = service_config['custom_params'] || {}
|
|
59
|
+
# Use private_ip (WireGuard) if available, otherwise custom listen_address or localhost
|
|
60
|
+
listen_address = private_ip || custom['listen_address'] || '127.0.0.1'
|
|
61
|
+
max_connections = custom['max_connections'] || 1024
|
|
62
|
+
threads = custom['threads'] || 4
|
|
63
|
+
max_item_size = custom['max_item_size'] # Optional, defaults to 1MB
|
|
64
|
+
verbosity = custom['verbosity'] # Optional, no default
|
|
65
|
+
run_as_user = custom['user'] || 'memcache'
|
|
66
|
+
disable_udp = custom.fetch('disable_udp', true)
|
|
67
|
+
modern_mode = custom.fetch('modern', true)
|
|
68
|
+
slab_growth_factor = custom['slab_growth_factor'] # Optional, default 1.25
|
|
69
|
+
|
|
70
|
+
config_content = <<~CONFIG
|
|
71
|
+
# Production-ready Memcached configuration
|
|
72
|
+
# Run as non-root user
|
|
73
|
+
-u #{run_as_user}
|
|
74
|
+
# Network settings
|
|
75
|
+
-l #{listen_address}
|
|
76
|
+
-p #{listen_port}
|
|
77
|
+
# Memory and performance
|
|
78
|
+
-m #{memory}
|
|
79
|
+
-c #{max_connections}
|
|
80
|
+
-t #{threads}
|
|
81
|
+
CONFIG
|
|
82
|
+
|
|
83
|
+
# Security: Disable UDP to prevent amplification attacks
|
|
84
|
+
config_content += "-U 0\n" if disable_udp
|
|
85
|
+
# Modern mode: enables slab rebalancing, LRU crawler, etc.
|
|
86
|
+
config_content += "-o modern\n" if modern_mode
|
|
87
|
+
# Custom slab growth factor (default 1.25, lower = less memory waste)
|
|
88
|
+
config_content += "-f #{slab_growth_factor}\n" if slab_growth_factor
|
|
89
|
+
# Max item size (default 1MB)
|
|
90
|
+
config_content += "-I #{max_item_size}\n" if max_item_size
|
|
91
|
+
# Verbosity for debugging
|
|
92
|
+
config_content += "#{'-v' * verbosity.to_i}\n" if verbosity&.to_i&.positive?
|
|
93
|
+
|
|
94
|
+
upload! StringIO.new(config_content), '/tmp/memcached.conf'
|
|
95
|
+
execute :sudo, :mv, '/tmp/memcached.conf', '/etc/memcached.conf'
|
|
96
|
+
execute :sudo, :chown, 'root:root', '/etc/memcached.conf'
|
|
97
|
+
execute :sudo, :chmod, '644', '/etc/memcached.conf'
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def rails_credentials_entry
|
|
101
|
+
listen_port = port || 11_211
|
|
102
|
+
server_address = private_ip || host
|
|
103
|
+
region_label = label&.include?('us') ? 'us' : 'eu'
|
|
104
|
+
{
|
|
105
|
+
type: 'memcached',
|
|
106
|
+
region: region_label,
|
|
107
|
+
server: "#{server_address}:#{listen_port}",
|
|
108
|
+
label: label || host
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
module BoringServices
|
|
2
|
+
module Services
|
|
3
|
+
class Nginx < Base
|
|
4
|
+
def install
|
|
5
|
+
execute_on_host do
|
|
6
|
+
puts " Installing Nginx on #{label || host}..."
|
|
7
|
+
ssh_executor.install_package('nginx')
|
|
8
|
+
configure_nginx
|
|
9
|
+
ssh_executor.systemd_enable('nginx')
|
|
10
|
+
ssh_executor.systemd_start('nginx')
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def uninstall
|
|
15
|
+
execute_on_host do
|
|
16
|
+
puts " Uninstalling Nginx from #{label || host}..."
|
|
17
|
+
ssh_executor.systemd_stop('nginx')
|
|
18
|
+
ssh_executor.systemd_disable('nginx')
|
|
19
|
+
ssh_executor.uninstall_package('nginx')
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def restart
|
|
24
|
+
execute_on_host do
|
|
25
|
+
puts " Restarting Nginx on #{label || host}..."
|
|
26
|
+
ssh_executor.systemd_restart('nginx')
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def configure_nginx
|
|
33
|
+
backends = service_config['backends'] || []
|
|
34
|
+
listen_port = port || 80
|
|
35
|
+
ssl_enabled = service_config['ssl'] == true
|
|
36
|
+
|
|
37
|
+
upstream_servers = backends.map do |backend|
|
|
38
|
+
backend_host = backend.is_a?(Hash) ? backend['host'] : backend
|
|
39
|
+
backend_port = backend.is_a?(Hash) ? (backend['port'] || 3000) : 3000
|
|
40
|
+
" server #{backend_host}:#{backend_port};"
|
|
41
|
+
end.join("\n")
|
|
42
|
+
|
|
43
|
+
config_content = <<~NGINX
|
|
44
|
+
upstream backend {
|
|
45
|
+
#{upstream_servers}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
server {
|
|
49
|
+
listen #{listen_port};
|
|
50
|
+
server_name _;
|
|
51
|
+
|
|
52
|
+
location / {
|
|
53
|
+
proxy_pass http://backend;
|
|
54
|
+
proxy_set_header Host $host;
|
|
55
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
56
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
57
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
location /health {
|
|
61
|
+
access_log off;
|
|
62
|
+
return 200 "healthy\\n";
|
|
63
|
+
add_header Content-Type text/plain;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
NGINX
|
|
67
|
+
|
|
68
|
+
if ssl_enabled
|
|
69
|
+
config_content += <<~NGINX
|
|
70
|
+
|
|
71
|
+
server {
|
|
72
|
+
listen 443 ssl http2;
|
|
73
|
+
server_name _;
|
|
74
|
+
|
|
75
|
+
ssl_certificate /etc/nginx/ssl/cert.pem;
|
|
76
|
+
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
|
77
|
+
|
|
78
|
+
location / {
|
|
79
|
+
proxy_pass http://backend;
|
|
80
|
+
proxy_set_header Host $host;
|
|
81
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
82
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
83
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
NGINX
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
upload! StringIO.new(config_content), '/tmp/default'
|
|
90
|
+
execute :sudo, :mv, '/tmp/default', '/etc/nginx/sites-available/default'
|
|
91
|
+
execute :sudo, :chown, 'root:root', '/etc/nginx/sites-available/default'
|
|
92
|
+
execute :sudo, :chmod, '644', '/etc/nginx/sites-available/default'
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module BoringServices
|
|
2
|
+
module Services
|
|
3
|
+
class Redis < Base
|
|
4
|
+
def install
|
|
5
|
+
execute_on_host do
|
|
6
|
+
puts " Installing Redis on #{label || host}..."
|
|
7
|
+
ssh_executor.install_package('redis-server')
|
|
8
|
+
configure_redis
|
|
9
|
+
ssh_executor.systemd_enable('redis-server')
|
|
10
|
+
ssh_executor.systemd_start('redis-server')
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def uninstall
|
|
15
|
+
execute_on_host do
|
|
16
|
+
puts " Uninstalling Redis from #{label || host}..."
|
|
17
|
+
ssh_executor.systemd_stop('redis-server')
|
|
18
|
+
ssh_executor.systemd_disable('redis-server')
|
|
19
|
+
ssh_executor.uninstall_package('redis-server')
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def restart
|
|
24
|
+
execute_on_host do
|
|
25
|
+
puts " Restarting Redis on #{label || host}..."
|
|
26
|
+
ssh_executor.systemd_restart('redis-server')
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def configure_redis
|
|
33
|
+
memory = memory_mb || 256
|
|
34
|
+
listen_port = port || 6379
|
|
35
|
+
password = resolve_secret('redis_password') if config.secrets['redis_password']
|
|
36
|
+
|
|
37
|
+
config_content = <<~REDIS
|
|
38
|
+
bind 0.0.0.0
|
|
39
|
+
port #{listen_port}
|
|
40
|
+
maxmemory #{memory}mb
|
|
41
|
+
maxmemory-policy allkeys-lru
|
|
42
|
+
appendonly yes
|
|
43
|
+
appendfsync everysec
|
|
44
|
+
REDIS
|
|
45
|
+
|
|
46
|
+
config_content += "requirepass #{password}\n" if password
|
|
47
|
+
|
|
48
|
+
upload! StringIO.new(config_content), '/tmp/redis.conf'
|
|
49
|
+
execute :sudo, :mv, '/tmp/redis.conf', '/etc/redis/redis.conf'
|
|
50
|
+
execute :sudo, :chown, 'redis:redis', '/etc/redis/redis.conf'
|
|
51
|
+
execute :sudo, :chmod, '640', '/etc/redis/redis.conf'
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
require 'sshkit'
|
|
2
|
+
require 'sshkit/dsl'
|
|
3
|
+
|
|
4
|
+
module BoringServices
|
|
5
|
+
class SSHExecutor
|
|
6
|
+
include SSHKit::DSL
|
|
7
|
+
|
|
8
|
+
attr_reader :config
|
|
9
|
+
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@config = config
|
|
12
|
+
setup_sshkit
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def execute_on_host(host, &)
|
|
16
|
+
on(formatted_host(host), &)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def execute_on_host_for_service(service, &block)
|
|
20
|
+
hosts = Array(service['hosts'] || service['host']).compact
|
|
21
|
+
raise Error, "No hosts defined for service #{service['name'] || 'unknown'}" if hosts.empty?
|
|
22
|
+
|
|
23
|
+
hosts.each do |host|
|
|
24
|
+
on formatted_host(host) do
|
|
25
|
+
block.call(host)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def install_package(package, host = nil)
|
|
31
|
+
if host
|
|
32
|
+
execute_on_host(host) do
|
|
33
|
+
wait_for_dpkg_lock
|
|
34
|
+
execute :sudo, 'apt-get', 'update'
|
|
35
|
+
execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', package
|
|
36
|
+
end
|
|
37
|
+
else
|
|
38
|
+
# Called from within SSHKit context
|
|
39
|
+
wait_for_dpkg_lock
|
|
40
|
+
backend.execute :sudo, 'apt-get', 'update'
|
|
41
|
+
backend.execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', package
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def wait_for_dpkg_lock
|
|
46
|
+
# Wait for dpkg/apt locks to be released (e.g., unattended-upgrades)
|
|
47
|
+
max_wait = 300 # 5 minutes max
|
|
48
|
+
wait_interval = 10 # Check every 10 seconds
|
|
49
|
+
elapsed = 0
|
|
50
|
+
|
|
51
|
+
loop do
|
|
52
|
+
# Check if dpkg lock exists and is held by another process
|
|
53
|
+
lock_held = backend.test '[ -f /var/lib/dpkg/lock-frontend ]'
|
|
54
|
+
|
|
55
|
+
if lock_held
|
|
56
|
+
# Check if lock is actually held by checking fuser
|
|
57
|
+
processes = backend.capture(:sudo, :fuser, '/var/lib/dpkg/lock-frontend', '2>/dev/null', raise_on_non_zero_exit: false).strip
|
|
58
|
+
|
|
59
|
+
if processes.empty?
|
|
60
|
+
# Lock file exists but no process holding it - safe to proceed
|
|
61
|
+
break
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if elapsed >= max_wait
|
|
65
|
+
puts " ⚠ Warning: dpkg lock still held after #{max_wait}s, proceeding anyway..."
|
|
66
|
+
break
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
puts " ⏳ Waiting for dpkg lock to be released (#{elapsed}s elapsed)..."
|
|
70
|
+
sleep wait_interval
|
|
71
|
+
elapsed += wait_interval
|
|
72
|
+
else
|
|
73
|
+
# No lock file - safe to proceed
|
|
74
|
+
break
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def uninstall_package(package, host = nil)
|
|
80
|
+
if host
|
|
81
|
+
execute_on_host(host) do
|
|
82
|
+
execute :sudo, 'apt-get', 'remove', '-y', package
|
|
83
|
+
execute :sudo, 'apt-get', 'autoremove', '-y'
|
|
84
|
+
end
|
|
85
|
+
else
|
|
86
|
+
backend.execute :sudo, 'apt-get', 'remove', '-y', package
|
|
87
|
+
backend.execute :sudo, 'apt-get', 'autoremove', '-y'
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def upload_template(template_path, destination, context = {}, host = nil)
|
|
92
|
+
template = File.read(template_path)
|
|
93
|
+
result = ERB.new(template).result_with_hash(context)
|
|
94
|
+
|
|
95
|
+
if host
|
|
96
|
+
execute_on_host(host) do
|
|
97
|
+
upload! StringIO.new(result), destination
|
|
98
|
+
execute :sudo, 'chown', 'root:root', destination
|
|
99
|
+
execute :sudo, 'chmod', '644', destination
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
backend.upload! StringIO.new(result), destination
|
|
103
|
+
backend.execute :sudo, 'chown', 'root:root', destination
|
|
104
|
+
backend.execute :sudo, 'chmod', '644', destination
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def systemd_enable(service_name, host = nil)
|
|
109
|
+
if host
|
|
110
|
+
execute_on_host(host) do
|
|
111
|
+
execute :sudo, 'systemctl', 'daemon-reload'
|
|
112
|
+
execute :sudo, 'systemctl', 'enable', service_name
|
|
113
|
+
end
|
|
114
|
+
else
|
|
115
|
+
backend.execute :sudo, 'systemctl', 'daemon-reload'
|
|
116
|
+
backend.execute :sudo, 'systemctl', 'enable', service_name
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def systemd_start(service_name, host = nil)
|
|
121
|
+
if host
|
|
122
|
+
execute_on_host(host) do
|
|
123
|
+
execute :sudo, 'systemctl', 'start', service_name
|
|
124
|
+
end
|
|
125
|
+
else
|
|
126
|
+
backend.execute :sudo, 'systemctl', 'start', service_name
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def systemd_stop(service_name, host = nil)
|
|
131
|
+
if host
|
|
132
|
+
execute_on_host(host) do
|
|
133
|
+
execute :sudo, 'systemctl', 'stop', service_name
|
|
134
|
+
end
|
|
135
|
+
else
|
|
136
|
+
backend.execute :sudo, 'systemctl', 'stop', service_name
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def systemd_restart(service_name, host = nil)
|
|
141
|
+
if host
|
|
142
|
+
execute_on_host(host) do
|
|
143
|
+
execute :sudo, 'systemctl', 'restart', service_name
|
|
144
|
+
end
|
|
145
|
+
else
|
|
146
|
+
backend.execute :sudo, 'systemctl', 'restart', service_name
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def systemd_disable(service_name, host = nil)
|
|
151
|
+
if host
|
|
152
|
+
execute_on_host(host) do
|
|
153
|
+
execute :sudo, 'systemctl', 'disable', service_name
|
|
154
|
+
end
|
|
155
|
+
else
|
|
156
|
+
backend.execute :sudo, 'systemctl', 'disable', service_name
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def systemd_status(service_name, host = nil)
|
|
161
|
+
if host
|
|
162
|
+
result = nil
|
|
163
|
+
execute_on_host(host) do
|
|
164
|
+
result = capture :sudo, 'systemctl', 'status', service_name, raise_on_non_zero_exit: false
|
|
165
|
+
end
|
|
166
|
+
result
|
|
167
|
+
else
|
|
168
|
+
backend.capture :sudo, 'systemctl', 'status', service_name, raise_on_non_zero_exit: false
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
def backend
|
|
175
|
+
SSHKit::Backend.current ||
|
|
176
|
+
raise('SSHKit backend is not available. Provide a host or call within execute_on_host.')
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def setup_sshkit
|
|
180
|
+
SSHKit::Backend::Netssh.configure do |ssh|
|
|
181
|
+
ssh.ssh_options = {
|
|
182
|
+
user: config.user,
|
|
183
|
+
keys: [File.expand_path(config.ssh_key)],
|
|
184
|
+
forward_agent: config.forward_agent,
|
|
185
|
+
auth_methods: config.ssh_auth_methods,
|
|
186
|
+
keys_only: !config.use_ssh_agent,
|
|
187
|
+
use_agent: config.use_ssh_agent
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def formatted_host(host)
|
|
193
|
+
case host
|
|
194
|
+
when Hash
|
|
195
|
+
target_host = host['host'] || host[:host]
|
|
196
|
+
raise Error, 'Host entry missing host field' unless target_host
|
|
197
|
+
|
|
198
|
+
user = host['user'] || host[:user] || config.user
|
|
199
|
+
"#{user}@#{target_host}"
|
|
200
|
+
else
|
|
201
|
+
host_string = host.to_s
|
|
202
|
+
host_string.include?('@') ? host_string : "#{config.user}@#{host_string}"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require_relative 'boring_services/version'
|
|
2
|
+
require_relative 'boring_services/configuration'
|
|
3
|
+
require_relative 'boring_services/secrets'
|
|
4
|
+
require_relative 'boring_services/cli'
|
|
5
|
+
require_relative 'boring_services/installer'
|
|
6
|
+
require_relative 'boring_services/ssh_executor'
|
|
7
|
+
require_relative 'boring_services/health_checker'
|
|
8
|
+
|
|
9
|
+
require_relative 'boring_services/services/base'
|
|
10
|
+
require_relative 'boring_services/services/memcached'
|
|
11
|
+
require_relative 'boring_services/services/redis'
|
|
12
|
+
require_relative 'boring_services/services/haproxy'
|
|
13
|
+
require_relative 'boring_services/services/nginx'
|
|
14
|
+
|
|
15
|
+
module BoringServices
|
|
16
|
+
class Error < StandardError; end
|
|
17
|
+
|
|
18
|
+
def self.root
|
|
19
|
+
File.expand_path('..', __dir__)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.status
|
|
23
|
+
config = Configuration.load
|
|
24
|
+
health_checker = HealthChecker.new(config)
|
|
25
|
+
health_checker.check_all
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
require 'stringio'
|
|
29
|
+
|
|
30
|
+
# Load railtie if Rails is already loaded (safe to require Rails components)
|
|
31
|
+
# This avoids load order issues with ActiveSupport
|
|
32
|
+
require_relative 'boring_services/railtie' if defined?(Rails::Railtie)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
namespace :boring_services do
|
|
2
|
+
def boringservices_cli(*args)
|
|
3
|
+
cli_args = ['bundle', 'exec', 'boringservices', *args]
|
|
4
|
+
env = ENV.fetch('BORING_SERVICES_ENV', nil)
|
|
5
|
+
cli_args += ['-e', env] if env && !env.empty?
|
|
6
|
+
config_path = ENV.fetch('BORING_SERVICES_CONFIG', nil)
|
|
7
|
+
cli_args += ['-c', config_path] if config_path && !config_path.empty?
|
|
8
|
+
system(*cli_args)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
desc 'Install BoringServices configuration'
|
|
12
|
+
task :install do
|
|
13
|
+
system('rails generate boring_services:install')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc 'Deploy all services'
|
|
17
|
+
task :setup do
|
|
18
|
+
config_path = 'config/services.yml'
|
|
19
|
+
unless File.exist?(config_path)
|
|
20
|
+
puts "⚠️ Config file not found: #{config_path}"
|
|
21
|
+
puts '📦 Running install generator first...'
|
|
22
|
+
system('rails generate boring_services:install') || raise('Failed to generate config file')
|
|
23
|
+
end
|
|
24
|
+
boringservices_cli('setup')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
desc 'Check services health'
|
|
28
|
+
task :health do
|
|
29
|
+
boringservices_cli('status')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
desc 'Restart all services'
|
|
33
|
+
task :restart do
|
|
34
|
+
boringservices_cli('restart')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc 'Reconfigure all services (skip package install, update config only)'
|
|
38
|
+
task :reconfigure do
|
|
39
|
+
boringservices_cli('reconfigure')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
desc 'Show services status'
|
|
43
|
+
task :status do
|
|
44
|
+
puts "\n🔧 Infrastructure Services Status\n\n"
|
|
45
|
+
boringservices_cli('status')
|
|
46
|
+
end
|
|
47
|
+
end
|