hetzner-bootstrap 0.0.1

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.
@@ -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