boring_services 0.2.0 → 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 +4 -4
- data/lib/boring_services/cli.rb +58 -24
- data/lib/boring_services/health_checker.rb +76 -13
- data/lib/boring_services/railtie.rb +6 -0
- data/lib/boring_services/service_locator.rb +105 -0
- data/lib/boring_services/services/memcached.rb +1 -1
- data/lib/boring_services/services/redis.rb +7 -1
- data/lib/boring_services/ssh_executor.rb +43 -46
- data/lib/boring_services/version.rb +1 -1
- data/lib/boring_services.rb +62 -7
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 156bb67ea987370602ed678f3d8a525f6eeae09dcbc2a3ae8ac23fa85b474ca4
|
|
4
|
+
data.tar.gz: 8ce1325011cac932711b77afed6a5d0307bebedaa4e52fc2b65d9a153daf2e91
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 72bf5746276b30243a9b75b8091491afd3e58e5e8feac6b3a8f62bc7cafa8f9088d6d1f279f075b6f3f77d0f350017a4bf277e18a3ef4844d273293fb2f3b363
|
|
7
|
+
data.tar.gz: 07ac07fe8b309a06ff23ca77a7681aab03c77d8ecc0fac5e86e6723c999eb05976827e2bafdd072f7dfc0eef700c731329402a627b6ab64d2492983aaf6d125f
|
data/lib/boring_services/cli.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'thor'
|
|
2
4
|
|
|
3
5
|
module BoringServices
|
|
@@ -16,8 +18,7 @@ module BoringServices
|
|
|
16
18
|
desc 'setup', 'Setup/install all services (alias for install)'
|
|
17
19
|
def setup
|
|
18
20
|
config = Configuration.load(options[:config], options[:environment])
|
|
19
|
-
|
|
20
|
-
installer.install_all
|
|
21
|
+
Installer.new(config).install_all
|
|
21
22
|
rescue Error => e
|
|
22
23
|
puts "Error: #{e.message}"
|
|
23
24
|
exit 1
|
|
@@ -27,12 +28,7 @@ module BoringServices
|
|
|
27
28
|
def install(service_name = nil)
|
|
28
29
|
config = Configuration.load(options[:config], options[:environment])
|
|
29
30
|
installer = Installer.new(config)
|
|
30
|
-
|
|
31
|
-
if service_name
|
|
32
|
-
installer.install_service(service_name)
|
|
33
|
-
else
|
|
34
|
-
installer.install_all
|
|
35
|
-
end
|
|
31
|
+
service_name ? installer.install_service(service_name) : installer.install_all
|
|
36
32
|
rescue Error => e
|
|
37
33
|
puts "Error: #{e.message}"
|
|
38
34
|
exit 1
|
|
@@ -41,8 +37,7 @@ module BoringServices
|
|
|
41
37
|
desc 'uninstall SERVICE', 'Uninstall a specific service'
|
|
42
38
|
def uninstall(service_name)
|
|
43
39
|
config = Configuration.load(options[:config], options[:environment])
|
|
44
|
-
|
|
45
|
-
installer.uninstall_service(service_name)
|
|
40
|
+
Installer.new(config).uninstall_service(service_name)
|
|
46
41
|
rescue Error => e
|
|
47
42
|
puts "Error: #{e.message}"
|
|
48
43
|
exit 1
|
|
@@ -51,8 +46,7 @@ module BoringServices
|
|
|
51
46
|
desc 'restart SERVICE', 'Restart a specific service'
|
|
52
47
|
def restart(service_name)
|
|
53
48
|
config = Configuration.load(options[:config], options[:environment])
|
|
54
|
-
|
|
55
|
-
installer.restart_service(service_name)
|
|
49
|
+
Installer.new(config).restart_service(service_name)
|
|
56
50
|
rescue Error => e
|
|
57
51
|
puts "Error: #{e.message}"
|
|
58
52
|
exit 1
|
|
@@ -62,12 +56,7 @@ module BoringServices
|
|
|
62
56
|
def reconfigure(service_name = nil)
|
|
63
57
|
config = Configuration.load(options[:config], options[:environment])
|
|
64
58
|
installer = Installer.new(config)
|
|
65
|
-
|
|
66
|
-
if service_name
|
|
67
|
-
installer.reconfigure_service(service_name)
|
|
68
|
-
else
|
|
69
|
-
installer.reconfigure_all
|
|
70
|
-
end
|
|
59
|
+
service_name ? installer.reconfigure_service(service_name) : installer.reconfigure_all
|
|
71
60
|
rescue Error => e
|
|
72
61
|
puts "Error: #{e.message}"
|
|
73
62
|
exit 1
|
|
@@ -76,15 +65,12 @@ module BoringServices
|
|
|
76
65
|
desc 'status', 'Check health status of all services'
|
|
77
66
|
def status
|
|
78
67
|
Configuration.load(options[:config], options[:environment])
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
results.each do |service_name, result|
|
|
68
|
+
BoringServices.status.each do |service_name, result|
|
|
82
69
|
puts "\n#{service_name}: #{result[:status]}"
|
|
83
70
|
next unless result[:hosts]
|
|
84
71
|
|
|
85
|
-
result[:hosts].each do |
|
|
86
|
-
|
|
87
|
-
puts " #{status_icon} #{host}: #{host_result[:running] ? 'running' : 'stopped'}"
|
|
72
|
+
result[:hosts].each do |host_result|
|
|
73
|
+
print_host_status(service_name, host_result)
|
|
88
74
|
end
|
|
89
75
|
end
|
|
90
76
|
rescue Error => e
|
|
@@ -96,5 +82,53 @@ module BoringServices
|
|
|
96
82
|
def version
|
|
97
83
|
puts "boring_services #{VERSION}"
|
|
98
84
|
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def print_host_status(service_name, host_result)
|
|
89
|
+
status_icon = host_result[:running] ? '✓' : '✗'
|
|
90
|
+
label = host_result[:label] || host_result[:host]
|
|
91
|
+
status_text = host_result[:running] ? 'running' : 'stopped'
|
|
92
|
+
puts " #{status_icon} #{label}: #{status_text}"
|
|
93
|
+
|
|
94
|
+
return unless host_result[:stats]&.any?
|
|
95
|
+
|
|
96
|
+
case service_name
|
|
97
|
+
when 'redis' then print_redis_stats(host_result[:stats])
|
|
98
|
+
when 'memcached' then print_memcached_stats(host_result[:stats])
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def print_redis_stats(stats)
|
|
103
|
+
memory = stats['used_memory_human'] || 'N/A'
|
|
104
|
+
max_memory = stats['maxmemory_human'] || 'N/A'
|
|
105
|
+
clients = stats['connected_clients'] || '0'
|
|
106
|
+
hit_rate = calculate_hit_rate(stats['keyspace_hits'], stats['keyspace_misses'])
|
|
107
|
+
puts " memory: #{memory} / #{max_memory}, clients: #{clients}, hit rate: #{hit_rate}%"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def print_memcached_stats(stats)
|
|
111
|
+
bytes = format_bytes(stats['bytes']&.to_i || 0)
|
|
112
|
+
conns = stats['curr_connections'] || '0'
|
|
113
|
+
items = stats['curr_items'] || '0'
|
|
114
|
+
hit_rate = calculate_hit_rate(stats['get_hits'], stats['get_misses'])
|
|
115
|
+
puts " memory: #{bytes}, connections: #{conns}, items: #{items}, hit rate: #{hit_rate}%"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def calculate_hit_rate(hits, misses)
|
|
119
|
+
hits = hits&.to_i || 0
|
|
120
|
+
misses = misses&.to_i || 0
|
|
121
|
+
total = hits + misses
|
|
122
|
+
total.positive? ? ((hits.to_f / total) * 100).round(1) : 0
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def format_bytes(bytes)
|
|
126
|
+
return '0B' if bytes.zero?
|
|
127
|
+
|
|
128
|
+
units = %w[B KB MB GB]
|
|
129
|
+
exp = (Math.log(bytes) / Math.log(1024)).to_i
|
|
130
|
+
exp = [exp, units.length - 1].min
|
|
131
|
+
"#{(bytes.to_f / (1024**exp)).round(1)}#{units[exp]}"
|
|
132
|
+
end
|
|
99
133
|
end
|
|
100
134
|
end
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module BoringServices
|
|
2
4
|
class HealthChecker
|
|
5
|
+
REDIS_INFO_KEYS = %w[used_memory_human connected_clients keyspace_hits keyspace_misses maxmemory_human].freeze
|
|
6
|
+
|
|
3
7
|
attr_reader :config, :ssh_executor
|
|
4
8
|
|
|
5
9
|
def initialize(config)
|
|
@@ -16,32 +20,91 @@ module BoringServices
|
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
def check_service(service)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return { status: 'no_host' } unless host
|
|
22
|
-
|
|
23
|
-
host_result = check_service_on_host(service_name, host)
|
|
23
|
+
hosts = extract_hosts(service)
|
|
24
|
+
return { status: 'no_host' } if hosts.empty?
|
|
24
25
|
|
|
26
|
+
host_results = hosts.map { |host_config| check_host(service['name'], host_config) }
|
|
25
27
|
{
|
|
26
|
-
status:
|
|
27
|
-
|
|
28
|
+
status: host_results.all? { |r| r[:running] } ? 'healthy' : 'unhealthy',
|
|
29
|
+
hosts: host_results
|
|
28
30
|
}
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
private
|
|
32
34
|
|
|
35
|
+
def extract_hosts(service)
|
|
36
|
+
return Array(service['hosts']) if service['hosts']
|
|
37
|
+
return [service['host']] if service['host']
|
|
38
|
+
|
|
39
|
+
[]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def check_host(service_name, host_config)
|
|
43
|
+
host = host_config.is_a?(Hash) ? (host_config['host'] || host_config[:host]) : host_config
|
|
44
|
+
label = host_config.is_a?(Hash) ? (host_config['label'] || host_config[:label]) : nil
|
|
45
|
+
|
|
46
|
+
result = check_service_on_host(service_name, host)
|
|
47
|
+
result[:label] = label
|
|
48
|
+
result[:host] = host
|
|
49
|
+
result
|
|
50
|
+
end
|
|
51
|
+
|
|
33
52
|
def check_service_on_host(service_name, host)
|
|
34
|
-
|
|
53
|
+
systemd_service = service_name == 'redis' ? 'redis-server' : service_name
|
|
54
|
+
status_output = ssh_executor.systemd_status(systemd_service, host)
|
|
55
|
+
running = status_output.to_s.include?('active (running)')
|
|
35
56
|
|
|
57
|
+
result = { running: running, message: status_output.to_s }
|
|
58
|
+
result[:stats] = fetch_stats(service_name, host) if running
|
|
59
|
+
result
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
{ running: false, message: e.message }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def fetch_stats(service_name, host)
|
|
65
|
+
case service_name
|
|
66
|
+
when 'redis' then get_redis_stats(host)
|
|
67
|
+
when 'memcached' then get_memcached_stats(host)
|
|
68
|
+
else {}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def get_redis_stats(host)
|
|
73
|
+
password = config.secrets['redis_password']
|
|
74
|
+
password_resolved = password ? Secrets.resolve(password) : nil
|
|
75
|
+
|
|
76
|
+
result = {}
|
|
36
77
|
ssh_executor.execute_on_host(host) do
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
78
|
+
args = ['redis-cli']
|
|
79
|
+
args += ['-a', password_resolved] if password_resolved
|
|
80
|
+
args << 'INFO'
|
|
81
|
+
|
|
82
|
+
info = capture(*args, raise_on_non_zero_exit: false)
|
|
83
|
+
info.to_s.each_line do |line|
|
|
84
|
+
key, value = line.strip.split(':')
|
|
85
|
+
result[key] = value if key && value && REDIS_INFO_KEYS.include?(key)
|
|
86
|
+
end
|
|
40
87
|
end
|
|
88
|
+
result
|
|
89
|
+
rescue StandardError
|
|
90
|
+
{}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def get_memcached_stats(host)
|
|
94
|
+
result = {}
|
|
95
|
+
ssh_executor.execute_on_host(host) do
|
|
96
|
+
stats = capture(:bash, '-c', "echo 'stats' | nc localhost 11211", raise_on_non_zero_exit: false)
|
|
97
|
+
stats.to_s.each_line do |line|
|
|
98
|
+
parts = line.strip.split
|
|
99
|
+
next unless parts[0] == 'STAT' && parts.length >= 3
|
|
100
|
+
next unless %w[bytes curr_connections get_hits get_misses curr_items].include?(parts[1])
|
|
41
101
|
|
|
102
|
+
result[parts[1]] = parts[2]
|
|
103
|
+
end
|
|
104
|
+
end
|
|
42
105
|
result
|
|
43
|
-
rescue StandardError
|
|
44
|
-
{
|
|
106
|
+
rescue StandardError
|
|
107
|
+
{}
|
|
45
108
|
end
|
|
46
109
|
end
|
|
47
110
|
end
|
|
@@ -5,6 +5,12 @@ begin
|
|
|
5
5
|
class Railtie < Rails::Railtie
|
|
6
6
|
railtie_name :boring_services
|
|
7
7
|
|
|
8
|
+
# Initialize service locator after Rails config is loaded
|
|
9
|
+
initializer 'boring_services.initialize' do
|
|
10
|
+
# Reset locator so it picks up the correct Rails environment
|
|
11
|
+
BoringServices.reset_locator!
|
|
12
|
+
end
|
|
13
|
+
|
|
8
14
|
rake_tasks do
|
|
9
15
|
load File.expand_path('../tasks/services.rake', __dir__)
|
|
10
16
|
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BoringServices
|
|
4
|
+
class ServiceLocator
|
|
5
|
+
attr_reader :config
|
|
6
|
+
|
|
7
|
+
def initialize(config = nil)
|
|
8
|
+
@config = config || Configuration.load
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Get all hosts for a service
|
|
12
|
+
# Returns array of host hashes with :host, :private_ip, :label keys
|
|
13
|
+
def hosts_for(service_name)
|
|
14
|
+
service = config.service_config(service_name.to_s)
|
|
15
|
+
return [] unless service
|
|
16
|
+
|
|
17
|
+
normalize_hosts(service['hosts'] || [])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Get the connection IP for a service by region/label
|
|
21
|
+
# Prefers private_ip, falls back to host
|
|
22
|
+
# Label matching: "redis-eu" matches region "eu", "redis-us" matches "us"
|
|
23
|
+
def host_for_region(service_name, region)
|
|
24
|
+
return nil if region.to_s.strip.empty?
|
|
25
|
+
|
|
26
|
+
hosts = hosts_for(service_name)
|
|
27
|
+
host_entry = hosts.find do |h|
|
|
28
|
+
label = h[:label].to_s.downcase
|
|
29
|
+
region_str = region.to_s.downcase
|
|
30
|
+
label == region_str || label.end_with?("-#{region_str}") || label.start_with?("#{region_str}-")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
return nil unless host_entry
|
|
34
|
+
|
|
35
|
+
connection_ip(host_entry)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Get the first available host for a service (prefers private_ip)
|
|
39
|
+
def primary_host(service_name)
|
|
40
|
+
hosts = hosts_for(service_name)
|
|
41
|
+
return nil if hosts.empty?
|
|
42
|
+
|
|
43
|
+
connection_ip(hosts.first)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get all connection IPs for a service (prefers private_ip for each)
|
|
47
|
+
def all_hosts(service_name)
|
|
48
|
+
hosts_for(service_name).map { |h| connection_ip(h) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Get port for a service
|
|
52
|
+
def port_for(service_name)
|
|
53
|
+
service = config.service_config(service_name.to_s)
|
|
54
|
+
service&.dig('port')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Build a Redis URL for a region
|
|
58
|
+
def redis_url(region: nil, password: nil, db: 0)
|
|
59
|
+
host = region ? host_for_region('redis', region) : primary_host('redis')
|
|
60
|
+
return nil unless host
|
|
61
|
+
|
|
62
|
+
port = port_for('redis') || 6379
|
|
63
|
+
auth = password.to_s.empty? ? '' : ":#{password}@"
|
|
64
|
+
"redis://#{auth}#{host}:#{port}/#{db}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Build memcached connection string (host:port,host:port format)
|
|
68
|
+
def memcached_servers(region: nil)
|
|
69
|
+
hosts = if region
|
|
70
|
+
host = host_for_region('memcached', region)
|
|
71
|
+
host ? [host] : []
|
|
72
|
+
else
|
|
73
|
+
all_hosts('memcached')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
return nil if hosts.empty?
|
|
77
|
+
|
|
78
|
+
port = port_for('memcached') || 11211
|
|
79
|
+
hosts.map { |h| "#{h}:#{port}" }.join(',')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# Normalize hosts array - handles both simple strings and hashes
|
|
85
|
+
def normalize_hosts(hosts)
|
|
86
|
+
hosts.map do |h|
|
|
87
|
+
if h.is_a?(Hash)
|
|
88
|
+
{
|
|
89
|
+
host: h['host'],
|
|
90
|
+
private_ip: h['private_ip'],
|
|
91
|
+
label: h['label']
|
|
92
|
+
}
|
|
93
|
+
else
|
|
94
|
+
{ host: h.to_s, private_ip: nil, label: nil }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get connection IP - prefer private_ip if available
|
|
100
|
+
def connection_ip(host_entry)
|
|
101
|
+
ip = host_entry[:private_ip].to_s.strip
|
|
102
|
+
ip.empty? ? host_entry[:host] : ip
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -33,10 +33,12 @@ module BoringServices
|
|
|
33
33
|
memory = memory_mb || 256
|
|
34
34
|
listen_port = port || 6379
|
|
35
35
|
password = resolve_secret('redis_password') if config.secrets['redis_password']
|
|
36
|
+
bind_address = private_ip.to_s.strip.empty? ? "0.0.0.0" : "127.0.0.1 #{private_ip}"
|
|
36
37
|
|
|
37
38
|
config_content = <<~REDIS
|
|
38
|
-
bind
|
|
39
|
+
bind #{bind_address}
|
|
39
40
|
port #{listen_port}
|
|
41
|
+
dir /var/lib/redis
|
|
40
42
|
maxmemory #{memory}mb
|
|
41
43
|
maxmemory-policy allkeys-lru
|
|
42
44
|
appendonly yes
|
|
@@ -49,6 +51,10 @@ module BoringServices
|
|
|
49
51
|
execute :sudo, :mv, '/tmp/redis.conf', '/etc/redis/redis.conf'
|
|
50
52
|
execute :sudo, :chown, 'redis:redis', '/etc/redis/redis.conf'
|
|
51
53
|
execute :sudo, :chmod, '640', '/etc/redis/redis.conf'
|
|
54
|
+
|
|
55
|
+
# Set vm.overcommit_memory for Redis background saves
|
|
56
|
+
execute :sudo, :sysctl, '-w', 'vm.overcommit_memory=1'
|
|
57
|
+
execute "echo 'vm.overcommit_memory = 1' | sudo tee -a /etc/sysctl.conf > /dev/null || true"
|
|
52
58
|
end
|
|
53
59
|
end
|
|
54
60
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'sshkit'
|
|
2
4
|
require 'sshkit/dsl'
|
|
3
5
|
|
|
@@ -5,6 +7,9 @@ module BoringServices
|
|
|
5
7
|
class SSHExecutor
|
|
6
8
|
include SSHKit::DSL
|
|
7
9
|
|
|
10
|
+
DPKG_LOCK_MAX_WAIT = 300
|
|
11
|
+
DPKG_LOCK_INTERVAL = 10
|
|
12
|
+
|
|
8
13
|
attr_reader :config
|
|
9
14
|
|
|
10
15
|
def initialize(config)
|
|
@@ -35,7 +40,6 @@ module BoringServices
|
|
|
35
40
|
execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', package
|
|
36
41
|
end
|
|
37
42
|
else
|
|
38
|
-
# Called from within SSHKit context
|
|
39
43
|
wait_for_dpkg_lock
|
|
40
44
|
backend.execute :sudo, 'apt-get', 'update'
|
|
41
45
|
backend.execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', package
|
|
@@ -43,36 +47,23 @@ module BoringServices
|
|
|
43
47
|
end
|
|
44
48
|
|
|
45
49
|
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
50
|
elapsed = 0
|
|
50
51
|
|
|
51
52
|
loop do
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
53
|
+
break unless backend.test '[ -f /var/lib/dpkg/lock-frontend ]'
|
|
54
|
+
|
|
55
|
+
processes = backend.capture(:sudo, :fuser, '/var/lib/dpkg/lock-frontend', '2>/dev/null',
|
|
56
|
+
raise_on_non_zero_exit: false).strip
|
|
57
|
+
break if processes.empty?
|
|
58
|
+
|
|
59
|
+
if elapsed >= DPKG_LOCK_MAX_WAIT
|
|
60
|
+
puts " ⚠ Warning: dpkg lock still held after #{DPKG_LOCK_MAX_WAIT}s, proceeding anyway..."
|
|
74
61
|
break
|
|
75
62
|
end
|
|
63
|
+
|
|
64
|
+
puts " ⏳ Waiting for dpkg lock to be released (#{elapsed}s elapsed)..."
|
|
65
|
+
sleep DPKG_LOCK_INTERVAL
|
|
66
|
+
elapsed += DPKG_LOCK_INTERVAL
|
|
76
67
|
end
|
|
77
68
|
end
|
|
78
69
|
|
|
@@ -119,9 +110,7 @@ module BoringServices
|
|
|
119
110
|
|
|
120
111
|
def systemd_start(service_name, host = nil)
|
|
121
112
|
if host
|
|
122
|
-
execute_on_host(host)
|
|
123
|
-
execute :sudo, 'systemctl', 'start', service_name
|
|
124
|
-
end
|
|
113
|
+
execute_on_host(host) { execute :sudo, 'systemctl', 'start', service_name }
|
|
125
114
|
else
|
|
126
115
|
backend.execute :sudo, 'systemctl', 'start', service_name
|
|
127
116
|
end
|
|
@@ -129,9 +118,7 @@ module BoringServices
|
|
|
129
118
|
|
|
130
119
|
def systemd_stop(service_name, host = nil)
|
|
131
120
|
if host
|
|
132
|
-
execute_on_host(host)
|
|
133
|
-
execute :sudo, 'systemctl', 'stop', service_name
|
|
134
|
-
end
|
|
121
|
+
execute_on_host(host) { execute :sudo, 'systemctl', 'stop', service_name }
|
|
135
122
|
else
|
|
136
123
|
backend.execute :sudo, 'systemctl', 'stop', service_name
|
|
137
124
|
end
|
|
@@ -139,9 +126,7 @@ module BoringServices
|
|
|
139
126
|
|
|
140
127
|
def systemd_restart(service_name, host = nil)
|
|
141
128
|
if host
|
|
142
|
-
execute_on_host(host)
|
|
143
|
-
execute :sudo, 'systemctl', 'restart', service_name
|
|
144
|
-
end
|
|
129
|
+
execute_on_host(host) { execute :sudo, 'systemctl', 'restart', service_name }
|
|
145
130
|
else
|
|
146
131
|
backend.execute :sudo, 'systemctl', 'restart', service_name
|
|
147
132
|
end
|
|
@@ -149,9 +134,7 @@ module BoringServices
|
|
|
149
134
|
|
|
150
135
|
def systemd_disable(service_name, host = nil)
|
|
151
136
|
if host
|
|
152
|
-
execute_on_host(host)
|
|
153
|
-
execute :sudo, 'systemctl', 'disable', service_name
|
|
154
|
-
end
|
|
137
|
+
execute_on_host(host) { execute :sudo, 'systemctl', 'disable', service_name }
|
|
155
138
|
else
|
|
156
139
|
backend.execute :sudo, 'systemctl', 'disable', service_name
|
|
157
140
|
end
|
|
@@ -159,21 +142,28 @@ module BoringServices
|
|
|
159
142
|
|
|
160
143
|
def systemd_status(service_name, host = nil)
|
|
161
144
|
if host
|
|
162
|
-
|
|
145
|
+
output = nil
|
|
163
146
|
execute_on_host(host) do
|
|
164
|
-
|
|
147
|
+
output = capture :sudo, 'systemctl', 'status', service_name, raise_on_non_zero_exit: false
|
|
165
148
|
end
|
|
166
|
-
|
|
149
|
+
output
|
|
167
150
|
else
|
|
168
151
|
backend.capture :sudo, 'systemctl', 'status', service_name, raise_on_non_zero_exit: false
|
|
169
152
|
end
|
|
170
153
|
end
|
|
171
154
|
|
|
155
|
+
def capture_on_host(host, *args)
|
|
156
|
+
output = nil
|
|
157
|
+
execute_on_host(host) do
|
|
158
|
+
output = capture(*args, raise_on_non_zero_exit: false)
|
|
159
|
+
end
|
|
160
|
+
output
|
|
161
|
+
end
|
|
162
|
+
|
|
172
163
|
private
|
|
173
164
|
|
|
174
165
|
def backend
|
|
175
|
-
SSHKit::Backend.current ||
|
|
176
|
-
raise('SSHKit backend is not available. Provide a host or call within execute_on_host.')
|
|
166
|
+
SSHKit::Backend.current || raise(Error, 'SSHKit backend not available')
|
|
177
167
|
end
|
|
178
168
|
|
|
179
169
|
def setup_sshkit
|
|
@@ -195,12 +185,19 @@ module BoringServices
|
|
|
195
185
|
target_host = host['host'] || host[:host]
|
|
196
186
|
raise Error, 'Host entry missing host field' unless target_host
|
|
197
187
|
|
|
198
|
-
|
|
199
|
-
"#{user}@#{target_host}"
|
|
188
|
+
build_sshkit_host(target_host, host['user'] || host[:user])
|
|
200
189
|
else
|
|
201
190
|
host_string = host.to_s
|
|
202
|
-
host_string.include?('@')
|
|
191
|
+
return SSHKit::Host.new(host_string) if host_string.include?('@')
|
|
192
|
+
|
|
193
|
+
build_sshkit_host(host_string, nil)
|
|
203
194
|
end
|
|
204
195
|
end
|
|
196
|
+
|
|
197
|
+
def build_sshkit_host(hostname, user)
|
|
198
|
+
sshkit_host = SSHKit::Host.new(hostname)
|
|
199
|
+
sshkit_host.user = user || config.user
|
|
200
|
+
sshkit_host
|
|
201
|
+
end
|
|
205
202
|
end
|
|
206
203
|
end
|
data/lib/boring_services.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative 'boring_services/cli'
|
|
|
5
5
|
require_relative 'boring_services/installer'
|
|
6
6
|
require_relative 'boring_services/ssh_executor'
|
|
7
7
|
require_relative 'boring_services/health_checker'
|
|
8
|
+
require_relative 'boring_services/service_locator'
|
|
8
9
|
|
|
9
10
|
require_relative 'boring_services/services/base'
|
|
10
11
|
require_relative 'boring_services/services/memcached'
|
|
@@ -15,14 +16,68 @@ require_relative 'boring_services/services/nginx'
|
|
|
15
16
|
module BoringServices
|
|
16
17
|
class Error < StandardError; end
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
class << self
|
|
20
|
+
def root
|
|
21
|
+
File.expand_path('..', __dir__)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def status
|
|
25
|
+
config = Configuration.load
|
|
26
|
+
health_checker = HealthChecker.new(config)
|
|
27
|
+
health_checker.check_all
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Service locator instance (cached)
|
|
31
|
+
def locator
|
|
32
|
+
@locator ||= ServiceLocator.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Reset cached locator (useful for testing or config reload)
|
|
36
|
+
def reset_locator!
|
|
37
|
+
@locator = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Convenience methods - delegate to locator
|
|
41
|
+
|
|
42
|
+
# Get Redis host for a region (e.g., "eu", "us")
|
|
43
|
+
def redis_host(region = nil)
|
|
44
|
+
region ? locator.host_for_region('redis', region) : locator.primary_host('redis')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get Redis port
|
|
48
|
+
def redis_port
|
|
49
|
+
locator.port_for('redis') || 6379
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Build Redis URL
|
|
53
|
+
def redis_url(region: nil, password: nil, db: 0)
|
|
54
|
+
locator.redis_url(region: region, password: password, db: db)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get Memcached host for a region
|
|
58
|
+
def memcached_host(region = nil)
|
|
59
|
+
region ? locator.host_for_region('memcached', region) : locator.primary_host('memcached')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get Memcached port
|
|
63
|
+
def memcached_port
|
|
64
|
+
locator.port_for('memcached') || 11211
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get Memcached servers string (host:port,host:port)
|
|
68
|
+
def memcached_servers(region: nil)
|
|
69
|
+
locator.memcached_servers(region: region)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Generic: get host for any service by region
|
|
73
|
+
def host_for(service, region = nil)
|
|
74
|
+
region ? locator.host_for_region(service, region) : locator.primary_host(service)
|
|
75
|
+
end
|
|
21
76
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
77
|
+
# Generic: get port for any service
|
|
78
|
+
def port_for(service)
|
|
79
|
+
locator.port_for(service)
|
|
80
|
+
end
|
|
26
81
|
end
|
|
27
82
|
end
|
|
28
83
|
require 'stringio'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: boring_services
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- BoringCache
|
|
@@ -127,6 +127,7 @@ files:
|
|
|
127
127
|
- lib/boring_services/installer.rb
|
|
128
128
|
- lib/boring_services/railtie.rb
|
|
129
129
|
- lib/boring_services/secrets.rb
|
|
130
|
+
- lib/boring_services/service_locator.rb
|
|
130
131
|
- lib/boring_services/services/base.rb
|
|
131
132
|
- lib/boring_services/services/haproxy.rb
|
|
132
133
|
- lib/boring_services/services/memcached.rb
|
|
@@ -158,7 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
158
159
|
- !ruby/object:Gem::Version
|
|
159
160
|
version: '0'
|
|
160
161
|
requirements: []
|
|
161
|
-
rubygems_version: 3.
|
|
162
|
+
rubygems_version: 3.7.2
|
|
162
163
|
specification_version: 4
|
|
163
164
|
summary: Deploy infrastructure services for Ruby & Rails apps
|
|
164
165
|
test_files: []
|