hetzner-bootstrap 1.0.2 → 2.0.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.
@@ -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