Class: Hetzner::Bootstrap::Target

Inherits:
Object
  • Object
show all
Defined in:
lib/hetzner/bootstrap/target.rb

Defined Under Namespace

Classes: CantActivateRescueSystemError, CantResetSystemError, InstallationError, NoTemplateProvidedError

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Target

Returns a new instance of Target.



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/hetzner/bootstrap/target.rb', line 25

def initialize(options = {})
  @rescue_os           = 'linux'
  @rescue_os_bit       = '64'
  @retries             = 0
  @bootstrap_cmd       = 'export TERM=xterm; /root/.oldroot/nfs/install/installimage -a -c /tmp/template'
  @login               = 'root'
  @post_install_remote = ''

  @template = Template.new options.delete(:template)

  raise NoTemplateProvidedError 'No imageinstall template provided.' unless @template

  options.each_pair do |k, v|
    send("#{k}=", v)
  end
end

Instance Attribute Details

#actionsObject

Returns the value of attribute actions.



17
18
19
# File 'lib/hetzner/bootstrap/target.rb', line 17

def actions
  @actions
end

#bootstrap_cmdObject

Returns the value of attribute bootstrap_cmd.



22
23
24
# File 'lib/hetzner/bootstrap/target.rb', line 22

def bootstrap_cmd
  @bootstrap_cmd
end

#hostnameObject

Returns the value of attribute hostname.



18
19
20
# File 'lib/hetzner/bootstrap/target.rb', line 18

def hostname
  @hostname
end

#ipObject

Returns the value of attribute ip.



11
12
13
# File 'lib/hetzner/bootstrap/target.rb', line 11

def ip
  @ip
end

#loggerObject

Returns the value of attribute logger.



23
24
25
# File 'lib/hetzner/bootstrap/target.rb', line 23

def logger
  @logger
end

#loginObject

Returns the value of attribute login.



12
13
14
# File 'lib/hetzner/bootstrap/target.rb', line 12

def 
  @login
end

#passwordObject

Returns the value of attribute password.



13
14
15
# File 'lib/hetzner/bootstrap/target.rb', line 13

def password
  @password
end

#post_installObject



162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/hetzner/bootstrap/target.rb', line 162

def post_install
  return unless @post_install

  post_install = render_post_install
  logger.info "executing post_install:\n #{post_install}"

  output = local do
    `#{post_install}`
  end

  logger.info output
end

#post_install_remoteObject



175
176
177
178
179
180
181
182
183
184
185
# File 'lib/hetzner/bootstrap/target.rb', line 175

def post_install_remote
  remote do |ssh|
    @post_install_remote.split("\n").each do |cmd|
      cmd.chomp!
      logger.info "executing #{cmd}"
      ssh.exec!(cmd)
    end
  end
rescue IOError, Net::SSH::Disconnect
  logger.debug 'SSH connection was closed.'
end

#public_keysObject

Returns the value of attribute public_keys.



21
22
23
# File 'lib/hetzner/bootstrap/target.rb', line 21

def public_keys
  @public_keys
end

#rescue_osObject

Returns the value of attribute rescue_os.



15
16
17
# File 'lib/hetzner/bootstrap/target.rb', line 15

def rescue_os
  @rescue_os
end

#rescue_os_bitObject

Returns the value of attribute rescue_os_bit.



16
17
18
# File 'lib/hetzner/bootstrap/target.rb', line 16

def rescue_os_bit
  @rescue_os_bit
end

#templateObject

Returns the value of attribute template.



14
15
16
# File 'lib/hetzner/bootstrap/target.rb', line 14

def template
  @template
end

Instance Method Details

#copy_ssh_keysObject



141
142
143
144
145
146
147
148
149
150
151
# File 'lib/hetzner/bootstrap/target.rb', line 141

def copy_ssh_keys
  return unless @public_keys

  remote do |ssh|
    ssh.exec!('mkdir /root/.ssh')
    Array(@public_keys).each do |key|
      pub = File.read(File.expand_path(key))
      ssh.exec!("echo \"#{pub}\" >> /root/.ssh/authorized_keys")
    end
  end
end

#default_log_formatterObject



240
241
242
243
244
245
246
# File 'lib/hetzner/bootstrap/target.rb', line 240

