awsborn 0.1.0
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.
- 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
|