boring_services 0.2.0 → 0.3.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/services/memcached.rb +1 -1
- data/lib/boring_services/services/redis.rb +5 -0
- data/lib/boring_services/ssh_executor.rb +43 -46
- data/lib/boring_services/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e386da541954a876687c37adade51199a571e5b75bc5583946a676a42a69e5e0
|
|
4
|
+
data.tar.gz: bb16f6517193ad18614eb91632c462e031856d06ac9d73abd25675698df50f43
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 87986c7987ff4b9ee2490fe4ca8035ad536a90b571d2d9cdcf0d995c93a4e8d7e8c2836a2c4fa99fcf2137c1096aad91c166b300b1ff4a88df817220e29aaa51
|
|
7
|
+
data.tar.gz: ae84eb250c619ceb130e90db62fb4538939ee22755700e42231164f3c012ab67df397a5ad6f927534862827b346cf00f82b563b154a97ea698835f798275e8cc
|
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
|
|
@@ -37,6 +37,7 @@ module BoringServices
|
|
|
37
37
|
config_content = <<~REDIS
|
|
38
38
|
bind 0.0.0.0
|
|
39
39
|
port #{listen_port}
|
|
40
|
+
dir /var/lib/redis
|
|
40
41
|
maxmemory #{memory}mb
|
|
41
42
|
maxmemory-policy allkeys-lru
|
|
42
43
|
appendonly yes
|
|
@@ -49,6 +50,10 @@ module BoringServices
|
|
|
49
50
|
execute :sudo, :mv, '/tmp/redis.conf', '/etc/redis/redis.conf'
|
|
50
51
|
execute :sudo, :chown, 'redis:redis', '/etc/redis/redis.conf'
|
|
51
52
|
execute :sudo, :chmod, '640', '/etc/redis/redis.conf'
|
|
53
|
+
|
|
54
|
+
# Set vm.overcommit_memory for Redis background saves
|
|
55
|
+
execute :sudo, :sysctl, '-w', 'vm.overcommit_memory=1'
|
|
56
|
+
execute "echo 'vm.overcommit_memory = 1' | sudo tee -a /etc/sysctl.conf > /dev/null || true"
|
|
52
57
|
end
|
|
53
58
|
end
|
|
54
59
|
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
|
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.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- BoringCache
|
|
@@ -158,7 +158,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
158
158
|
- !ruby/object:Gem::Version
|
|
159
159
|
version: '0'
|
|
160
160
|
requirements: []
|
|
161
|
-
rubygems_version: 3.
|
|
161
|
+
rubygems_version: 3.7.2
|
|
162
162
|
specification_version: 4
|
|
163
163
|
summary: Deploy infrastructure services for Ruby & Rails apps
|
|
164
164
|
test_files: []
|