def default_log_formatter
  proc do |_severity, datetime, _progname, msg|
    caller(5..5).first =~ /`(.*?)'/
    "[#{datetime.strftime '%H:%M:%S'}][#{format '%-15s', ip}]" \
    "[#{Regexp.last_match(1)}] #{msg}\n"
  end
end

#enable_rescue_mode(options = {}) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/hetzner/bootstrap/target.rb', line 42

def enable_rescue_mode(options = {})
  result = @api.enable_rescue! @ip, @rescue_os, @rescue_os_bit

  if result.success? && result['rescue']
    @password = result['rescue']['password']
    reset_retries
    logger.info "IP: #{ip} => password: #{@password}"
  elsif @retries > 3
    logger.error 'rescue system could not be activated'
    raise CantActivateRescueSystemError, result
  else
    @retries += 1

    logger.warn "problem while trying to activate rescue system (retries: #{@retries})"
    @api.disable_rescue! @ip

    rolling_sleep
    enable_rescue_mode options
  end
end

#installimageObject



114
115
116
117
118
119
120
121
122
123
# File 'lib/hetzner/bootstrap/target.rb', line 114

def installimage
  template = render_template

  remote do |ssh|
    ssh.exec! "echo \"#{template}\" > /tmp/template"
    logger.info "remote executing: #{@bootstrap_cmd}"
    output = ssh.exec!(@bootstrap_cmd)
    logger.info output.gsub(`clear`, '')
  end
end

#localObject



227
228
229
# File 'lib/hetzner/bootstrap/target.rb', line 227

def local
  yield
end

#port_open?(ip, port) ⇒ Boolean

Returns:

  • (Boolean)


79
80
81
82
83
84
# File 'lib/hetzner/bootstrap/target.rb', line 79

def port_open?(ip, port)
  ssh_port_probe = TCPSocket.new ip, port
  IO.select([ssh_port_probe], nil, nil, 2)
  ssh_port_probe.close
  true
end

#rebootObject



125
126
127
128
129
130
131
# File 'lib/hetzner/bootstrap/target.rb', line 125

def reboot
  remote do |ssh|
    ssh.exec!('reboot')
  end
rescue IOError, Net::SSH::Disconnect
  logger.debug 'SSH connection was closed as anticipated.'
end

#remote(options = {}) ⇒ Object



218
219
220
221
222
223
224
225
# File 'lib/hetzner/bootstrap/target.rb', line 218

def remote(options = {})
  default = { verify_host_key: :never, password: @password }
  default.merge! options

  Net::SSH.start(@ip, @login, default) do |ssh|
    yield ssh
  end
end

#render_post_installObject



197
198
199
200
201
202
203
204
205
206
207
# File 'lib/hetzner/bootstrap/target.rb', line 197

def render_post_install
  eruby = Erubis::Eruby.new @post_install.to_s

  params = {}
  params[:hostname] = @hostname
  params[:ip]       = @ip
  params[:login]    = @login
  params[:password] = @password

  eruby.result(params)
end

#render_templateObject



187
188
189
190
191
192
193
194
195
# File 'lib/hetzner/bootstrap/target.rb', line 187

def render_template
  eruby = Erubis::Eruby.new @template.to_s

  params = {}
  params[:hostname] = @hostname
  params[:ip]       = @ip

  eruby.result(params)
end

#reset(options = {}) ⇒ Object



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/hetzner/bootstrap/target.rb', line 63

def reset(options = {})
  result = @api.reset! @ip, :hw

  if result.success?
    reset_retries
  elsif @retries > 3
    logger.error 'resetting through webservice failed.'
    raise CantResetSystemError, result
  else
    @retries += 1
    logger.warn "problem while trying to reset/reboot system (retries: #{@retries})"
    rolling_sleep
    reset options
  end
end

#reset_retriesObject



231
232
233
# File 'lib/hetzner/bootstrap/target.rb', line 231

def reset_retries
  @retries = 0
end

#rolling_sleepObject



235
236
237
238
# File 'lib/hetzner/bootstrap/target.rb', line 235

def rolling_sleep
  # => 1, 4, 13, 28, 49, 76, 109, 148, 193, 244, 301, 364 ... seconds
  sleep @retries * @retries * 3 + 1
end

#update_local_known_hostsObject



153
154
155
156
157
158
159
160
# File 'lib/hetzner/bootstrap/target.rb', line 153

def update_local_known_hosts
  remote(verify_host_key: :accept_new_or_local_tunnel) do |ssh|
    # dummy
  end
rescue Net::SSH::HostKeyMismatch => e
  e.remember_host!
  logger.info 'remote host key added to local ~/.ssh/known_hosts file.'
end

#use_api(api_obj) ⇒ Object



209
210
211
# File 'lib/hetzner/bootstrap/target.rb', line 209

def use_api(api_obj)
  @api = api_obj
end

#use_logger(logger_obj) ⇒ Object



213
214
215
216
# File 'lib/hetzner/bootstrap/target.rb', line 213

def use_logger(logger_obj)
  @logger = logger_obj
  @logger.formatter = default_log_formatter
end

#verify_installationObject



133
134
135
136
137
138
139
# File 'lib/hetzner/bootstrap/target.rb', line 133

def verify_installation
  remote do |ssh|
    working_hostname = ssh.exec!('cat /etc/hostname')
    working_hostname.chomp!
    logger.debug "hostnames do not match: assumed #{@hostname} but received #{working_hostname}" unless @hostname == working_hostname.chomp
  end
end

#wait_for_ssh_downObject



86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/hetzner/bootstrap/target.rb', line 86

def wait_for_ssh_down
  loop do
    sleep 2
    Timeout.timeout(4) do
      raise Errno::ECONNREFUSED unless port_open? @ip, 22

      logger.debug 'SSH UP'
    end
  end
rescue Timeout::Error, Errno::ECONNREFUSED
  logger.debug 'SSH DOWN'
end

#wait_for_ssh_upObject



99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/hetzner/bootstrap/target.rb', line 99

def wait_for_ssh_up
  loop do
    Timeout.timeout(4) do
      raise Errno::ECONNREFUSED unless port_open? @ip, 22

      logger.debug 'SSH UP'
      return true
    end
  end
rescue Errno::ECONNREFUSED, Timeout::Error
  logger.debug 'SSH DOWN'
  sleep 2
  retry
end