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.
@@ -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(enable_rescue_mode
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(STDOUT)
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
- @targets << param
38
- else
39
- @targets << (Hetzner::Bootstrap::Target.new param)
40
- end
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!(options = {})
46
+ def bootstrap!(_options = {})
48
47
  @targets.each do |target|
49
- #fork do
50
- target.use_api @api
51
- target.use_logger @logger
52
- bootstrap_one_target! target
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, index|
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}] #{sprintf "%-20s", "START"}"
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 #{sprintf "%.5f",d} seconds"
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
- attr_accessor :login
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 = 'linux'
25
- @rescue_os_bit = '64'
26
- @retries = 0
27
- @bootstrap_cmd = 'export TERM=xterm; /root/.oldroot/nfs/install/installimage -a -c /tmp/template'
28
- @login = 'root'
29
-
30
- if tmpl = options.delete(:template)
31
- @template = Template.new tmpl
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
- options.each_pair do |k,v|
37
- self.send("#{k}=", v)
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 "rescue system could not be activated"
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 "resetting through webservice failed."
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? ip, port
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(options = {})
75
+ def wait_for_ssh_down
86
76
  loop do
87
77
  sleep 2
88
- Timeout::timeout(4) do
89
- if port_open? @ip, 22
90
- logger.debug "SSH UP"
91
- else
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 "SSH DOWN"
85
+ logger.debug 'SSH DOWN'
98
86
  end
99
87
 
100
- def wait_for_ssh_up(options = {})
88
+ def wait_for_ssh_up
101
89
  loop do
102
- Timeout::timeout(4) do
103
- if port_open? @ip, 22
104
- logger.debug "SSH UP"
105
- return true
106
- else
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 "SSH DOWN"
98
+ logger.debug 'SSH DOWN'
113
99
  sleep 2
114
100
  retry
115
101
  end
116
102
 
117
- def installimage(options = {})
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(options = {})
114
+ def reboot
129
115
  remote do |ssh|
130
- ssh.exec!("reboot")
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(options = {})
122
+ def verify_installation
135
123
  remote do |ssh|
136
- working_hostname = ssh.exec!("cat /etc/hostname")
137
- unless @hostname == working_hostname.chomp
138
- raise InstallationError, "hostnames do not match: assumed #{@hostname} but received #{working_hostname}"
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(options = {})
144
- if @public_keys
145
- remote do |ssh|
146
- ssh.exec!("mkdir /root/.ssh")
147
- Array(@public_keys).each do |key|
148
- pub = File.read(File.expand_path(key))
149
- ssh.exec!("echo \"#{pub}\" >> /root/.ssh/authorized_keys")
150
- end
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(options = {})
156
- remote(:paranoid => true) do |ssh|
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 "remote host key added to local ~/.ssh/known_hosts file."
148
+ logger.info 'remote host key added to local ~/.ssh/known_hosts file.'
162
149
  end
163
150
 
164
- def post_install(options = {})
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(options = {})
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
- return eruby.result(params)
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
- return eruby.result(params)
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) do |ssh|
224
- block.call ssh
225
- end
211
+ Net::SSH.start(@ip, @login, default, &block)
226
212
  end
227
213
 
228
- def local(&block)
229
- block.call
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
- sleep @retries * @retries * 3 + 1 # => 1, 4, 13, 28, 49, 76, 109, 148, 193, 244, 301, 364 ... seconds
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
- proc do |severity, datetime, progname, msg|
242
- caller[4]=~/`(.*?)'/
243
- "[#{datetime.strftime "%H:%M:%S"}][#{sprintf "%-15s", ip}][#{$1}] #{msg}\n"
244
- end
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
- if param.is_a? Hetzner::Bootstrap::Template
16
- return param
17
- elsif param.is_a? String
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,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hetzner
2
4
  class Bootstrap
3
- VERSION = '1.1.0'
5
+ VERSION = '2.0.1'
4
6
  end
5
7
  end
@@ -1,51 +1,46 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'hetzner-api'
2
- require 'spec_helper'
4
+ require 'hetzner-bootstrap'
3
5
 
4
- describe "Bootstrap" do
5
- before(:all) do
6
- @api = Hetzner::API.new API_USERNAME, API_PASSWORD
7
- @bootstrap = Hetzner::Bootstrap.new :api => @api
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 "add target" do
11
-
12
- it "should be able to add a server to operate on" do
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
- it "should have the default template if none is specified" do
19
- @bootstrap.add_target proper_target
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 "should raise an NoTemplateProvidedError when no template option provided" do
24
- lambda {
25
- @bootstrap.add_target improper_target_without_template
26
- }.should raise_error(Hetzner::Bootstrap::Target::NoTemplateProvidedError)
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
- return {
33
- :ip => "1.2.3.4",
34
- :login => "root",
35
- # :password => "halloMartin!",
36
- :rescue_os => "linux",
37
- :rescue_os_bit => "64",
38
- :template => default_template
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.select { |k,v| k != :template }
39
+ proper_target.except(:template)
44
40
  end
45
41
 
46
42
  def default_template
47
- "bla"
43
+ 'string'
48
44
  end
49
45
  end
50
-
51
-
46
+ # rubocop:enable Metrics/BlockLength