hetzner-bootstrap 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ .idea/*
2
+ bin/*
3
+ *.gem
4
+ .bundle
5
+ Gemfile.lock
6
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in hetzner.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2011 Moriz GmbH, Roland Moriz, http://moriz.de/
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ --
23
+ <a href="http://moriz.de/opensource">a Moriz GmbH OpenSource project.</a>
data/README ADDED
@@ -0,0 +1,9 @@
1
+ hetzner-bootstrap allows you to bootstrap a provisioned EQ Server from hetzner.de
2
+
3
+ Requirements
4
+
5
+ - get a webservice login (robots.your-server.de)
6
+ - the ip address of the shipped systems
7
+
8
+ see example.rb for usage
9
+
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby
2
+ require "hetzner-bootstrap"
3
+
4
+ API_USERNAME="xxx"
5
+ API_PASSWORD="yyy"
6
+
7
+ bs = Hetzner::Bootstrap.new :api => Hetzner::API.new(API_USERNAME, API_PASSWORD)
8
+
9
+ # 2 disks, software raid 1, etc.
10
+ template = <<EOT
11
+ DRIVE1 /dev/sda
12
+ DRIVE2 /dev/sdb
13
+ FORMATDRIVE2 0
14
+
15
+ SWRAID 1
16
+ SWRAIDLEVEL 1
17
+
18
+ BOOTLOADER grub
19
+
20
+ HOSTNAME <%= hostname %>
21
+
22
+ PART /boot ext2 1G
23
+ PART lvm host 75G
24
+ PART lvm guest all
25
+
26
+ LV host root / ext3 50G
27
+ LV host swap swap swap 5G
28
+
29
+ IMAGE /root/images/Ubuntu-1010-maverick-64-minimal.tar.gz
30
+ EOT
31
+
32
+ post_install = <<EOT
33
+ knife bootstrap <%= ip %> -N <%= hostname %> "role[base],role[kvm_host]"
34
+ EOT
35
+
36
+ # duplicate entry for each system
37
+ bs << { :ip => "1.2.3.4",
38
+ :template => template, # string will be parsed by erubis
39
+ :hostname => 'server100.example.com', # will be used for setting the systems' hostname
40
+ :public_keys => "~/.ssh/id_dsa.pub", # will be copied over to the freshly bootstrapped system
41
+ :post_install => post_install } # will be called locally at the end and can be used e.g. to run a chef bootstrap
42
+
43
+ bs.bootstrap!
44
+
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "hetzner/bootstrap/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "hetzner-bootstrap"
7
+ s.version = Hetzner::Bootstrap::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Roland Moriz"]
10
+ s.email = ["roland@moriz.de"]
11
+ s.homepage = "http://moriz.de/opensource/hetzner-api"
12
+ s.summary = %q{Easy bootstrapping of hetzner.de rootservers using hetzner-api}
13
+ s.description = %q{Easy bootstrapping of hetzner.de rootservers using hetzner-api}
14
+
15
+ s.rubyforge_project = "hetzner"
16
+
17
+ s.add_dependency 'hetzner-api'
18
+ s.add_dependency 'net-ssh'
19
+ s.add_dependency 'erubis'
20
+
21
+ s.add_development_dependency "rspec", ">= 2.4.0"
22
+
23
+ s.files = `git ls-files`.split("\n")
24
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
25
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
26
+ s.require_paths = ["lib"]
27
+ end
@@ -0,0 +1,86 @@
1
+ require 'benchmark'
2
+
3
+ require 'hetzner-api'
4
+ require 'hetzner/bootstrap/version'
5
+ require 'hetzner/bootstrap/target'
6
+ require 'hetzner/bootstrap/template'
7
+
8
+ module Hetzner
9
+ class Bootstrap
10
+ attr_accessor :targets
11
+ attr_accessor :api
12
+ attr_accessor :use_threads
13
+ attr_accessor :actions
14
+
15
+ def initialize(options = {})
16
+ @targets = []
17
+ @actions = %w(enable_rescue_mode
18
+ reset
19
+ wait_for_ssh
20
+ installimage
21
+ wait_for_ssh
22
+ verify_installation
23
+ copy_ssh_keys
24
+ post_install)
25
+ @api = options[:api]
26
+ @use_threads = options[:use_threads] || true
27
+ end
28
+
29
+ def add_target(param)
30
+ if param.is_a? Hetzner::Bootstrap::Target
31
+ @targets << param
32
+ else
33
+ @targets << (Hetzner::Bootstrap::Target.new param)
34
+ end
35
+ end
36
+
37
+ def <<(param)
38
+ add_target param
39
+ end
40
+
41
+ def bootstrap!(options = {})
42
+ threads = []
43
+
44
+ @targets.each do |target|
45
+ target.use_api @api
46
+
47
+ if uses_threads?
48
+ threads << Thread.new do
49
+ bootstrap_one_target! target
50
+ end
51
+ else
52
+ bootstrap_one_target! target
53
+ end
54
+ end
55
+
56
+ finalize_threads(threads) if uses_threads?
57
+ end
58
+
59
+ def bootstrap_one_target!(target)
60
+ actions = (target.actions || @actions)
61
+ actions.each_with_index do |action, index|
62
+
63
+ log target.ip, action, index, 'START'
64
+ d = Benchmark.realtime do
65
+ target.send action
66
+ end
67
+
68
+ log target.ip, action, index, "FINISHED in #{sprintf "%.5f",d} seconds"
69
+ end
70
+ rescue => e
71
+ puts "something bad happend: #{e.class} #{e.message}"
72
+ end
73
+
74
+ def uses_threads?
75
+ @use_threads
76
+ end
77
+
78
+ def finalize_threads(threads)
79
+ threads.each { |t| t.join }
80
+ end
81
+
82
+ def log(where, what, index, message)
83
+ puts "[#{where}] #{what} #{' ' * (index * 4)}#{message}"
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,186 @@
1
+ require 'erubis'
2
+ require 'net/ssh'
3
+ require 'socket'
4
+
5
+ module Hetzner
6
+ class Bootstrap
7
+ class Target
8
+ attr_accessor :ip
9
+ attr_accessor :login
10
+ attr_accessor :password
11
+ attr_accessor :template
12
+ attr_accessor :rescue_os
13
+ attr_accessor :rescue_os_bit
14
+ attr_accessor :actions
15
+ attr_accessor :hostname
16
+ attr_accessor :post_install
17
+ attr_accessor :public_keys
18
+ attr_accessor :bootstrap_cmd
19
+
20
+ def initialize(options = {})
21
+ @rescue_os = 'linux'
22
+ @rescue_os_bit = '64'
23
+ @retries = 0
24
+ @bootstrap_cmd = '/root/.oldroot/nfs/install/installimage -a -c /tmp/template'
25
+
26
+ if tmpl = options.delete(:template)
27
+ @template = Template.new tmpl
28
+ else
29
+ raise NoTemplateProvidedError.new 'No imageinstall template provided.'
30
+ end
31
+
32
+ options.each_pair do |k,v|
33
+ self.send("#{k}=", v)
34
+ end
35
+ end
36
+
37
+ def enable_rescue_mode(options = {})
38
+ result = @api.enable_rescue! @ip, @rescue_os, @rescue_os_bit
39
+
40
+ if result.success? && result['rescue']
41
+ @login = 'root'
42
+ @password = result['rescue']['password']
43
+ reset_retries
44
+ puts "IP: #{ip} => password: #{@password}"
45
+ elsif @retries > 3
46
+ raise CantActivateRescueSystemError, result
47
+ else
48
+ @retries += 1
49
+
50
+ puts "problem while trying to activate rescue system (retries: #{@retries})"
51
+ @api.disable_rescue! @ip
52
+
53
+ sleep @retries * 5 # => 5, 10, 15s
54
+ enable_rescue_mode options
55
+ end
56
+ end
57
+
58
+ def reset(options = {})
59
+ result = @api.reset! @ip, :hw
60
+
61
+ if result.success?
62
+ reset_retries
63
+ sleep 15
64
+ elsif @retries > 3
65
+ raise CantResetSystemError, result
66
+ else
67
+ @retries += 1
68
+ rolling_sleep
69
+ puts "problem while trying to reset/reboot system (retries: #{@retries})"
70
+ reset options
71
+ end
72
+ end
73
+
74
+ def wait_for_ssh(options = {})
75
+ ssh_port_probe = TCPSocket.new @ip, 22
76
+ return if IO.select([ssh_port_probe], nil, nil, 5)
77
+
78
+ rescue Errno::ECONNREFUSED
79
+ @retries += 1
80
+ print "."
81
+ STDOUT.flush
82
+
83
+ if @retries > 20
84
+ raise CantSshAfterResetError
85
+ else
86
+ rolling_sleep
87
+ wait_for_ssh options
88
+ end
89
+ rescue => e
90
+ puts "Exception: #{e.class} #{e.message}"
91
+ ensure
92
+ puts ""
93
+ ssh_port_probe && ssh_port_probe.close
94
+ end
95
+
96
+ def installimage(options = {})
97
+ template = render_template
98
+
99
+ Net::SSH.start(@ip, @login, :password => @password) do |ssh|
100
+ ssh.exec!("echo \"#{template}\" > /tmp/template")
101
+ puts "remote executing: #{@bootstrap_cmd}"
102
+ output = ssh.exec!(@bootstrap_cmd)
103
+ puts output
104
+ ssh.exec!("reboot")
105
+ sleep 4
106
+ end
107
+ rescue Net::SSH::HostKeyMismatch => e
108
+ e.remember_host!
109
+ retry
110
+ end
111
+
112
+ def verify_installation(options = {})
113
+ Net::SSH.start(@ip, @login, :password => @password) do |ssh|
114
+ working_hostname = ssh.exec!("cat /etc/hostname")
115
+ unless @hostname == working_hostname.chomp
116
+ raise InstallationError, "hostnames do not match: assumed #{@hostname} but received #{working_hostname}"
117
+ end
118
+ end
119
+ rescue Net::SSH::HostKeyMismatch => e
120
+ e.remember_host!
121
+ retry
122
+ end
123
+
124
+ def copy_ssh_keys(options = {})
125
+ if @public_keys
126
+ Net::SSH.start(@ip, @login, :password => @password) do |ssh|
127
+ ssh.exec!("mkdir /root/.ssh")
128
+ @public_keys.to_a.each do |key|
129
+ pub = File.read(File.expand_path(key))
130
+ ssh.exec!("echo \"#{pub}\" >> /root/.ssh/authorized_keys")
131
+ end
132
+ end
133
+ end
134
+ rescue Net::SSH::HostKeyMismatch => e
135
+ e.remember_host!
136
+ retry
137
+ end
138
+
139
+ def post_install(options = {})
140
+ return unless @post_install
141
+ post_install = render_post_install
142
+ puts "executing:\n #{post_install}"
143
+ puts `#{post_install}`
144
+ end
145
+
146
+ def render_template
147
+ eruby = Erubis::Eruby.new @template.to_s
148
+
149
+ params = {}
150
+ params[:hostname] = @hostname
151
+ params[:ip] = @ip
152
+
153
+ return eruby.result(params)
154
+ end
155
+
156
+ def render_post_install
157
+ eruby = Erubis::Eruby.new @post_install.to_s
158
+
159
+ params = {}
160
+ params[:hostname] = @hostname
161
+ params[:ip] = @ip
162
+ params[:login] = @login
163
+ params[:password] = @password
164
+
165
+ return eruby.result(params)
166
+ end
167
+
168
+ def use_api(api)
169
+ @api = api
170
+ end
171
+
172
+ def reset_retries
173
+ @retries = 0
174
+ end
175
+
176
+ def rolling_sleep
177
+ sleep @retries * @retries * 3 + 1 # => 1, 4, 13, 28, 49, 76, 109, 148, 193, 244, 301, 364 ... seconds
178
+ end
179
+
180
+ class NoTemplateProvidedError < ArgumentError; end
181
+ class CantActivateRescueSystemError < StandardError; end
182
+ class CantResetSystemError < StandardError; end
183
+ class InstallationError < StandardError; end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,27 @@
1
+ module Hetzner
2
+ class Bootstrap
3
+ class Template
4
+ attr_accessor :raw_template
5
+
6
+ def initialize(param)
7
+ # Available templating configurations can be found after
8
+ # manually booting the rescue system, then reading the
9
+ # hetzner templates at:
10
+ #
11
+ # /root/.oldroot/nfs/install/configs/
12
+ #
13
+ # also run: $ installimage -h
14
+ #
15
+ if param.is_a? Hetzner::Bootstrap::Template
16
+ return param
17
+ elsif param.is_a? String
18
+ @raw_template = param
19
+ end
20
+ end
21
+
22
+ def to_s
23
+ @raw_template
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ module Hetzner
2
+ class Bootstrap
3
+ VERSION = '0.0.1'
4
+ end
5
+ end
@@ -0,0 +1,51 @@
1
+ require 'hetzner-api'
2
+ require 'spec_helper'
3
+
4
+ describe "Bootstrap" do
5
+ before(:all) do
6
+ @api = Hetzner::API.new API_USERNAME, API_PASSWORD
7
+ @bootstrap = Hetzner::Bootstrap.new :api => @api
8
+ end
9
+
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
17
+
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
21
+ end
22
+
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)
27
+ end
28
+
29
+ end
30
+
31
+ 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
39
+ }
40
+ end
41
+
42
+ def improper_target_without_template
43
+ proper_target.select { |k,v| k != :template }
44
+ end
45
+
46
+ def default_template
47
+ "bla"
48
+ end
49
+ end
50
+
51
+
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hetzner-bootstrap
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Roland Moriz
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-02-06 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: hetzner-api
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: net-ssh
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: erubis
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ type: :runtime
62
+ version_requirements: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ name: rspec
65
+ prerelease: false
66
+ requirement: &id004 !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 31
72
+ segments:
73
+ - 2
74
+ - 4
75
+ - 0
76
+ version: 2.4.0
77
+ type: :development
78
+ version_requirements: *id004
79
+ description: Easy bootstrapping of hetzner.de rootservers using hetzner-api
80
+ email:
81
+ - roland@moriz.de
82
+ executables: []
83
+
84
+ extensions: []
85
+
86
+ extra_rdoc_files: []
87
+
88
+ files:
89
+ - .gitignore
90
+ - Gemfile
91
+ - LICENSE
92
+ - README
93
+ - Rakefile
94
+ - example.rb
95
+ - hetzner-bootstrap.gemspec
96
+ - lib/hetzner-bootstrap.rb
97
+ - lib/hetzner/bootstrap/target.rb
98
+ - lib/hetzner/bootstrap/template.rb
99
+ - lib/hetzner/bootstrap/version.rb
100
+ - spec/hetzner_bootstrap_spec.rb
101
+ has_rdoc: true
102
+ homepage: http://moriz.de/opensource/hetzner-api
103
+ licenses: []
104
+
105
+ post_install_message:
106
+ rdoc_options: []
107
+
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ none: false
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ hash: 3
116
+ segments:
117
+ - 0
118
+ version: "0"
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ none: false
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ hash: 3
125
+ segments:
126
+ - 0
127
+ version: "0"
128
+ requirements: []
129
+
130
+ rubyforge_project: hetzner
131
+ rubygems_version: 1.4.2
132
+ signing_key:
133
+ specification_version: 3
134
+ summary: Easy bootstrapping of hetzner.de rootservers using hetzner-api
135
+ test_files:
136
+ - spec/hetzner_bootstrap_spec.rb