Skip to content

Commit

Permalink
import
Browse files Browse the repository at this point in the history
  • Loading branch information
rmoriz committed Feb 6, 2011
0 parents commit ab662ae
Show file tree
Hide file tree
Showing 12 changed files with 470 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
@@ -0,0 +1,6 @@
.idea/*
bin/*
*.gem
.bundle
Gemfile.lock
pkg/*
4 changes: 4 additions & 0 deletions Gemfile
@@ -0,0 +1,4 @@
source "http://rubygems.org"

# Specify your gem's dependencies in hetzner.gemspec
gemspec
23 changes: 23 additions & 0 deletions LICENSE
@@ -0,0 +1,23 @@
Copyright (c) 2011 Moriz GmbH, Roland Moriz, http://moriz.de/

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

--
<a href="http://moriz.de/opensource">a Moriz GmbH OpenSource project.</a>
9 changes: 9 additions & 0 deletions README
@@ -0,0 +1,9 @@
hetzner-bootstrap allows you to bootstrap a provisioned EQ Server from hetzner.de

Requirements

- get a webservice login (robots.your-server.de)
- the ip address of the shipped systems

see example.rb for usage

2 changes: 2 additions & 0 deletions Rakefile
@@ -0,0 +1,2 @@
require 'bundler'
Bundler::GemHelper.install_tasks
44 changes: 44 additions & 0 deletions example.rb
@@ -0,0 +1,44 @@
#!/usr/bin/env ruby
require "hetzner-bootstrap"

API_USERNAME="xxx"
API_PASSWORD="yyy"

bs = Hetzner::Bootstrap.new :api => Hetzner::API.new(API_USERNAME, API_PASSWORD)

# 2 disks, software raid 1, etc.
template = <<EOT
DRIVE1 /dev/sda
DRIVE2 /dev/sdb
FORMATDRIVE2 0
SWRAID 1
SWRAIDLEVEL 1
BOOTLOADER grub
HOSTNAME <%= hostname %>
PART /boot ext2 1G
PART lvm host 75G
PART lvm guest all
LV host root / ext3 50G
LV host swap swap swap 5G
IMAGE /root/images/Ubuntu-1010-maverick-64-minimal.tar.gz
EOT

post_install = <<EOT
bundle exec knife bootstrap <%= hostname %> "role[base],role[kvm_host]" -x <%= login %> -P "<%= password %> --sudo -l debug
EOT

# duplicate entry for each system
bs << { :ip => "1.2.3.4",
:template => template, # string will be parsed by erubis
:hostname => 'server100.example.com', # will be used for setting the systems' hostname
:public_keys => "~/.ssh/id_dsa.pub", # will be copied over to the freshly bootstrapped system
:post_install => post_install } # will be called locally at the end and can be used e.g. to run a chef bootstrap

bs.bootstrap!

28 changes: 28 additions & 0 deletions hetzner-bootstrap.gemspec
@@ -0,0 +1,28 @@
# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "hetzner/bootstrap/version"

Gem::Specification.new do |s|
s.name = "hetzner"
s.version = Hetzner::Bootstrap::VERSION
#s.version = '0.0.1'
s.platform = Gem::Platform::RUBY
s.authors = ["TODO: Write your name"]
s.email = ["TODO: Write your email address"]
s.homepage = ""
s.summary = %q{TODO: Write a gem summary}
s.description = %q{TODO: Write a gem description}

s.rubyforge_project = "hetzner"

s.add_dependency 'hetzner-api'
s.add_dependency 'net-ssh'
s.add_dependency 'erubis'

s.add_development_dependency "rspec", ">= 2.4.0"

s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]
end
86 changes: 86 additions & 0 deletions lib/hetzner-bootstrap.rb
@@ -0,0 +1,86 @@
require 'benchmark'

require 'hetzner-api'
require 'hetzner/bootstrap/version'
require 'hetzner/bootstrap/target'
require 'hetzner/bootstrap/template'

module Hetzner
class Bootstrap
attr_accessor :targets
attr_accessor :api
attr_accessor :use_threads
attr_accessor :actions

def initialize(options = {})
@targets = []
@actions = %w(enable_rescue_mode
reset
wait_for_ssh
installimage
wait_for_ssh
verify_installation
copy_ssh_keys
post_install)
@api = options[:api]
@use_threads = options[:use_threads] || true
end

def add_target(param)
if param.is_a? Hetzner::Bootstrap::Target
@targets << param
else
@targets << (Hetzner::Bootstrap::Target.new param)
end
end

def <<(param)
add_target param
end

def bootstrap!(options = {})
threads = []

@targets.each do |target|
target.use_api @api

if uses_threads?
threads << Thread.new do
bootstrap_one_target! target
end
else
bootstrap_one_target! target
end
end

finalize_threads(threads) if uses_threads?
end

def bootstrap_one_target!(target)
actions = (target.actions || @actions)
actions.each_with_index do |action, index|

log target.ip, action, index, 'START'
d = Benchmark.realtime do
target.send action
end

log target.ip, action, index, "FINISHED in #{sprintf "%.5f",d} seconds"
end
rescue => e
puts "something bad happend: #{e.class} #{e.message}"
end

def uses_threads?
@use_threads
end

def finalize_threads(threads)
threads.each { |t| t.join }
end

def log(where, what, index, message)
puts "[#{where}] #{what} #{' ' * (index * 4)}#{message}"
end
end
end
185 changes: 185 additions & 0 deletions lib/hetzner/bootstrap/target.rb
@@ -0,0 +1,185 @@
require 'erubis'
require 'net/ssh'
require 'socket'

module Hetzner
class Bootstrap
class Target
attr_accessor :ip
attr_accessor :login
attr_accessor :password
attr_accessor :template
attr_accessor :rescue_os
attr_accessor :rescue_os_bit
attr_accessor :actions
attr_accessor :hostname
attr_accessor :post_install
attr_accessor :public_keys
attr_accessor :bootstrap_cmd

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

if tmpl = options.delete(:template)
@template = Template.new tmpl
else
raise NoTemplateProvidedError.new 'No imageinstall template provided.'
end

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

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

if result.success? && result['rescue']
@login = 'root'
@password = result['rescue']['password']
reset_retries
puts "IP: #{ip} => password: #{@password}"
elsif @retries > 3
raise CantActivateRescueSystemError, result
else
@retries += 1

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

sleep @retries * 5 # => 5, 10, 15s
enable_rescue_mode options
end
end

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

if result.success?
reset_retries
sleep 10
elsif @retries > 3
raise CantResetSystemError, result
else
@retries += 1
rolling_sleep
puts "problem while trying to reset/reboot system (retries: #{@retries})"
reset options
end
end

def wait_for_ssh(options = {})
ssh_port_probe = TCPSocket.new @ip, 22
return if IO.select([ssh_port_probe], nil, nil, 5)

rescue Errno::ECONNREFUSED
@retries += 1
print "."
STDOUT.flush

if @retries > 20
raise CantSshAfterResetError
else
rolling_sleep
wait_for_ssh options
end
rescue => e
puts "Exception: #{e.class} #{e.message}"
ensure
puts ""
ssh_port_probe && ssh_port_probe.close
end

def installimage(options = {})
template = render_template

Net::SSH.start(@ip, @login, :password => @password) do |ssh|
ssh.exec!("echo \"#{template}\" > /tmp/template")
puts "remote executing: #{@bootstrap_cmd}"
output = ssh.exec!(@bootstrap_cmd)
puts output
ssh.exec!("reboot")
sleep 4
end
rescue Net::SSH::HostKeyMismatch => e
e.remember_host!
retry
end

def verify_installation(options = {})
Net::SSH.start(@ip, @login, :password => @password) do |ssh|
working_hostname = ssh.exec!("cat /etc/hostname")
unless @hostname == working_hostname.chomp
raise InstallationError, "hostnames do not match: assumed #{@hostname} but received #{working_hostname}"
end
end
rescue Net::SSH::HostKeyMismatch => e
e.remember_host!
retry
end

def copy_ssh_keys(options = {})
if @public_keys
Net::SSH.start(@ip, @login, :password => @password) do |ssh|
ssh.exec!("mkdir /root/.ssh")
@public_keys.to_a.each do |key|
pub = File.read(File.expand_path(key))
ssh.exec!("echo \"#{pub}\" >> /root/.ssh/authorized_keys")
end
end
end
rescue Net::SSH::HostKeyMismatch => e
e.remember_host!
retry
end

def post_install(options = {})
return unless @post_install
post_install = render_post_install
`post_install`
end

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

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

return eruby.result(params)
end

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

return eruby.result(params)
end

def use_api(api)
@api = api
end

def reset_retries
@retries = 0
end

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

class NoTemplateProvidedError < ArgumentError; end
class CantActivateRescueSystemError < StandardError; end
class CantResetSystemError < StandardError; end
class InstallationError < StandardError; end
end
end
end

0 comments on commit ab662ae

Please sign in to comment.