hetzner-bootstrap 1.1.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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