hetzner-bootstrap 1.1.0 → 2.0.1
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/.rspec +2 -0
- data/.rubocop.yml +21 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -1
- data/LICENSE +1 -1
- data/README.md +116 -10
- data/Rakefile +2 -0
- data/example.rb +95 -96
- data/hetzner-bootstrap.gemspec +23 -15
- data/lib/hetzner-bootstrap.rb +22 -25
- data/lib/hetzner/bootstrap/target.rb +73 -82
- data/lib/hetzner/bootstrap/template.rb +7 -4
- data/lib/hetzner/bootstrap/version.rb +3 -1
- data/spec/hetzner_bootstrap_spec.rb +26 -31
- data/spec/spec_helper.rb +99 -0
- metadata +132 -45
data/lib/hetzner-bootstrap.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'benchmark'
|
2
4
|
require 'logger'
|
3
5
|
|
@@ -8,14 +10,11 @@ require 'hetzner/bootstrap/template'
|
|
8
10
|
|
9
11
|
module Hetzner
|
10
12
|
class Bootstrap
|
11
|
-
attr_accessor :targets
|
12
|
-
attr_accessor :api
|
13
|
-
attr_accessor :actions
|
14
|
-
attr_accessor :logger
|
13
|
+
attr_accessor :targets, :api, :actions, :logger
|
15
14
|
|
16
15
|
def initialize(options = {})
|
17
16
|
@targets = []
|
18
|
-
@actions = %w
|
17
|
+
@actions = %w[enable_rescue_mode
|
19
18
|
reset
|
20
19
|
wait_for_ssh_down
|
21
20
|
wait_for_ssh_up
|
@@ -27,48 +26,46 @@ module Hetzner
|
|
27
26
|
copy_ssh_keys
|
28
27
|
update_local_known_hosts
|
29
28
|
post_install
|
30
|
-
post_install_remote
|
29
|
+
post_install_remote]
|
31
30
|
@api = options[:api]
|
32
|
-
@logger = options[:logger] || Logger.new(
|
31
|
+
@logger = options[:logger] || Logger.new($stdout)
|
33
32
|
end
|
34
33
|
|
35
34
|
def add_target(param)
|
36
|
-
if param.is_a? Hetzner::Bootstrap::Target
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
35
|
+
@targets << if param.is_a? Hetzner::Bootstrap::Target
|
36
|
+
param
|
37
|
+
else
|
38
|
+
Hetzner::Bootstrap::Target.new(param)
|
39
|
+
end
|
41
40
|
end
|
42
41
|
|
43
42
|
def <<(param)
|
44
43
|
add_target param
|
45
44
|
end
|
46
45
|
|
47
|
-
def bootstrap!(
|
46
|
+
def bootstrap!(_options = {})
|
48
47
|
@targets.each do |target|
|
49
|
-
#fork do
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
#end
|
48
|
+
# fork do
|
49
|
+
target.use_api @api
|
50
|
+
target.use_logger @logger
|
51
|
+
bootstrap_one_target! target
|
52
|
+
# end
|
54
53
|
end
|
55
|
-
#Process.waitall
|
54
|
+
# Process.waitall
|
56
55
|
end
|
57
56
|
|
58
57
|
def bootstrap_one_target!(target)
|
59
58
|
actions = (target.actions || @actions)
|
60
|
-
actions.each_with_index do |action,
|
61
|
-
|
59
|
+
actions.each_with_index do |action, _index|
|
62
60
|
loghack = "\b" * 24 # remove: "[bootstrap_one_target!] ".length
|
63
|
-
target.logger.info "#{loghack}[#{action}] #{
|
61
|
+
target.logger.info "#{loghack}[#{action}] #{format '%-20s', 'START'}"
|
64
62
|
d = Benchmark.realtime do
|
65
63
|
target.send action
|
66
64
|
end
|
67
|
-
target.logger.info "#{loghack}[#{action}] FINISHED in #{
|
65
|
+
target.logger.info "#{loghack}[#{action}] FINISHED in #{format '%.5f', d} seconds"
|
68
66
|
end
|
69
|
-
rescue => e
|
67
|
+
rescue StandardError => e
|
70
68
|
puts "something bad happened unexpectedly: #{e.class} => #{e.message}"
|
71
69
|
end
|
72
70
|
end
|
73
71
|
end
|
74
|
-
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'erubis'
|
2
4
|
require 'net/ssh'
|
3
5
|
require 'socket'
|
@@ -6,35 +8,23 @@ require 'timeout'
|
|
6
8
|
module Hetzner
|
7
9
|
class Bootstrap
|
8
10
|
class Target
|
9
|
-
attr_accessor :ip
|
10
|
-
|
11
|
-
attr_accessor :password
|
12
|
-
attr_accessor :template
|
13
|
-
attr_accessor :rescue_os
|
14
|
-
attr_accessor :rescue_os_bit
|
15
|
-
attr_accessor :actions
|
16
|
-
attr_accessor :hostname
|
17
|
-
attr_accessor :post_install
|
18
|
-
attr_accessor :post_install_remote
|
19
|
-
attr_accessor :public_keys
|
20
|
-
attr_accessor :bootstrap_cmd
|
21
|
-
attr_accessor :logger
|
11
|
+
attr_accessor :ip, :login, :password, :template, :rescue_os, :rescue_os_bit, :actions, :hostname, :public_keys, :bootstrap_cmd, :logger
|
12
|
+
attr_writer :post_install, :post_install_remote
|
22
13
|
|
23
14
|
def initialize(options = {})
|
24
|
-
@rescue_os
|
25
|
-
@rescue_os_bit
|
26
|
-
@retries
|
27
|
-
@bootstrap_cmd
|
28
|
-
@login
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
else
|
33
|
-
raise NoTemplateProvidedError.new 'No imageinstall template provided.'
|
34
|
-
end
|
15
|
+
@rescue_os = 'linux'
|
16
|
+
@rescue_os_bit = '64'
|
17
|
+
@retries = 0
|
18
|
+
@bootstrap_cmd = 'export TERM=xterm; /root/.oldroot/nfs/install/installimage -a -c /tmp/template'
|
19
|
+
@login = 'root'
|
20
|
+
@post_install_remote = ''
|
21
|
+
|
22
|
+
@template = Template.new options.delete(:template)
|
35
23
|
|
36
|
-
|
37
|
-
|
24
|
+
raise NoTemplateProvidedError 'No imageinstall template provided.' unless @template
|
25
|
+
|
26
|
+
options.each_pair do |k, v|
|
27
|
+
send("#{k}=", v)
|
38
28
|
end
|
39
29
|
end
|
40
30
|
|
@@ -46,7 +36,7 @@ module Hetzner
|
|
46
36
|
reset_retries
|
47
37
|
logger.info "IP: #{ip} => password: #{@password}"
|
48
38
|
elsif @retries > 3
|
49
|
-
logger.error
|
39
|
+
logger.error 'rescue system could not be activated'
|
50
40
|
raise CantActivateRescueSystemError, result
|
51
41
|
else
|
52
42
|
@retries += 1
|
@@ -65,7 +55,7 @@ module Hetzner
|
|
65
55
|
if result.success?
|
66
56
|
reset_retries
|
67
57
|
elsif @retries > 3
|
68
|
-
logger.error
|
58
|
+
logger.error 'resetting through webservice failed.'
|
69
59
|
raise CantResetSystemError, result
|
70
60
|
else
|
71
61
|
@retries += 1
|
@@ -75,93 +65,90 @@ module Hetzner
|
|
75
65
|
end
|
76
66
|
end
|
77
67
|
|
78
|
-
def port_open?
|
68
|
+
def port_open?(ip, port)
|
79
69
|
ssh_port_probe = TCPSocket.new ip, port
|
80
70
|
IO.select([ssh_port_probe], nil, nil, 2)
|
81
71
|
ssh_port_probe.close
|
82
72
|
true
|
83
73
|
end
|
84
74
|
|
85
|
-
def wait_for_ssh_down
|
75
|
+
def wait_for_ssh_down
|
86
76
|
loop do
|
87
77
|
sleep 2
|
88
|
-
Timeout
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
raise Errno::ECONNREFUSED
|
93
|
-
end
|
78
|
+
Timeout.timeout(4) do
|
79
|
+
raise Errno::ECONNREFUSED unless port_open? @ip, 22
|
80
|
+
|
81
|
+
logger.debug 'SSH UP'
|
94
82
|
end
|
95
83
|
end
|
96
84
|
rescue Timeout::Error, Errno::ECONNREFUSED
|
97
|
-
logger.debug
|
85
|
+
logger.debug 'SSH DOWN'
|
98
86
|
end
|
99
87
|
|
100
|
-
def wait_for_ssh_up
|
88
|
+
def wait_for_ssh_up
|
101
89
|
loop do
|
102
|
-
Timeout
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
raise Errno::ECONNREFUSED
|
108
|
-
end
|
90
|
+
Timeout.timeout(4) do
|
91
|
+
raise Errno::ECONNREFUSED unless port_open? @ip, 22
|
92
|
+
|
93
|
+
logger.debug 'SSH UP'
|
94
|
+
return true
|
109
95
|
end
|
110
96
|
end
|
111
97
|
rescue Errno::ECONNREFUSED, Timeout::Error
|
112
|
-
logger.debug
|
98
|
+
logger.debug 'SSH DOWN'
|
113
99
|
sleep 2
|
114
100
|
retry
|
115
101
|
end
|
116
102
|
|
117
|
-
def installimage
|
103
|
+
def installimage
|
118
104
|
template = render_template
|
119
105
|
|
120
106
|
remote do |ssh|
|
121
107
|
ssh.exec! "echo \"#{template}\" > /tmp/template"
|
122
108
|
logger.info "remote executing: #{@bootstrap_cmd}"
|
123
109
|
output = ssh.exec!(@bootstrap_cmd)
|
124
|
-
logger.info output
|
110
|
+
logger.info output.gsub(`clear`, '')
|
125
111
|
end
|
126
112
|
end
|
127
113
|
|
128
|
-
def reboot
|
114
|
+
def reboot
|
129
115
|
remote do |ssh|
|
130
|
-
ssh.exec!(
|
116
|
+
ssh.exec!('reboot')
|
131
117
|
end
|
118
|
+
rescue IOError, Net::SSH::Disconnect
|
119
|
+
logger.debug 'SSH connection was closed as anticipated.'
|
132
120
|
end
|
133
121
|
|
134
|
-
def verify_installation
|
122
|
+
def verify_installation
|
135
123
|
remote do |ssh|
|
136
|
-
working_hostname = ssh.exec!(
|
137
|
-
|
138
|
-
|
139
|
-
end
|
124
|
+
working_hostname = ssh.exec!('cat /etc/hostname')
|
125
|
+
working_hostname.chomp!
|
126
|
+
logger.debug "hostnames do not match: assumed #{@hostname} but received #{working_hostname}" unless @hostname == working_hostname.chomp
|
140
127
|
end
|
141
128
|
end
|
142
129
|
|
143
|
-
def copy_ssh_keys
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
130
|
+
def copy_ssh_keys
|
131
|
+
return unless @public_keys
|
132
|
+
|
133
|
+
remote do |ssh|
|
134
|
+
ssh.exec!('mkdir /root/.ssh')
|
135
|
+
Array(@public_keys).each do |key|
|
136
|
+
pub = File.read(File.expand_path(key))
|
137
|
+
ssh.exec!("echo \"#{pub}\" >> /root/.ssh/authorized_keys")
|
151
138
|
end
|
152
139
|
end
|
153
140
|
end
|
154
141
|
|
155
|
-
def update_local_known_hosts
|
156
|
-
remote(:
|
142
|
+
def update_local_known_hosts
|
143
|
+
remote(verify_host_key: :accept_new_or_local_tunnel) do |ssh|
|
157
144
|
# dummy
|
158
145
|
end
|
159
146
|
rescue Net::SSH::HostKeyMismatch => e
|
160
147
|
e.remember_host!
|
161
|
-
logger.info
|
148
|
+
logger.info 'remote host key added to local ~/.ssh/known_hosts file.'
|
162
149
|
end
|
163
150
|
|
164
|
-
def post_install
|
151
|
+
def post_install
|
165
152
|
return unless @post_install
|
166
153
|
|
167
154
|
post_install = render_post_install
|
@@ -174,7 +161,7 @@ module Hetzner
|
|
174
161
|
logger.info output
|
175
162
|
end
|
176
163
|
|
177
|
-
def post_install_remote
|
164
|
+
def post_install_remote
|
178
165
|
remote do |ssh|
|
179
166
|
@post_install_remote.split("\n").each do |cmd|
|
180
167
|
cmd.chomp!
|
@@ -182,6 +169,8 @@ module Hetzner
|
|
182
169
|
ssh.exec!(cmd)
|
183
170
|
end
|
184
171
|
end
|
172
|
+
rescue IOError, Net::SSH::Disconnect
|
173
|
+
logger.debug 'SSH connection was closed.'
|
185
174
|
end
|
186
175
|
|
187
176
|
def render_template
|
@@ -191,7 +180,7 @@ module Hetzner
|
|
191
180
|
params[:hostname] = @hostname
|
192
181
|
params[:ip] = @ip
|
193
182
|
|
194
|
-
|
183
|
+
eruby.result(params)
|
195
184
|
end
|
196
185
|
|
197
186
|
def render_post_install
|
@@ -203,7 +192,7 @@ module Hetzner
|
|
203
192
|
params[:login] = @login
|
204
193
|
params[:password] = @password
|
205
194
|
|
206
|
-
|
195
|
+
eruby.result(params)
|
207
196
|
end
|
208
197
|
|
209
198
|
def use_api(api_obj)
|
@@ -216,17 +205,14 @@ module Hetzner
|
|
216
205
|
end
|
217
206
|
|
218
207
|
def remote(options = {}, &block)
|
219
|
-
|
220
|
-
default = { :paranoid => false, :password => @password }
|
208
|
+
default = { verify_host_key: :never, password: @password }
|
221
209
|
default.merge! options
|
222
210
|
|
223
|
-
Net::SSH.start(@ip, @login, default)
|
224
|
-
block.call ssh
|
225
|
-
end
|
211
|
+
Net::SSH.start(@ip, @login, default, &block)
|
226
212
|
end
|
227
213
|
|
228
|
-
def local
|
229
|
-
|
214
|
+
def local
|
215
|
+
yield
|
230
216
|
end
|
231
217
|
|
232
218
|
def reset_retries
|
@@ -234,19 +220,24 @@ module Hetzner
|
|
234
220
|
end
|
235
221
|
|
236
222
|
def rolling_sleep
|
237
|
-
|
223
|
+
# => 1, 4, 13, 28, 49, 76, 109, 148, 193, 244, 301, 364 ... seconds
|
224
|
+
sleep @retries * @retries * 3 + 1
|
238
225
|
end
|
239
226
|
|
240
227
|
def default_log_formatter
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
228
|
+
proc do |_severity, datetime, _progname, msg|
|
229
|
+
caller(5..5).first =~ /`(.*?)'/
|
230
|
+
"[#{datetime.strftime '%H:%M:%S'}][#{format '%-15s', ip}]" \
|
231
|
+
"[#{Regexp.last_match(1)}] #{msg}\n"
|
232
|
+
end
|
245
233
|
end
|
246
234
|
|
247
235
|
class NoTemplateProvidedError < ArgumentError; end
|
236
|
+
|
248
237
|
class CantActivateRescueSystemError < StandardError; end
|
238
|
+
|
249
239
|
class CantResetSystemError < StandardError; end
|
240
|
+
|
250
241
|
class InstallationError < StandardError; end
|
251
242
|
end
|
252
243
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Hetzner
|
2
4
|
class Bootstrap
|
3
5
|
class Template
|
@@ -12,9 +14,10 @@ module Hetzner
|
|
12
14
|
#
|
13
15
|
# also run: $ installimage -h
|
14
16
|
#
|
15
|
-
|
16
|
-
|
17
|
-
|
17
|
+
case param
|
18
|
+
when Hetzner::Bootstrap::Template
|
19
|
+
param
|
20
|
+
when String
|
18
21
|
@raw_template = param
|
19
22
|
end
|
20
23
|
end
|
@@ -24,4 +27,4 @@ module Hetzner
|
|
24
27
|
end
|
25
28
|
end
|
26
29
|
end
|
27
|
-
end
|
30
|
+
end
|
@@ -1,51 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'hetzner-api'
|
2
|
-
require '
|
4
|
+
require 'hetzner-bootstrap'
|
3
5
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
6
|
+
# rubocop:disable Metrics/BlockLength
|
7
|
+
describe 'Bootstrap' do
|
8
|
+
let(:bs) do
|
9
|
+
Hetzner::Bootstrap.new(api: Hetzner::API.new(API_USERNAME, API_PASSWORD))
|
8
10
|
end
|
9
11
|
|
10
|
-
context
|
11
|
-
|
12
|
-
|
13
|
-
@bootstrap.add_target proper_target
|
14
|
-
@bootstrap.targets.should have(1).target
|
15
|
-
@bootstrap.targets.first.should be_instance_of Hetzner::Bootstrap::Target
|
16
|
-
end
|
12
|
+
context 'add target' do
|
13
|
+
it 'should be able to add a server to operate on' do
|
14
|
+
bs.add_target proper_target
|
17
15
|
|
18
|
-
|
19
|
-
|
20
|
-
@bootstrap.targets.first.template.should be_instance_of Hetzner::Bootstrap::Template
|
16
|
+
expect(bs.targets.size).to be_eql(1)
|
17
|
+
expect(bs.targets.first).to be_instance_of(Hetzner::Bootstrap::Target)
|
21
18
|
end
|
22
19
|
|
23
|
-
it
|
24
|
-
|
25
|
-
|
26
|
-
|
20
|
+
it 'should have the default template if none is specified' do
|
21
|
+
bs.add_target proper_target
|
22
|
+
|
23
|
+
expect(bs.targets.first.template).to be_instance_of Hetzner::Bootstrap::Template
|
27
24
|
end
|
28
|
-
|
29
25
|
end
|
30
26
|
|
31
27
|
def proper_target
|
32
|
-
|
33
|
-
:
|
34
|
-
:
|
35
|
-
# :password => "halloMartin!",
|
36
|
-
:
|
37
|
-
:
|
38
|
-
:
|
28
|
+
{
|
29
|
+
ip: '1.2.3.4',
|
30
|
+
login: 'root',
|
31
|
+
# :password => "halloMartin!",
|
32
|
+
rescue_os: 'linux',
|
33
|
+
rescue_os_bit: '64',
|
34
|
+
template: default_template
|
39
35
|
}
|
40
36
|
end
|
41
37
|
|
42
38
|
def improper_target_without_template
|
43
|
-
proper_target.
|
39
|
+
proper_target.except(:template)
|
44
40
|
end
|
45
41
|
|
46
42
|
def default_template
|
47
|
-
|
43
|
+
'string'
|
48
44
|
end
|
49
45
|
end
|
50
|
-
|
51
|
-
|
46
|
+
# rubocop:enable Metrics/BlockLength
|