awsborn 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.mdown +204 -0
- data/Rakefile +47 -0
- data/VERSION +1 -0
- data/lib/awsborn/awsborn.rb +58 -0
- data/lib/awsborn/ec2.rb +87 -0
- data/lib/awsborn/extensions/object.rb +0 -0
- data/lib/awsborn/extensions/proc.rb +13 -0
- data/lib/awsborn/known_hosts_updater.rb +117 -0
- data/lib/awsborn/server.rb +207 -0
- data/lib/awsborn/server_cluster.rb +69 -0
- data/lib/awsborn.rb +17 -0
- data/spec/awsborn_spec.rb +7 -0
- data/spec/server_spec.rb +24 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +14 -0
- metadata +123 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 David Vrensk
|
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.
|
data/README.mdown
ADDED
@@ -0,0 +1,204 @@
|
|
1
|
+
# awsborn
|
2
|
+
|
3
|
+
This Gem lets you define and launch a server cluster on Amazon EC2.
|
4
|
+
The `launch` operation is idempotent, i.e., it only launches instances
|
5
|
+
that are not running.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
gem install awsborn
|
10
|
+
|
11
|
+
## Synopsis
|
12
|
+
|
13
|
+
#! /usr/bin/env ruby
|
14
|
+
|
15
|
+
require 'rubygems'
|
16
|
+
require 'awsborn'
|
17
|
+
|
18
|
+
# If not set, will be picked up from ENV['AMAZON_ACCESS_KEY_ID']
|
19
|
+
# and ENV['AMAZON_SECRET_ACCESS_KEY'].
|
20
|
+
Awsborn.access_key_id = 'AAAAAAAAAAAA'
|
21
|
+
Awsborn.secret_access_key = 'KKKKKKKKKKKK'
|
22
|
+
|
23
|
+
# Logs to `awsborn.log` in current dir with level INFO by default.
|
24
|
+
# To suppress logging, do `Awsborn.logger = Logger.new('/dev/null')`
|
25
|
+
Awsborn.logger.level = Logger::DEBUG
|
26
|
+
|
27
|
+
class LogServer < Awsborn::Server
|
28
|
+
instance_type :m1_small
|
29
|
+
image_id 'ami-2fc2e95b', :sudo_user => 'ubuntu'
|
30
|
+
security_group 'Basic web'
|
31
|
+
keys 'keys/*.pub'
|
32
|
+
bootstrap_script 'chef-bootstrap.sh'
|
33
|
+
end
|
34
|
+
|
35
|
+
log_servers = LogServer.cluster do
|
36
|
+
domain 'releware.net'
|
37
|
+
server :log_a, :zone => :eu_west_1a, :disk => {:sdf => "vol-a857b8c1"}, :ip => 'log-a'
|
38
|
+
server :log_b, :zone => :eu_west_1b, :disk => {:sdf => "vol-aa57b8c3"}, :ip => 'log-b'
|
39
|
+
end
|
40
|
+
|
41
|
+
log_servers.launch
|
42
|
+
|
43
|
+
log_servers.each do |server|
|
44
|
+
system "rake cook server=#{server.host_name}"
|
45
|
+
end
|
46
|
+
|
47
|
+
## How it works
|
48
|
+
|
49
|
+
It's really simple:
|
50
|
+
|
51
|
+
1. Define a server type (`LogServer` in the example above).
|
52
|
+
2. Define a cluster of server instances.
|
53
|
+
3. Launch the cluster.
|
54
|
+
|
55
|
+
See *Launching a cluster* below for details.
|
56
|
+
|
57
|
+
### Defining a server type
|
58
|
+
|
59
|
+
Server types can be named anything but must inherit from `Awsborn::Server`.
|
60
|
+
Servers take five different directives:
|
61
|
+
|
62
|
+
* `instance_type` -- a symbol. One of `:m1_small`, `:m1_large`, `:m1_xlarge`,
|
63
|
+
`:m2_2xlarge`, `:m2_4xlarge`, `:c1_medium`, and `:c1_xlarge`.
|
64
|
+
* `image_id` -- a valid EC2 AMI. Specify `:sudo_user` as an option if the AMI
|
65
|
+
does not allow you to log in as root.
|
66
|
+
* `security_group` -- a security group that you own.
|
67
|
+
* `keys` -- one or more globs to public ssh keys. When the servers are running,
|
68
|
+
`root` will be able to log in using any one of these keys.
|
69
|
+
* `bootstrap_script` -- path to a script which will be run on each instance as
|
70
|
+
soon as it is started. Use it to bootstrap `chef` and let `chef` take it from
|
71
|
+
there. A sample bootstrap script is included towards the end of this document.
|
72
|
+
|
73
|
+
A server cannot be started without `instance_type`, `image_id` and `security_group`.
|
74
|
+
The don't have to be defined for the server type though, but can
|
75
|
+
equally well be added as options to the specific servers.
|
76
|
+
|
77
|
+
### Defining a cluster
|
78
|
+
|
79
|
+
The `cluster` method accepts two commands, `domain` and `server`. `domain` is
|
80
|
+
optional and is just a way to avoid repetition in the `server` commands. `server`
|
81
|
+
takes a name (which can be used as a key in the cluster, e.g. `log_servers[:log_a]`)
|
82
|
+
and a hash:
|
83
|
+
|
84
|
+
Mandatory keys:
|
85
|
+
|
86
|
+
* `:zone` -- the availability zone for the server. One of `:us_east_1a`, `:us_east_1b`,
|
87
|
+
`:us_east_1c`, `:us_west_1a`, `:us_west_1b`, `:eu_west_1a`, `:eu_west_1b`.
|
88
|
+
* `:disk` -- a hash of `device => volume-id`. Awsborn uses the disks to tell if a server
|
89
|
+
is running or not (see *Launching a cluster*).
|
90
|
+
* `:ip` -- a domain name which translates to an elastic ip. If the domain name does not
|
91
|
+
contain a full stop (dot) and `domain` has been specified above, the domain is added.
|
92
|
+
|
93
|
+
Keys that override server type settings:
|
94
|
+
|
95
|
+
* `:instance_type`
|
96
|
+
* `:sudo_user`
|
97
|
+
* `:security_group`
|
98
|
+
* `:keys`
|
99
|
+
* `:bootstrap_script`
|
100
|
+
|
101
|
+
### Launching a cluster
|
102
|
+
|
103
|
+
The `launch` method on the cluster checks to see if each server is running by checking
|
104
|
+
if *one of the server's disks is attached to an EC2 instance*, i.e., the server is
|
105
|
+
defined by its content, not by its AMI or ip address.
|
106
|
+
|
107
|
+
Servers that not running are started by calling the `start` method on the server,
|
108
|
+
which does the following:
|
109
|
+
|
110
|
+
def start (key_pair)
|
111
|
+
launch_instance(key_pair)
|
112
|
+
|
113
|
+
update_known_hosts
|
114
|
+
install_ssh_keys(key_pair) if keys
|
115
|
+
|
116
|
+
if elastic_ip
|
117
|
+
associate_address
|
118
|
+
update_known_hosts
|
119
|
+
end
|
120
|
+
|
121
|
+
bootstrap if bootstrap_script
|
122
|
+
attach_volumes
|
123
|
+
end
|
124
|
+
|
125
|
+
The `key_pair` is a temporary key pair that is used only for launching this cluster.
|
126
|
+
In case of a failed launch, the private key is available as `/tmp/temp_key_*`.
|
127
|
+
|
128
|
+
## A bootstrapping script
|
129
|
+
|
130
|
+
This is what we use with the AMI above:
|
131
|
+
|
132
|
+
#!/bin/bash
|
133
|
+
|
134
|
+
echo '------------------'
|
135
|
+
echo 'Bootstrapping Chef'
|
136
|
+
echo
|
137
|
+
|
138
|
+
aptitude -y update
|
139
|
+
aptitude -y install gcc g++ curl build-essential \
|
140
|
+
libxml-ruby libxml2-dev \
|
141
|
+
ruby irb ri rdoc ruby1.8-dev libzlib-ruby libyaml-ruby libreadline-ruby \
|
142
|
+
libruby libruby-extras libopenssl-ruby \
|
143
|
+
libdbm-ruby libdbi-ruby libdbd-sqlite3-ruby \
|
144
|
+
sqlite3 libsqlite3-dev libsqlite3-ruby
|
145
|
+
|
146
|
+
curl -L 'http://rubyforge.org/frs/download.php/69365/rubygems-1.3.6.tgz' | tar zxf -
|
147
|
+
cd rubygems* && ruby setup.rb --no-ri --no-rdoc
|
148
|
+
|
149
|
+
ln -sfv /usr/bin/gem1.8 /usr/bin/gem
|
150
|
+
|
151
|
+
gem install chef ohai --no-ri --no-rdoc --source http://gems.opscode.com --source http://gems.rubyforge.org
|
152
|
+
|
153
|
+
echo
|
154
|
+
echo 'Bootstrapping Chef - done'
|
155
|
+
echo '-------------------------'
|
156
|
+
|
157
|
+
## Bugs and surprising features
|
158
|
+
|
159
|
+
No bugs are known at this time.
|
160
|
+
|
161
|
+
If a server is defined with two disks and the disks are attached to different
|
162
|
+
EC2 instances, the server will still be considered running. If one disk is
|
163
|
+
attached and the other isn't, one of these things will happen:
|
164
|
+
|
165
|
+
* The server is considered to be running, *or*
|
166
|
+
* The server is considered down and an attempt to start it will likely fail since
|
167
|
+
one disk is already attached to another instance.
|
168
|
+
|
169
|
+
### Untested features
|
170
|
+
|
171
|
+
* Launching without ssh keys. (I suppose certain AMIs would support that.)
|
172
|
+
* Running without elastic ip.
|
173
|
+
|
174
|
+
## To do / Could do
|
175
|
+
|
176
|
+
* Tests.
|
177
|
+
* Add cloud watch
|
178
|
+
* Dynamic discovery of instance types and availability zones.
|
179
|
+
* Hack RightAws to verify certificates.
|
180
|
+
* Elastic Load Balancing.
|
181
|
+
* Launch servers in parallel.
|
182
|
+
|
183
|
+
## Note on Patches/Pull Requests
|
184
|
+
|
185
|
+
* Fork the project.
|
186
|
+
* Make your feature addition or bug fix.
|
187
|
+
* Add tests for it. This is important so I don't break it in a
|
188
|
+
future version unintentionally.
|
189
|
+
* At the moment, the gem does not have any tests. I intend to fix that,
|
190
|
+
and I won't accept patches without tests.
|
191
|
+
* Commit, do not mess with rakefile, version, or history.
|
192
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
193
|
+
* Send me a pull request. Bonus points for topic branches.
|
194
|
+
|
195
|
+
## History
|
196
|
+
|
197
|
+
This gem is inspired by [Awsymandias](http://github.com/bguthrie/awsymandias)
|
198
|
+
which was sort of what I needed but was too complicated to fix, partly because
|
199
|
+
the documentation was out of date. Big thanks for the inspiration and random
|
200
|
+
code snippets.
|
201
|
+
|
202
|
+
## Copyright
|
203
|
+
|
204
|
+
Copyright (c) 2010 ICE House & David Vrensk. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "awsborn"
|
8
|
+
gem.summary = %Q{Awsborn lets you define and launch a server cluster on Amazon EC2.}
|
9
|
+
gem.description = %Q{Awsborn defines servers as instances with a certain disk volume, which makes it easy to restart missing servers.}
|
10
|
+
gem.email = "david@icehouse.se"
|
11
|
+
gem.homepage = "http://github.com/icehouse/awsborn"
|
12
|
+
gem.authors = ["David Vrensk"]
|
13
|
+
gem.add_dependency "right_aws", ">= 1.10.0"
|
14
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
15
|
+
gem.add_development_dependency "webmock", ">= 0.9.1"
|
16
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
17
|
+
end
|
18
|
+
Jeweler::GemcutterTasks.new
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'spec/rake/spectask'
|
24
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
25
|
+
spec.libs << 'lib' << 'spec'
|
26
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
27
|
+
end
|
28
|
+
|
29
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
30
|
+
spec.libs << 'lib' << 'spec'
|
31
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
32
|
+
spec.rcov = true
|
33
|
+
end
|
34
|
+
|
35
|
+
task :spec => :check_dependencies
|
36
|
+
|
37
|
+
task :default => :spec
|
38
|
+
|
39
|
+
require 'rake/rdoctask'
|
40
|
+
Rake::RDocTask.new do |rdoc|
|
41
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
42
|
+
|
43
|
+
rdoc.rdoc_dir = 'rdoc'
|
44
|
+
rdoc.title = "awsborn #{version}"
|
45
|
+
rdoc.rdoc_files.include('README*')
|
46
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
47
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Awsborn
|
2
|
+
class SecurityError < StandardError ; end
|
3
|
+
|
4
|
+
class << self
|
5
|
+
attr_writer :access_key_id, :secret_access_key, :logger
|
6
|
+
attr_accessor :verbose
|
7
|
+
|
8
|
+
Awsborn.verbose = true
|
9
|
+
|
10
|
+
def access_key_id
|
11
|
+
@access_key_id ||= ENV['AMAZON_ACCESS_KEY_ID']
|
12
|
+
end
|
13
|
+
|
14
|
+
def secret_access_key
|
15
|
+
@secret_access_key ||= ENV['AMAZON_SECRET_ACCESS_KEY']
|
16
|
+
end
|
17
|
+
|
18
|
+
def logger
|
19
|
+
unless defined? @logger
|
20
|
+
dir = [File.dirname(File.expand_path($0)), '/tmp'].find { |d| File.writable?(d) }
|
21
|
+
if dir
|
22
|
+
file = File.open(File.join(dir, 'awsborn.log'), 'a')
|
23
|
+
file.sync = true
|
24
|
+
end
|
25
|
+
@logger = Logger.new(file || $stdout)
|
26
|
+
@logger.level = Logger::INFO
|
27
|
+
end
|
28
|
+
@logger
|
29
|
+
end
|
30
|
+
|
31
|
+
# @throws Interrupt
|
32
|
+
def wait_for (message, sleep_seconds = 5, max_wait_seconds = nil)
|
33
|
+
stdout_sync = $stdout.sync
|
34
|
+
$stdout.sync = true
|
35
|
+
|
36
|
+
start_at = Time.now
|
37
|
+
print "Waiting for #{message}.." if Awsborn.verbose
|
38
|
+
result = yield
|
39
|
+
while ! result
|
40
|
+
if max_wait_seconds && Time.now > start_at + max_wait_seconds
|
41
|
+
raise Interrupt, "Timed out after #{Time.now - start_at} seconds."
|
42
|
+
end
|
43
|
+
print "." if Awsborn.verbose
|
44
|
+
sleep sleep_seconds
|
45
|
+
result = yield
|
46
|
+
end
|
47
|
+
verbose_output "OK!"
|
48
|
+
result
|
49
|
+
ensure
|
50
|
+
$stdout.sync = stdout_sync
|
51
|
+
end
|
52
|
+
|
53
|
+
def verbose_output(message)
|
54
|
+
puts message if Awsborn.verbose
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
data/lib/awsborn/ec2.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
module Awsborn
|
2
|
+
class Ec2
|
3
|
+
|
4
|
+
attr_accessor :instance_id
|
5
|
+
|
6
|
+
def connection
|
7
|
+
unless @connection
|
8
|
+
env_ec2_url = ENV['EC2_URL']
|
9
|
+
begin
|
10
|
+
ENV['EC2_URL'] = @endpoint
|
11
|
+
@connection = ::RightAws::Ec2.new(Awsborn.access_key_id,
|
12
|
+
Awsborn.secret_access_key,
|
13
|
+
:logger => Awsborn.logger)
|
14
|
+
ensure
|
15
|
+
ENV['EC2_URL'] = env_ec2_url
|
16
|
+
end
|
17
|
+
end
|
18
|
+
@connection
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize (zone)
|
22
|
+
@endpoint = case zone
|
23
|
+
when :eu_west_1a, :eu_west_1b
|
24
|
+
'https://eu-west-1.ec2.amazonaws.com/'
|
25
|
+
when :us_east_1a, :us_east_1b
|
26
|
+
'https://us-east-1.ec2.amazonaws.com'
|
27
|
+
else
|
28
|
+
'https://ec2.amazonaws.com'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
KeyPair = Struct.new :name, :path
|
33
|
+
|
34
|
+
def generate_key_pair
|
35
|
+
time = Time.now
|
36
|
+
key_name = "temp_key_#{time.to_i}_#{time.usec}"
|
37
|
+
key_data = connection.create_key_pair(key_name)
|
38
|
+
file_path = File.join(ENV['TMP_DIR'] || '/tmp', "#{key_name}.pem")
|
39
|
+
File.open file_path, "w", 0600 do |f|
|
40
|
+
f.write key_data[:aws_material]
|
41
|
+
end
|
42
|
+
KeyPair.new key_name, file_path
|
43
|
+
end
|
44
|
+
|
45
|
+
def delete_key_pair (key_pair)
|
46
|
+
connection.delete_key_pair(key_pair.name)
|
47
|
+
end
|
48
|
+
|
49
|
+
def volume_has_instance? (volume_id)
|
50
|
+
response = connection.describe_volumes(volume_id).first
|
51
|
+
if response[:aws_status] == 'in-use'
|
52
|
+
self.instance_id = response[:aws_instance_id]
|
53
|
+
true
|
54
|
+
else
|
55
|
+
false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
IP4_REGEXP = /^(\d{1,3}\.){3}\d{1,3}$/
|
60
|
+
|
61
|
+
def associate_address (address)
|
62
|
+
unless address.match(IP4_REGEXP)
|
63
|
+
address = Resolv.getaddress address
|
64
|
+
end
|
65
|
+
connection.associate_address(instance_id, address)
|
66
|
+
end
|
67
|
+
|
68
|
+
def describe_instance
|
69
|
+
connection.describe_instances(instance_id).first
|
70
|
+
end
|
71
|
+
|
72
|
+
def launch_instance (*args)
|
73
|
+
response = connection.launch_instances(*args).first
|
74
|
+
self.instance_id = response[:aws_instance_id]
|
75
|
+
response
|
76
|
+
end
|
77
|
+
|
78
|
+
def get_console_output
|
79
|
+
output = connection.get_console_output(instance_id)
|
80
|
+
output[:aws_output]
|
81
|
+
end
|
82
|
+
|
83
|
+
def attach_volume (volume, device)
|
84
|
+
connection.attach_volume(volume, instance_id, device)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
File without changes
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# Copied from ActiveSupport and modified
|
2
|
+
class Proc #:nodoc:
|
3
|
+
def bind (object, basename = nil)
|
4
|
+
block, time = self, Time.now
|
5
|
+
(class << object; self end).class_eval do
|
6
|
+
method_name = "__#{basename || 'bind'}_#{time.to_i}_#{time.usec}"
|
7
|
+
define_method(method_name, &block)
|
8
|
+
method = instance_method(method_name)
|
9
|
+
remove_method(method_name)
|
10
|
+
method
|
11
|
+
end.bind(object)
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Awsborn
|
2
|
+
class KnownHostsUpdater
|
3
|
+
|
4
|
+
class << self
|
5
|
+
attr_accessor :logger
|
6
|
+
def update_for_server (server)
|
7
|
+
known_hosts = new(server.ec2, server.host_name)
|
8
|
+
known_hosts.update
|
9
|
+
end
|
10
|
+
|
11
|
+
def logger
|
12
|
+
@logger ||= Awsborn.logger
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_accessor :ec2, :host_name, :host_ip, :console_fingerprint, :fingerprint_file, :logger
|
17
|
+
|
18
|
+
def initialize (ec2, host_name)
|
19
|
+
@host_name = host_name
|
20
|
+
@ec2 = ec2
|
21
|
+
end
|
22
|
+
|
23
|
+
def update
|
24
|
+
tries = 0
|
25
|
+
begin
|
26
|
+
tries += 1
|
27
|
+
try_update
|
28
|
+
rescue SecurityError => e
|
29
|
+
if tries < 5
|
30
|
+
logger.debug e.message
|
31
|
+
logger.debug "Sleeping, try #{tries}"
|
32
|
+
sleep([2**tries, 15].min)
|
33
|
+
retry
|
34
|
+
else
|
35
|
+
raise e
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def try_update
|
41
|
+
get_console_fingerprint
|
42
|
+
clean_out_known_hosts
|
43
|
+
scan_hosts
|
44
|
+
compare_fingerprints!
|
45
|
+
save_fingerprints
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_console_fingerprint
|
49
|
+
# ec2: -----BEGIN SSH HOST KEY FINGERPRINTS-----
|
50
|
+
# ec2: 2048 7a:e9:eb:41:b7:45:b1:07:30:ad:13:c5:a9:2a:a1:e5 /etc/ssh/ssh_host_rsa_key.pub (RSA)
|
51
|
+
regexp = %r{BEGIN SSH HOST KEY FINGERPRINTS.*((?:[0-9a-f]{2}:){15}[0-9a-f]{2}) /etc/ssh/ssh_host_rsa_key.pub }m
|
52
|
+
@console_fingerprint = Awsborn.wait_for "console output", 15, 420 do
|
53
|
+
console = ec2.get_console_output
|
54
|
+
if console.any?
|
55
|
+
fingerprint = console[regexp, 1]
|
56
|
+
if ! fingerprint
|
57
|
+
logger.error "*** SSH RSA fingerprint not found ***"
|
58
|
+
logger.error lines
|
59
|
+
logger.error "*** SSH RSA fingerprint not found ***"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
fingerprint
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def clean_out_known_hosts
|
67
|
+
remove_from_known_hosts host_name
|
68
|
+
remove_from_known_hosts host_ip
|
69
|
+
end
|
70
|
+
|
71
|
+
def remove_from_known_hosts (host)
|
72
|
+
system "ssh-keygen -R #{host} > /dev/null 2>&1"
|
73
|
+
end
|
74
|
+
|
75
|
+
def scan_hosts
|
76
|
+
self.fingerprint_file = create_tempfile
|
77
|
+
system "ssh-keyscan -t rsa #{host_name} #{host_ip} > #{fingerprint_file} 2>/dev/null"
|
78
|
+
end
|
79
|
+
|
80
|
+
def compare_fingerprints!
|
81
|
+
name_fingerprint, ip_fingerprint = fingerprints_from_file
|
82
|
+
|
83
|
+
if name_fingerprint.nil? || ip_fingerprint.nil?
|
84
|
+
raise SecurityError, "name_fingerprint = #{name_fingerprint.inspect}, ip_fingerprint = #{ip_fingerprint.inspect}"
|
85
|
+
elsif name_fingerprint.split[1] != ip_fingerprint.split[1]
|
86
|
+
raise SecurityError, "Fingerprints do not match:\n#{name_fingerprint} (#{host_name})\n#{ip_fingerprint} (#{host_ip})!"
|
87
|
+
elsif name_fingerprint.split[1] != console_fingerprint
|
88
|
+
raise SecurityError, "Fingerprints do not match:\n#{name_fingerprint} (#{host_name})\n#{console_fingerprint} (EC2 Console)!"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def fingerprints_from_file
|
93
|
+
`ssh-keygen -l -f #{fingerprint_file}`.split("\n")
|
94
|
+
end
|
95
|
+
|
96
|
+
def save_fingerprints
|
97
|
+
system "cat #{fingerprint_file} >> #{ENV['HOME']}/.ssh/known_hosts"
|
98
|
+
end
|
99
|
+
|
100
|
+
def create_tempfile
|
101
|
+
tmp = Tempfile.new "awsborn"
|
102
|
+
tmp.close
|
103
|
+
def tmp.to_s
|
104
|
+
path
|
105
|
+
end
|
106
|
+
tmp
|
107
|
+
end
|
108
|
+
|
109
|
+
def logger
|
110
|
+
@logger ||= self.class.logger
|
111
|
+
end
|
112
|
+
|
113
|
+
def host_ip
|
114
|
+
@host_ip ||= Resolv.getaddress host_name
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
module Awsborn
|
2
|
+
class Server
|
3
|
+
|
4
|
+
def initialize (name, options = {})
|
5
|
+
@name = name
|
6
|
+
@options = options.dup
|
7
|
+
self.host_name = elastic_ip
|
8
|
+
end
|
9
|
+
|
10
|
+
class << self
|
11
|
+
attr_accessor :logger
|
12
|
+
def image_id (*args)
|
13
|
+
unless args.empty?
|
14
|
+
@image_id = args.first
|
15
|
+
@sudo_user = args.last[:sudo_user] if args.last.is_a?(Hash)
|
16
|
+
end
|
17
|
+
@image_id
|
18
|
+
end
|
19
|
+
def instance_type (*args)
|
20
|
+
@instance_type = args.first unless args.empty?
|
21
|
+
@instance_type
|
22
|
+
end
|
23
|
+
def security_group (*args)
|
24
|
+
@security_group = args.first unless args.empty?
|
25
|
+
@security_group
|
26
|
+
end
|
27
|
+
def keys (*args)
|
28
|
+
@keys = args unless args.empty?
|
29
|
+
@keys
|
30
|
+
end
|
31
|
+
def sudo_user (*args)
|
32
|
+
@sudo_user = args.first unless args.empty?
|
33
|
+
@sudo_user
|
34
|
+
end
|
35
|
+
def bootstrap_script (*args)
|
36
|
+
@bootstrap_script = args.first unless args.empty?
|
37
|
+
@bootstrap_script
|
38
|
+
end
|
39
|
+
|
40
|
+
def cluster (&block)
|
41
|
+
ServerCluster.build self, &block
|
42
|
+
end
|
43
|
+
def logger
|
44
|
+
@logger ||= Awsborn.logger
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def one_of_my_disks_is_attached_to_a_running_instance?
|
49
|
+
vol_id = disk.values.first
|
50
|
+
ec2.volume_has_instance?(vol_id)
|
51
|
+
end
|
52
|
+
alias :running? :one_of_my_disks_is_attached_to_a_running_instance?
|
53
|
+
|
54
|
+
def start (key_pair)
|
55
|
+
launch_instance(key_pair)
|
56
|
+
|
57
|
+
update_known_hosts
|
58
|
+
install_ssh_keys(key_pair) if keys
|
59
|
+
|
60
|
+
if elastic_ip
|
61
|
+
associate_address
|
62
|
+
update_known_hosts
|
63
|
+
end
|
64
|
+
|
65
|
+
bootstrap if bootstrap_script
|
66
|
+
attach_volumes
|
67
|
+
end
|
68
|
+
|
69
|
+
def launch_instance (key_pair)
|
70
|
+
@launch_response = ec2.launch_instance(image_id,
|
71
|
+
:instance_type => constant(instance_type),
|
72
|
+
:availability_zone => constant(zone),
|
73
|
+
:key_name => key_pair.name,
|
74
|
+
:group_ids => security_group
|
75
|
+
)
|
76
|
+
logger.debug @launch_response
|
77
|
+
|
78
|
+
Awsborn.wait_for("instance #{instance_id} (#{name}) to start", 10) { instance_running? }
|
79
|
+
self.host_name = aws_dns_name
|
80
|
+
end
|
81
|
+
|
82
|
+
def update_known_hosts
|
83
|
+
KnownHostsUpdater.update_for_server self
|
84
|
+
end
|
85
|
+
|
86
|
+
def install_ssh_keys (temp_key_pair)
|
87
|
+
cmd = "ssh -i #{temp_key_pair.path} #{sudo_user || 'root'}@#{aws_dns_name} 'cat > .ssh/authorized_keys'"
|
88
|
+
logger.debug cmd
|
89
|
+
IO.popen(cmd, "w") do |pipe|
|
90
|
+
pipe.puts key_data
|
91
|
+
end
|
92
|
+
if sudo_user
|
93
|
+
system("ssh #{sudo_user}@#{aws_dns_name} 'sudo cp .ssh/authorized_keys /root/.ssh/authorized_keys'")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def key_data
|
98
|
+
Dir[*keys].inject([]) do |memo, file_name|
|
99
|
+
memo + File.readlines(file_name).map { |line| line.chomp }
|
100
|
+
end.join("\n")
|
101
|
+
end
|
102
|
+
|
103
|
+
def path_relative_to_script (path)
|
104
|
+
File.join(File.dirname(File.expand_path($0)), path)
|
105
|
+
end
|
106
|
+
|
107
|
+
def associate_address
|
108
|
+
ec2.associate_address(elastic_ip)
|
109
|
+
self.host_name = elastic_ip
|
110
|
+
end
|
111
|
+
|
112
|
+
def bootstrap
|
113
|
+
script = path_relative_to_script(bootstrap_script)
|
114
|
+
basename = File.basename(script)
|
115
|
+
system "scp #{script} root@#{elastic_ip}:/tmp"
|
116
|
+
system "ssh root@#{elastic_ip} 'cd /tmp && chmod 700 #{basename} && ./#{basename}'"
|
117
|
+
end
|
118
|
+
|
119
|
+
def attach_volumes
|
120
|
+
disk.each_pair do |device, volume|
|
121
|
+
device = "/dev/#{device}" if device.is_a?(Symbol) || ! device.match('/')
|
122
|
+
res = ec2.attach_volume(volume, device)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def ec2
|
127
|
+
@ec2 ||= Ec2.new(zone)
|
128
|
+
end
|
129
|
+
|
130
|
+
begin :accessors
|
131
|
+
attr_accessor :name, :host_name, :logger
|
132
|
+
def zone
|
133
|
+
@options[:zone]
|
134
|
+
end
|
135
|
+
def disk
|
136
|
+
@options[:disk]
|
137
|
+
end
|
138
|
+
def image_id
|
139
|
+
self.class.image_id
|
140
|
+
end
|
141
|
+
def instance_type
|
142
|
+
@options[:instance_type] || self.class.instance_type
|
143
|
+
end
|
144
|
+
def security_group
|
145
|
+
@options[:security_group] || self.class.security_group
|
146
|
+
end
|
147
|
+
def sudo_user
|
148
|
+
@options[:sudo_user] || self.class.sudo_user
|
149
|
+
end
|
150
|
+
def bootstrap_script
|
151
|
+
@options[:bootstrap_script] || self.class.bootstrap_script
|
152
|
+
end
|
153
|
+
def keys
|
154
|
+
@options[:keys] || self.class.keys
|
155
|
+
end
|
156
|
+
def elastic_ip
|
157
|
+
@options[:ip]
|
158
|
+
end
|
159
|
+
def instance_id
|
160
|
+
ec2.instance_id
|
161
|
+
end
|
162
|
+
def aws_dns_name
|
163
|
+
describe_instance[:dns_name]
|
164
|
+
end
|
165
|
+
def launch_time
|
166
|
+
xml_time = describe_instance[:aws_launch_time]
|
167
|
+
logger.debug xml_time
|
168
|
+
Time.xmlschema(xml_time)
|
169
|
+
end
|
170
|
+
def instance_running?
|
171
|
+
describe_instance![:aws_state] == 'running'
|
172
|
+
end
|
173
|
+
def describe_instance!
|
174
|
+
@describe_instance = nil
|
175
|
+
logger.debug describe_instance
|
176
|
+
describe_instance
|
177
|
+
end
|
178
|
+
def describe_instance
|
179
|
+
@describe_instance ||= ec2.describe_instance
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def constant (symbol)
|
184
|
+
{
|
185
|
+
:us_east_1a => "us-east-1a",
|
186
|
+
:us_east_1b => "us-east-1b",
|
187
|
+
:us_east_1c => "us-east-1c",
|
188
|
+
:us_west_1a => "us-west-1a",
|
189
|
+
:us_west_1b => "us-west-1b",
|
190
|
+
:eu_west_1a => "eu-west-1a",
|
191
|
+
:eu_west_1b => "eu-west-1b",
|
192
|
+
:m1_small => "m1.small",
|
193
|
+
:m1_large => "m1.large" ,
|
194
|
+
:m1_xlarge => "m1.xlarge",
|
195
|
+
:m2_2xlarge => "m2.2xlarge",
|
196
|
+
:m2_4xlarge => "m2.4xlarge",
|
197
|
+
:c1_medium => "c1.medium",
|
198
|
+
:c1_xlarge => "c1.xlarge"
|
199
|
+
}[symbol]
|
200
|
+
end
|
201
|
+
|
202
|
+
def logger
|
203
|
+
@logger ||= self.class.logger
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Awsborn
|
2
|
+
class ServerCluster
|
3
|
+
def self.build (klass, &block)
|
4
|
+
cluster = new(klass)
|
5
|
+
block.bind(cluster, 'cluster').call
|
6
|
+
cluster
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize (klass)
|
10
|
+
@klass = klass
|
11
|
+
@instances = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def domain (*args)
|
15
|
+
@domain = args.first unless args.empty?
|
16
|
+
@domain
|
17
|
+
end
|
18
|
+
|
19
|
+
def server (name, options = {})
|
20
|
+
options = add_domain_to_ip(options)
|
21
|
+
instance = @klass.new name, options
|
22
|
+
@instances << instance
|
23
|
+
end
|
24
|
+
|
25
|
+
def launch
|
26
|
+
start_missing_instances
|
27
|
+
end
|
28
|
+
|
29
|
+
def start_missing_instances
|
30
|
+
to_start = find_missing_instances
|
31
|
+
return if to_start.empty?
|
32
|
+
generate_key_pair(to_start)
|
33
|
+
to_start.each { |e| e.start(@key_pair) }
|
34
|
+
delete_key_pair(to_start)
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_missing_instances
|
38
|
+
@instances.reject { |e| e.running? }
|
39
|
+
end
|
40
|
+
|
41
|
+
def generate_key_pair (instances)
|
42
|
+
@key_pair = instances.first.ec2.generate_key_pair
|
43
|
+
end
|
44
|
+
|
45
|
+
def delete_key_pair (instances)
|
46
|
+
instances.first.ec2.delete_key_pair(@key_pair)
|
47
|
+
end
|
48
|
+
|
49
|
+
def each (&block)
|
50
|
+
@instances.each &block
|
51
|
+
end
|
52
|
+
|
53
|
+
def [] (name)
|
54
|
+
@instances.detect { |i| i.name == name }
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def add_domain_to_ip (hash)
|
60
|
+
if @domain && hash.has_key?(:ip) && ! hash[:ip].include?('.')
|
61
|
+
ip = [hash[:ip], @domain].join('.')
|
62
|
+
hash.merge(:ip => ip)
|
63
|
+
else
|
64
|
+
hash
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
data/lib/awsborn.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'resolv'
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'right_aws'
|
7
|
+
|
8
|
+
# %w[
|
9
|
+
# awsborn
|
10
|
+
# server
|
11
|
+
# server_cluster
|
12
|
+
# extensions/proc
|
13
|
+
# ].each { |e| require File.dirname(__FILE__) + "/awsborn/#{e}" }
|
14
|
+
|
15
|
+
require 'pp'
|
16
|
+
|
17
|
+
Dir[File.join(File.dirname(__FILE__), 'awsborn/**/*.rb')].sort.each { |lib| require lib }
|
data/spec/server_spec.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
class SampleServer < Awsborn::Server
|
4
|
+
instance_type :m1_small
|
5
|
+
image_id 'ami-2fc2e95b'
|
6
|
+
keys :all
|
7
|
+
end
|
8
|
+
|
9
|
+
describe Awsborn::Server do
|
10
|
+
before(:each) do
|
11
|
+
@server = SampleServer.new :sample, :zone => :eu_west_1a, :disk => {:sdf => "vol-aaaaaaaa"}
|
12
|
+
end
|
13
|
+
context "first of all" do
|
14
|
+
it "should have a connection to the EU service point"
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
context "#launch when not started" do
|
19
|
+
before(:each) do
|
20
|
+
@server.stub(:running?).and_return(false)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
require 'awsborn'
|
4
|
+
require 'spec'
|
5
|
+
require 'spec/autorun'
|
6
|
+
|
7
|
+
require 'rubygems'
|
8
|
+
require 'webmock/rspec'
|
9
|
+
include WebMock
|
10
|
+
WebMock.disable_net_connect!
|
11
|
+
|
12
|
+
Spec::Runner.configure do |config|
|
13
|
+
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: awsborn
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- David Vrensk
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-04-13 00:00:00 +02:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: right_aws
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 1
|
29
|
+
- 10
|
30
|
+
- 0
|
31
|
+
version: 1.10.0
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: rspec
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 1
|
43
|
+
- 2
|
44
|
+
- 9
|
45
|
+
version: 1.2.9
|
46
|
+
type: :development
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: webmock
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
segments:
|
56
|
+
- 0
|
57
|
+
- 9
|
58
|
+
- 1
|
59
|
+
version: 0.9.1
|
60
|
+
type: :development
|
61
|
+
version_requirements: *id003
|
62
|
+
description: Awsborn defines servers as instances with a certain disk volume, which makes it easy to restart missing servers.
|
63
|
+
email: david@icehouse.se
|
64
|
+
executables: []
|
65
|
+
|
66
|
+
extensions: []
|
67
|
+
|
68
|
+
extra_rdoc_files:
|
69
|
+
- LICENSE
|
70
|
+
- README.mdown
|
71
|
+
files:
|
72
|
+
- .document
|
73
|
+
- .gitignore
|
74
|
+
- LICENSE
|
75
|
+
- README.mdown
|
76
|
+
- Rakefile
|
77
|
+
- VERSION
|
78
|
+
- lib/awsborn.rb
|
79
|
+
- lib/awsborn/awsborn.rb
|
80
|
+
- lib/awsborn/ec2.rb
|
81
|
+
- lib/awsborn/extensions/object.rb
|
82
|
+
- lib/awsborn/extensions/proc.rb
|
83
|
+
- lib/awsborn/known_hosts_updater.rb
|
84
|
+
- lib/awsborn/server.rb
|
85
|
+
- lib/awsborn/server_cluster.rb
|
86
|
+
- spec/awsborn_spec.rb
|
87
|
+
- spec/server_spec.rb
|
88
|
+
- spec/spec.opts
|
89
|
+
- spec/spec_helper.rb
|
90
|
+
has_rdoc: true
|
91
|
+
homepage: http://github.com/icehouse/awsborn
|
92
|
+
licenses: []
|
93
|
+
|
94
|
+
post_install_message:
|
95
|
+
rdoc_options:
|
96
|
+
- --charset=UTF-8
|
97
|
+
require_paths:
|
98
|
+
- lib
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
segments:
|
104
|
+
- 0
|
105
|
+
version: "0"
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
segments:
|
111
|
+
- 0
|
112
|
+
version: "0"
|
113
|
+
requirements: []
|
114
|
+
|
115
|
+
rubyforge_project:
|
116
|
+
rubygems_version: 1.3.6
|
117
|
+
signing_key:
|
118
|
+
specification_version: 3
|
119
|
+
summary: Awsborn lets you define and launch a server cluster on Amazon EC2.
|
120
|
+
test_files:
|
121
|
+
- spec/awsborn_spec.rb
|
122
|
+
- spec/server_spec.rb
|
123
|
+
- spec/spec_helper.rb
|