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