awsborn 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
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
@@ -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 }
@@ -0,0 +1,7 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Awsborn" do
4
+ it "fails" do
5
+ fail "hey buddy, you should probably rename this file and start specing for real"
6
+ end
7
+ end
@@ -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
@@ -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