Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

import

  • Loading branch information...
commit ab662ae7fc78bfb57087bdb47d3f329e631ec19f 0 parents
@rmoriz authored
6 .gitignore
@@ -0,0 +1,6 @@
+.idea/*
+bin/*
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
4 Gemfile
@@ -0,0 +1,4 @@
+source "http://rubygems.org"
+
+# Specify your gem's dependencies in hetzner.gemspec
+gemspec
23 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 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  Rakefile
@@ -0,0 +1,2 @@
+require 'bundler'
+Bundler::GemHelper.install_tasks
44 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 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 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 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
27 lib/hetzner/bootstrap/template.rb
@@ -0,0 +1,27 @@
+module Hetzner
+ class Bootstrap
+ class Template
+ attr_accessor :raw_template
+
+ def initialize(param)
+ # Available templating configurations can be found after
+ # manually booting the rescue system, then reading the
+ # hetzner templates at:
+ #
+ # /root/.oldroot/nfs/install/configs/
+ #
+ # also run: $ installimage -h
+ #
+ if param.is_a? Hetzner::Bootstrap::Template
+ return param
+ elsif param.is_a? String
+ @raw_template = param
+ end
+ end
+
+ def to_s
+ @raw_template
+ end
+ end
+ end
+end
5 lib/hetzner/bootstrap/version.rb
@@ -0,0 +1,5 @@
+module Hetzner
+ class Bootstrap
+ VERSION = '0.0.1'
+ end
+end
51 spec/hetzner_bootstrap_spec.rb
@@ -0,0 +1,51 @@
+require 'hetzner-api'
+require 'spec_helper'
+
+describe "Bootstrap" do
+ before(:all) do
+ @api = Hetzner::API.new API_USERNAME, API_PASSWORD
+ @bootstrap = Hetzner::Bootstrap.new :api => @api
+ end
+
+ context "add target" do
+
+ it "should be able to add a server to operate on" do
+ @bootstrap.add_target proper_target
+ @bootstrap.targets.should have(1).target
+ @bootstrap.targets.first.should be_instance_of Hetzner::Bootstrap::Target
+ end
+
+ it "should have the default template if none is specified" do
+ @bootstrap.add_target proper_target
+ @bootstrap.targets.first.template.should be_instance_of Hetzner::Bootstrap::Template
+ end
+
+ it "should raise an NoTemplateProvidedError when no template option provided" do
+ lambda {
+ @bootstrap.add_target improper_target_without_template
+ }.should raise_error(Hetzner::Bootstrap::Target::NoTemplateProvidedError)
+ end
+
+ end
+
+ def proper_target
+ return {
+ :ip => "1.2.3.4",
+ :login => "root",
+# :password => "halloMartin!",
+ :rescue_os => "linux",
+ :rescue_os_bit => "64",
+ :template => default_template
+ }
+ end
+
+ def improper_target_without_template
+ proper_target.select { |k,v| k != :template }
+ end
+
+ def default_template
+ "bla"
+ end
+end
+
+
Please sign in to comment.
Something went wrong with that request. Please try again.