hetzner-bootstrap 1.0.2 → 2.0.0

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