bguthrie-awsymandias 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/README.rdoc +53 -0
- data/Rakefile +31 -0
- data/VERSION +1 -0
- data/lib/awsymandias.rb +316 -0
- data/spec/awsymandias_spec.rb +678 -0
- metadata +97 -0
data/.gitignore
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
= Awsymandias
|
2
|
+
|
3
|
+
== Description
|
4
|
+
|
5
|
+
Awsymandias is a library that helps you set up and tear down complicated AWS environments. In addition to offering a clean, fluent domain model for working with instances and groups of instances it allows you to persist role-to-instance-id mappings to SimpleDB, allowing you to manage your stack from multiple different machines. Eventually it will allow you to add arbitrary instance metadata and help you track instance running costs over time.
|
6
|
+
|
7
|
+
== Example
|
8
|
+
|
9
|
+
# Give the stack a name, and describe its members.
|
10
|
+
stack = Awsymandias::EC2::ApplicationStack.new("test") do |s|
|
11
|
+
s.role "db", :instance_type => "m1.large", ...
|
12
|
+
s.role "app", :instance_type => "c1.xlarge", ...
|
13
|
+
end
|
14
|
+
|
15
|
+
# Check if we're running by pulling stack description from SDB; if not, launch asynchronously.
|
16
|
+
stack.launch unless stack.running?
|
17
|
+
until stack.running?
|
18
|
+
sleep(5)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Capistrano
|
22
|
+
task :test do
|
23
|
+
set :db, stack.db.public_dns
|
24
|
+
set :app, stack.app.public_dns
|
25
|
+
end
|
26
|
+
|
27
|
+
This should allow you to re-launch and deploy that AWS stack from any one of several different workstations.
|
28
|
+
|
29
|
+
== License
|
30
|
+
|
31
|
+
(The MIT License)
|
32
|
+
|
33
|
+
Copyright (c) 2009 Brian Guthrie (btguthrie@gmail.com)
|
34
|
+
|
35
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
36
|
+
a copy of this software and associated documentation files (the
|
37
|
+
"Software"), to deal in the Software without restriction, including
|
38
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
39
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
40
|
+
permit persons to whom the Software is furnished to do so, subject to
|
41
|
+
the following conditions:
|
42
|
+
|
43
|
+
The above copyright notice and this permission notice shall be
|
44
|
+
included in all copies or substantial portions of the Software.
|
45
|
+
|
46
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
47
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
48
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
49
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
50
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
51
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
52
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
53
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'spec/rake/spectask'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'jeweler'
|
5
|
+
|
6
|
+
# task :spec
|
7
|
+
Spec::Rake::SpecTask.new do |t|
|
8
|
+
t.rcov = true
|
9
|
+
t.rcov_opts = ["--text-summary", "--include-file lib/awsymandias", "--exclude gems,spec"]
|
10
|
+
end
|
11
|
+
|
12
|
+
task :default => [:spec]
|
13
|
+
|
14
|
+
desc "Open an irb session preloaded with this library"
|
15
|
+
task :console do
|
16
|
+
sh "irb -rubygems -I lib -r awsymandias"
|
17
|
+
end
|
18
|
+
|
19
|
+
Jeweler::Tasks.new do |s|
|
20
|
+
s.name = "awsymandias"
|
21
|
+
s.summary = "A library for helping you set up, track, and tear down complicated deployment configurations in Amazon EC2."
|
22
|
+
s.email = "btguthrie@gmail.com"
|
23
|
+
s.homepage = "http://github.com/bguthrie/awsymandias"
|
24
|
+
s.description = "A library for helping you set up, track, and tear down complicated deployment configurations in Amazon EC2."
|
25
|
+
s.authors = ["Brian Guthrie"]
|
26
|
+
|
27
|
+
s.add_dependency 'activesupport', '>= 2.3.0'
|
28
|
+
s.add_dependency 'activeresource', '>= 2.3.0'
|
29
|
+
s.add_dependency 'grempe-amazon-ec2', '>= 0.4.2'
|
30
|
+
s.add_dependency 'money', '>= 2.1.3'
|
31
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.1
|
data/lib/awsymandias.rb
ADDED
@@ -0,0 +1,316 @@
|
|
1
|
+
Dir[File.dirname(__FILE__) + "/../vendor/**/lib"].each { |dir| $: << dir }
|
2
|
+
|
3
|
+
require 'EC2'
|
4
|
+
require 'aws_sdb'
|
5
|
+
require 'money'
|
6
|
+
require 'activesupport'
|
7
|
+
require 'activeresource'
|
8
|
+
|
9
|
+
module Awsymandias
|
10
|
+
class << self
|
11
|
+
attr_writer :access_key_id, :secret_access_key
|
12
|
+
|
13
|
+
def access_key_id
|
14
|
+
@access_key_id || AMAZON_ACCESS_KEY_ID || ENV['AMAZON_ACCESS_KEY_ID']
|
15
|
+
end
|
16
|
+
|
17
|
+
def secret_access_key
|
18
|
+
@secret_access_key || AMAZON_SECRET_ACCESS_KEY || ENV['AMAZON_SECRET_ACCESS_KEY']
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module EC2
|
23
|
+
class << self
|
24
|
+
# Define the values for AMAZON_ACCESS_KEY_ID and AMAZON_SECRET_ACCESS_KEY_ID to allow for automatic
|
25
|
+
# connection creation.
|
26
|
+
def connection
|
27
|
+
@connection ||= ::EC2::Base.new(
|
28
|
+
:access_key_id => Awsymandias.access_key_id || ENV['AMAZON_ACCESS_KEY_ID'],
|
29
|
+
:secret_access_key => Awsymandias.secret_access_key || ENV['AMAZON_SECRET_ACCESS_KEY']
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
def instance_types
|
34
|
+
[
|
35
|
+
Awsymandias::EC2::InstanceTypes::M1_SMALL,
|
36
|
+
Awsymandias::EC2::InstanceTypes::M1_LARGE,
|
37
|
+
Awsymandias::EC2::InstanceTypes::M1_XLARGE,
|
38
|
+
Awsymandias::EC2::InstanceTypes::C1_MEDIUM,
|
39
|
+
Awsymandias::EC2::InstanceTypes::C1_XLARGE
|
40
|
+
].index_by(&:name)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
InstanceType = Struct.new(:name, :price_per_hour)
|
45
|
+
|
46
|
+
# All currently available instance types.
|
47
|
+
# TODO Generate dynamically.
|
48
|
+
module InstanceTypes
|
49
|
+
M1_SMALL = InstanceType.new("m1.small", Money.new(10))
|
50
|
+
M1_LARGE = InstanceType.new("m1.large", Money.new(40))
|
51
|
+
M1_XLARGE = InstanceType.new("m1.xlarge", Money.new(80))
|
52
|
+
|
53
|
+
C1_MEDIUM = InstanceType.new("c1.medium", Money.new(20))
|
54
|
+
C1_XLARGE = InstanceType.new("c1.xlarge", Money.new(80))
|
55
|
+
end
|
56
|
+
|
57
|
+
# All currently availability zones.
|
58
|
+
# TODO Generate dynamically.
|
59
|
+
module AvailabilityZones
|
60
|
+
US_EAST_1A = "us_east_1a"
|
61
|
+
US_EAST_1B = "us_east_1b"
|
62
|
+
US_EAST_1C = "us_east_1c"
|
63
|
+
|
64
|
+
EU_WEST_1A = "eu_west_1a"
|
65
|
+
EU_WEST_1B = "eu_west_1b"
|
66
|
+
end
|
67
|
+
|
68
|
+
# An instance represents an AWS instance as derived from a call to EC2's describe-instances methods.
|
69
|
+
# It wraps the simple hash structures returned by the EC2 gem with a domain model.
|
70
|
+
# It inherits from ARes::B in order to provide simple XML <-> domain model mapping.
|
71
|
+
class Instance < ActiveResource::Base
|
72
|
+
include ActiveSupport::CoreExtensions::Hash::Conversions::ClassMethods
|
73
|
+
extend ActiveSupport::CoreExtensions::Hash::Conversions::ClassMethods # unrename_keys
|
74
|
+
|
75
|
+
self.site = "mu"
|
76
|
+
|
77
|
+
def id; instance_id; end
|
78
|
+
def public_dns; dns_name; end
|
79
|
+
def private_dns; private_dns_name; end
|
80
|
+
|
81
|
+
def pending?
|
82
|
+
instance_state.name == "pending"
|
83
|
+
end
|
84
|
+
|
85
|
+
def running?
|
86
|
+
instance_state.name == "running"
|
87
|
+
end
|
88
|
+
|
89
|
+
def terminated?
|
90
|
+
instance_state.name == "terminated"
|
91
|
+
end
|
92
|
+
|
93
|
+
def terminate!
|
94
|
+
Awsymandias::EC2.connection.terminate_instances :instance_id => self.instance_id
|
95
|
+
reload
|
96
|
+
end
|
97
|
+
|
98
|
+
def reload
|
99
|
+
load(unrename_keys(
|
100
|
+
EC2.connection.describe_instances(:instance_id => [ self.instance_id ])["reservationSet"]["item"].
|
101
|
+
first["instancesSet"]["item"].
|
102
|
+
first # Good lord.
|
103
|
+
))
|
104
|
+
end
|
105
|
+
|
106
|
+
def to_params
|
107
|
+
{
|
108
|
+
:image_id => self.image_id,
|
109
|
+
:key_name => self.key_name,
|
110
|
+
:instance_type => self.instance_type,
|
111
|
+
:availability_zone => self.placement.availability_zone
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
def instance_type
|
116
|
+
Awsymandias::EC2.instance_types[@attributes['instance_type']]
|
117
|
+
end
|
118
|
+
|
119
|
+
def launch_time
|
120
|
+
Time.parse(@attributes['launch_time'])
|
121
|
+
end
|
122
|
+
|
123
|
+
def uptime
|
124
|
+
return 0.seconds if pending?
|
125
|
+
Time.now - self.launch_time
|
126
|
+
end
|
127
|
+
|
128
|
+
def running_cost
|
129
|
+
return Money.new(0) if pending?
|
130
|
+
instance_type.price_per_hour * (uptime / 1.hour).ceil
|
131
|
+
end
|
132
|
+
|
133
|
+
class << self
|
134
|
+
def find(*args)
|
135
|
+
opts = args.extract_options!
|
136
|
+
what = args.first
|
137
|
+
|
138
|
+
if what == :all
|
139
|
+
find_all(opts[:instance_ids], opts)
|
140
|
+
else
|
141
|
+
find_one(what, opts)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def find_all(ids, opts={})
|
146
|
+
reservation_set = EC2.connection.describe_instances(:instance_id => ids)["reservationSet"]
|
147
|
+
if reservation_set.nil?
|
148
|
+
[]
|
149
|
+
else
|
150
|
+
reservation_set["item"].first["instancesSet"]["item"].map do |item|
|
151
|
+
instantiate_record(unrename_keys(item))
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def find_one(id, opts={})
|
157
|
+
reservation_set = EC2.connection.describe_instances(:instance_id => [ id ])["reservationSet"]
|
158
|
+
if reservation_set.nil?
|
159
|
+
raise ActiveResource::ResourceNotFound, "not found: #{id}"
|
160
|
+
else
|
161
|
+
reservation_set["item"].first["instancesSet"]["item"].map do |item|
|
162
|
+
instantiate_record(unrename_keys(item))
|
163
|
+
end.first
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def launch(opts={})
|
168
|
+
opts.assert_valid_keys! :image_id, :key_name, :instance_type, :availability_zone, :user_data
|
169
|
+
|
170
|
+
opts[:instance_type] = opts[:instance_type].name if opts[:instance_type].is_a?(Awsymandias::EC2::InstanceType)
|
171
|
+
|
172
|
+
response = Awsymandias::EC2.connection.run_instances opts
|
173
|
+
instance_id = response["instancesSet"]["item"].map {|h| h["instanceId"]}.first
|
174
|
+
find(instance_id)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Goal:
|
180
|
+
# stack = EC2::ApplicationStack.new do |stack|
|
181
|
+
# stack.role "db", :instance_type => EC2::InstanceTypes::C1_XLARGE, :image_id => "ami-3576915c"
|
182
|
+
# stack.role "app1", "app2", "app3", :instance_type => EC2::InstanceType::M1_XLARGE, :image_id => "ami-dc789fb5"
|
183
|
+
# stack.role "memcache", :instance_type => EC2::InstanceType::C1_LARGE, :image_id => "ami-dc789fb5"
|
184
|
+
# end
|
185
|
+
# stack.app1.running?
|
186
|
+
class ApplicationStack
|
187
|
+
attr_reader :name, :roles, :sdb_domain
|
188
|
+
|
189
|
+
DEFAULT_SDB_DOMAIN = "application-stack"
|
190
|
+
|
191
|
+
class << self
|
192
|
+
def find(name)
|
193
|
+
returning(new(name)) do |stack|
|
194
|
+
return nil unless stack.launched?
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def launch(name, opts={})
|
199
|
+
returning(new(name, opts)) do |stack|
|
200
|
+
stack.launch
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def initialize(name, opts={})
|
206
|
+
opts.assert_valid_keys! :roles
|
207
|
+
|
208
|
+
@name = name
|
209
|
+
@roles = opts[:roles] || {}
|
210
|
+
@sdb_domain = opts[:sdb_domain] || DEFAULT_SDB_DOMAIN
|
211
|
+
@instances = {}
|
212
|
+
yield self if block_given?
|
213
|
+
end
|
214
|
+
|
215
|
+
def role(*names)
|
216
|
+
opts = names.extract_options!
|
217
|
+
names.each do |name|
|
218
|
+
@roles[name] = opts
|
219
|
+
self.metaclass.send(:define_method, name) { @instances[name] }
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def launch
|
224
|
+
@roles.each do |name, params| # TODO Optimize this for a single remote call.
|
225
|
+
@instances[name] = Awsymandias::EC2::Instance.launch(params)
|
226
|
+
end
|
227
|
+
store_role_to_instance_id_mapping!
|
228
|
+
self
|
229
|
+
end
|
230
|
+
|
231
|
+
def reload
|
232
|
+
raise "Can't reload unless launched" unless launched?
|
233
|
+
@instances.values.each(&:reload) # TODO Optimize this for a single remote call.
|
234
|
+
self
|
235
|
+
end
|
236
|
+
|
237
|
+
def terminate!
|
238
|
+
@instances.values.each(&:terminate!) # TODO Optimize this for a single remote call.
|
239
|
+
remove_role_to_instance_id_mapping!
|
240
|
+
self
|
241
|
+
end
|
242
|
+
|
243
|
+
def launched?
|
244
|
+
@instances.any? || restore_from_role_to_instance_id_mapping.any?
|
245
|
+
end
|
246
|
+
|
247
|
+
def running?
|
248
|
+
launched? && @instances.values.all?(&:running?)
|
249
|
+
end
|
250
|
+
|
251
|
+
def inspect
|
252
|
+
( [ "Environment #{@name}, running? #{running?}" ] + roles.map do |role_name, opts|
|
253
|
+
"** #{role_name}: #{opts.inspect}"
|
254
|
+
end ).join("\n")
|
255
|
+
end
|
256
|
+
|
257
|
+
private
|
258
|
+
|
259
|
+
def store_role_to_instance_id_mapping!
|
260
|
+
Awsymandias::SimpleDB.put @sdb_domain, @name, ( returning({}) do |h|
|
261
|
+
@instances.each {|role_name, instance| h[role_name] = instance.instance_id}
|
262
|
+
end )
|
263
|
+
end
|
264
|
+
|
265
|
+
def remove_role_to_instance_id_mapping!
|
266
|
+
Awsymandias::SimpleDB.delete @sdb_domain, @name
|
267
|
+
end
|
268
|
+
|
269
|
+
def restore_from_role_to_instance_id_mapping
|
270
|
+
@instances = returning(Awsymandias::SimpleDB.get(@sdb_domain, @name)) do |mapping|
|
271
|
+
unless mapping.empty?
|
272
|
+
live_instances = Awsymandias::EC2::Instance.find(:all, :instance_ids => mapping.values.flatten).index_by(&:instance_id)
|
273
|
+
mapping.each do |role_name, instance_id|
|
274
|
+
mapping[role_name] = live_instances[instance_id.first]
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# TODO Locate a nicer SimpleDB API and get out of the business of maintaining this one.
|
284
|
+
module SimpleDB # :nodoc
|
285
|
+
class << self
|
286
|
+
def connection(opts={})
|
287
|
+
@connection ||= ::AwsSdb::Service.new({
|
288
|
+
:access_key_id => Awsymandias.access_key_id || ENV['AMAZON_ACCESS_KEY_ID'],
|
289
|
+
:secret_access_key => Awsymandias.secret_access_key || ENV['AMAZON_SECRET_ACCESS_KEY']
|
290
|
+
}.merge(opts))
|
291
|
+
end
|
292
|
+
|
293
|
+
def put(domain, name, stuff)
|
294
|
+
connection.put_attributes handle_domain(domain), name, stuff
|
295
|
+
end
|
296
|
+
|
297
|
+
def get(domain, name)
|
298
|
+
connection.get_attributes(handle_domain(domain), name) || {}
|
299
|
+
end
|
300
|
+
|
301
|
+
def delete(domain, name)
|
302
|
+
connection.delete_attributes handle_domain(domain), name
|
303
|
+
end
|
304
|
+
|
305
|
+
private
|
306
|
+
|
307
|
+
def domain_exists?(domain)
|
308
|
+
Awsymandias::SimpleDB.connection.list_domains[0].include?(domain)
|
309
|
+
end
|
310
|
+
|
311
|
+
def handle_domain(domain)
|
312
|
+
returning(domain) { connection.create_domain(domain) unless domain_exists?(domain) }
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
@@ -0,0 +1,678 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'spec'
|
3
|
+
require File.dirname(__FILE__) + "/../lib/awsymandias"
|
4
|
+
|
5
|
+
describe Awsymandias do
|
6
|
+
describe Awsymandias::SimpleDB do
|
7
|
+
describe "connection" do
|
8
|
+
it "configure an instance of AwsSdb::Service" do
|
9
|
+
Awsymandias.access_key_id = "configured key"
|
10
|
+
Awsymandias.secret_access_key = "configured secret"
|
11
|
+
|
12
|
+
::AwsSdb::Service.should_receive(:new).
|
13
|
+
with(hash_including(:access_key_id => "configured key", :secret_access_key => "configured secret")).
|
14
|
+
and_return(:a_connection)
|
15
|
+
|
16
|
+
Awsymandias::SimpleDB.connection.should == :a_connection
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe Awsymandias::EC2 do
|
22
|
+
def stub_connection_with(return_value)
|
23
|
+
Awsymandias::EC2.stub!(:connection).and_return stub("a connection", :describe_instances => return_value)
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "connection" do
|
27
|
+
it "should configure an instance of EC2::Base" do
|
28
|
+
Awsymandias.access_key_id = "configured key"
|
29
|
+
Awsymandias.secret_access_key = "configured secret"
|
30
|
+
|
31
|
+
::EC2::Base.should_receive(:new).
|
32
|
+
with(hash_including(:access_key_id => "configured key", :secret_access_key => "configured secret")).
|
33
|
+
and_return(:a_connection)
|
34
|
+
|
35
|
+
Awsymandias::EC2.connection.should == :a_connection
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe Instance = Awsymandias::EC2::Instance do
|
40
|
+
|
41
|
+
DESCRIBE_INSTANCES_NO_RESULTS_XML = {
|
42
|
+
"requestId" => "7bca5c7c-1b51-473e-a930-611e55920e39",
|
43
|
+
"xmlns"=>"http://ec2.amazonaws.com/doc/2008-12-01/",
|
44
|
+
"reservationSet" => nil
|
45
|
+
}
|
46
|
+
|
47
|
+
DESCRIBE_INSTANCES_SINGLE_RESULT_PENDING_XML = {
|
48
|
+
"requestId" => "7bca5c7c-1b51-473e-a930-611e55920e39",
|
49
|
+
"xmlns"=>"http://ec2.amazonaws.com/doc/2008-12-01/",
|
50
|
+
"reservationSet" => {
|
51
|
+
"item" => [ {
|
52
|
+
"reservationId" => "r-db68e3b2",
|
53
|
+
"requesterId" => "058890971305",
|
54
|
+
"ownerId" => "358110980006",
|
55
|
+
"groupSet" => { "item" => [ { "groupId" => "default" } ] },
|
56
|
+
"instancesSet" => { "item" => [ {
|
57
|
+
"productCodes" => nil,
|
58
|
+
"kernelId" => "aki-some-kernel",
|
59
|
+
"amiLaunchIndex" => "0",
|
60
|
+
"keyName" => "gsg-keypair",
|
61
|
+
"ramdiskId" => "ari-b31cf9da",
|
62
|
+
"launchTime" => "2009-04-20T01:30:35.000Z",
|
63
|
+
"instanceType" => "m1.large",
|
64
|
+
"imageId" => "ami-some-image",
|
65
|
+
"privateDnsName" => nil,
|
66
|
+
"reason" => nil,
|
67
|
+
"placement" => {
|
68
|
+
"availabilityZone" => "us-east-1c"
|
69
|
+
},
|
70
|
+
"dnsName" => nil,
|
71
|
+
"instanceId" => "i-some-instance",
|
72
|
+
"instanceState" => {
|
73
|
+
"name" => "pending",
|
74
|
+
"code"=>"0"
|
75
|
+
} } ] } } ] }
|
76
|
+
}
|
77
|
+
|
78
|
+
DESCRIBE_INSTANCES_SINGLE_RESULT_RUNNING_XML = {
|
79
|
+
"requestId" => "7bca5c7c-1b51-473e-a930-611e55920e39",
|
80
|
+
"xmlns"=>"http://ec2.amazonaws.com/doc/2008-12-01/",
|
81
|
+
"reservationSet" => {
|
82
|
+
"item" => [ {
|
83
|
+
"reservationId" => "r-db68e3b2",
|
84
|
+
"requesterId" => "058890971305",
|
85
|
+
"ownerId" => "358110980006",
|
86
|
+
"groupSet" => { "item" => [ { "groupId" => "default" } ] },
|
87
|
+
"instancesSet" => { "item" => [ {
|
88
|
+
"productCodes" => nil,
|
89
|
+
"kernelId" => "aki-some-kernel",
|
90
|
+
"amiLaunchIndex" => "0",
|
91
|
+
"keyName" => "gsg-keypair",
|
92
|
+
"ramdiskId" => "ari-b31cf9da",
|
93
|
+
"launchTime" => "2009-04-20T01:30:35.000Z",
|
94
|
+
"instanceType" => "m1.large",
|
95
|
+
"imageId" => "ami-some-image",
|
96
|
+
"privateDnsName" => nil,
|
97
|
+
"reason" => nil,
|
98
|
+
"placement" => {
|
99
|
+
"availabilityZone" => "us-east-1c"
|
100
|
+
},
|
101
|
+
"dnsName" => nil,
|
102
|
+
"instanceId" => "i-some-instance",
|
103
|
+
"instanceState" => {
|
104
|
+
"name" => "running",
|
105
|
+
"code"=>"0"
|
106
|
+
} } ] } } ] }
|
107
|
+
}
|
108
|
+
|
109
|
+
DESCRIBE_INSTANCES_MULTIPLE_RESULTS_RUNNING_XML = {
|
110
|
+
"requestId" => "7bca5c7c-1b51-473e-a930-611e55920e39",
|
111
|
+
"xmlns"=>"http://ec2.amazonaws.com/doc/2008-12-01/",
|
112
|
+
"reservationSet" => {
|
113
|
+
"item" => [ {
|
114
|
+
"reservationId" => "r-db68e3b2",
|
115
|
+
"requesterId" => "058890971305",
|
116
|
+
"ownerId" => "358110980006",
|
117
|
+
"groupSet" => { "item" => [ { "groupId" => "default" } ] },
|
118
|
+
"instancesSet" => { "item" => [
|
119
|
+
{
|
120
|
+
"productCodes" => nil,
|
121
|
+
"kernelId" => "aki-some-kernel",
|
122
|
+
"amiLaunchIndex" => "0",
|
123
|
+
"keyName" => "gsg-keypair",
|
124
|
+
"ramdiskId" => "ari-b31cf9da",
|
125
|
+
"launchTime" => "2009-04-20T01:30:35.000Z",
|
126
|
+
"instanceType" => "m1.large",
|
127
|
+
"imageId" => "ami-some-image",
|
128
|
+
"privateDnsName" => nil,
|
129
|
+
"reason" => nil,
|
130
|
+
"placement" => {
|
131
|
+
"availabilityZone" => "us-east-1c"
|
132
|
+
},
|
133
|
+
"dnsName" => nil,
|
134
|
+
"instanceId" => "i-some-instance",
|
135
|
+
"instanceState" => {
|
136
|
+
"name" => "running",
|
137
|
+
"code"=>"0"
|
138
|
+
}
|
139
|
+
},
|
140
|
+
{
|
141
|
+
"productCodes" => nil,
|
142
|
+
"kernelId" => "aki-some-kernel",
|
143
|
+
"amiLaunchIndex" => "0",
|
144
|
+
"keyName" => "gsg-keypair",
|
145
|
+
"ramdiskId" => "ari-b31cf9da",
|
146
|
+
"launchTime" => "2009-04-20T01:30:35.000Z",
|
147
|
+
"instanceType" => "m1.large",
|
148
|
+
"imageId" => "ami-some-image",
|
149
|
+
"privateDnsName" => nil,
|
150
|
+
"reason" => nil,
|
151
|
+
"placement" => {
|
152
|
+
"availabilityZone" => "us-east-1c"
|
153
|
+
},
|
154
|
+
"dnsName" => nil,
|
155
|
+
"instanceId" => "i-another-instance",
|
156
|
+
"instanceState" => {
|
157
|
+
"name" => "pending",
|
158
|
+
"code"=>"0"
|
159
|
+
}
|
160
|
+
} ] } } ] }
|
161
|
+
}
|
162
|
+
|
163
|
+
RUN_INSTANCES_SINGLE_RESULT_XML = {
|
164
|
+
"reservationId" => "r-276ee54e",
|
165
|
+
"groupSet" => { "item" => [ {
|
166
|
+
"groupId" => "default"
|
167
|
+
} ] },
|
168
|
+
"requestId" => "a29db909-d8ef-4a14-80c1-c53157c0cd49",
|
169
|
+
"instancesSet" => {
|
170
|
+
"item" => [ {
|
171
|
+
"kernelId" => "aki-some-kernel",
|
172
|
+
"amiLaunchIndex" => "0",
|
173
|
+
"keyName" => "gsg-keypair",
|
174
|
+
"ramdiskId" => "ari-b31cf9da",
|
175
|
+
"launchTime" => "2009-04-20T01:39:12.000Z",
|
176
|
+
"instanceType" => "m1.large",
|
177
|
+
"imageId" => "ami-some-image",
|
178
|
+
"privateDnsName" => nil,
|
179
|
+
"reason" => nil,
|
180
|
+
"placement" => {
|
181
|
+
"availabilityZone" => "us-east-1a"
|
182
|
+
},
|
183
|
+
"dnsName" => nil,
|
184
|
+
"instanceId" => "i-some-instance",
|
185
|
+
"instanceState" => {
|
186
|
+
"name" => "pending",
|
187
|
+
"code" => "0"
|
188
|
+
}
|
189
|
+
} ] },
|
190
|
+
"ownerId"=>"358110980006",
|
191
|
+
"xmlns"=>"http://ec2.amazonaws.com/doc/2008-12-01/"
|
192
|
+
}
|
193
|
+
|
194
|
+
TERMINATE_INSTANCES_SINGLE_RESULT_XML = {
|
195
|
+
"requestId" => "c80c4770-eaab-45ce-972d-10e928e3f80c",
|
196
|
+
"instancesSet" => {
|
197
|
+
"item" => [ {
|
198
|
+
"previousState" => {
|
199
|
+
"name" => "running",
|
200
|
+
"code"=>"16"
|
201
|
+
},
|
202
|
+
"shutdownState" => {
|
203
|
+
"name" => "shutting-down",
|
204
|
+
"code" => "32"
|
205
|
+
},
|
206
|
+
"instanceId" => "i-some-instance" } ] },
|
207
|
+
"xmlns"=>"http://ec2.amazonaws.com/doc/2008-12-01/"
|
208
|
+
}
|
209
|
+
|
210
|
+
DESCRIBE_INSTANCES_SINGLE_RESULT_TERMINATED_XML = {
|
211
|
+
"requestId" => "8b4fb505-de40-41b2-b18e-58f9bcba6f09",
|
212
|
+
"reservationSet" => {
|
213
|
+
"item" => [ {
|
214
|
+
"reservationId" => "r-75961c1c",
|
215
|
+
"groupSet" => {
|
216
|
+
"item" => [ {
|
217
|
+
"groupId" => "default"
|
218
|
+
} ]
|
219
|
+
},
|
220
|
+
"instancesSet" => {
|
221
|
+
"item" => [ {
|
222
|
+
"productCodes" => nil,
|
223
|
+
"kernelId" => "aki-some-kernel",
|
224
|
+
"amiLaunchIndex" => "0",
|
225
|
+
"keyName" => "gsg-keypair",
|
226
|
+
"ramdiskId" => "ari-b31cf9da",
|
227
|
+
"launchTime" => "2009-04-22T00:54:06.000Z",
|
228
|
+
"instanceType" => "c1.xlarge",
|
229
|
+
"imageId" => "ami-some-image",
|
230
|
+
"privateDnsName" => nil,
|
231
|
+
"reason" => "User initiated (2009-04-22 00:59:53 GMT)",
|
232
|
+
"placement" => {
|
233
|
+
"availabilityZone" => nil
|
234
|
+
},
|
235
|
+
"dnsName" => nil,
|
236
|
+
"instanceId" => "i-some-instance",
|
237
|
+
"instanceState" => {
|
238
|
+
"name" => "terminated",
|
239
|
+
"code" => "48"
|
240
|
+
}
|
241
|
+
} ]
|
242
|
+
},
|
243
|
+
"ownerId" => "358110980006"
|
244
|
+
} ] },
|
245
|
+
"xmlns"=>"http://ec2.amazonaws.com/doc/2008-12-01/"
|
246
|
+
}
|
247
|
+
|
248
|
+
describe "find" do
|
249
|
+
it "should raise ActiveResource::ResourceNotFound if the given instance ID is not found" do
|
250
|
+
stub_connection_with DESCRIBE_INSTANCES_NO_RESULTS_XML
|
251
|
+
lambda do
|
252
|
+
Instance.find("i-some-instance")
|
253
|
+
end.should raise_error(ActiveResource::ResourceNotFound)
|
254
|
+
end
|
255
|
+
|
256
|
+
it "should return an object with the appropriate instance ID when an instance with the given ID is found" do
|
257
|
+
stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_RUNNING_XML
|
258
|
+
Instance.find("i-some-instance").instance_id.should == "i-some-instance"
|
259
|
+
end
|
260
|
+
|
261
|
+
it "should return more than one object if multiple IDs are requested" do
|
262
|
+
stub_connection_with DESCRIBE_INSTANCES_MULTIPLE_RESULTS_RUNNING_XML
|
263
|
+
Instance.find(:all, :instance_ids => ["i-some-instance", "i-another-instance"]).map(&:instance_id).should == [ "i-some-instance", "i-another-instance" ]
|
264
|
+
end
|
265
|
+
|
266
|
+
it "should map camelized XML properties to Ruby-friendly underscored method names" do
|
267
|
+
stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_RUNNING_XML
|
268
|
+
instance = Instance.find("i-some-instance")
|
269
|
+
instance.image_id.should == "ami-some-image"
|
270
|
+
instance.key_name.should == "gsg-keypair"
|
271
|
+
instance.instance_type.should == Awsymandias::EC2.instance_types["m1.large"]
|
272
|
+
instance.placement.availability_zone.should == "us-east-1c"
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
describe "to_params" do
|
277
|
+
it "should be able to reproduce a reasonable set of its launch params as a hash" do
|
278
|
+
stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_RUNNING_XML
|
279
|
+
Instance.find("i-some-instance").to_params.should == {
|
280
|
+
:image_id => "ami-some-image",
|
281
|
+
:key_name => "gsg-keypair",
|
282
|
+
:instance_type => Awsymandias::EC2.instance_types["m1.large"],
|
283
|
+
:availability_zone => "us-east-1c"
|
284
|
+
}
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
describe "running?" do
|
289
|
+
it "should return false if it contains an instances set with the given instance ID and its state is pending" do
|
290
|
+
stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_PENDING_XML
|
291
|
+
Instance.find("i-some-instance").should_not be_running
|
292
|
+
end
|
293
|
+
|
294
|
+
it "should return true if it contains an instances set with the given instance ID and its state is running" do
|
295
|
+
stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_RUNNING_XML
|
296
|
+
Instance.find("i-some-instance").should be_running
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
describe "reload" do
|
301
|
+
it "should reload an instance without replacing the object" do
|
302
|
+
stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_PENDING_XML
|
303
|
+
instance = Instance.find("i-some-instance")
|
304
|
+
instance.should_not be_running
|
305
|
+
|
306
|
+
stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_RUNNING_XML
|
307
|
+
instance.reload.should be_running
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
describe "launch" do
|
312
|
+
it "should launch a new instance given some values" do
|
313
|
+
mock_connection = mock("a connection")
|
314
|
+
mock_connection.should_receive(:run_instances).with(hash_including(
|
315
|
+
:image_id => "an_id",
|
316
|
+
:key_name => "gsg-keypair",
|
317
|
+
:instance_type => "m1.small",
|
318
|
+
:availability_zone => Awsymandias::EC2::AvailabilityZones::US_EAST_1A
|
319
|
+
)).and_return(RUN_INSTANCES_SINGLE_RESULT_XML)
|
320
|
+
|
321
|
+
mock_connection.should_receive(:describe_instances).and_return(DESCRIBE_INSTANCES_SINGLE_RESULT_PENDING_XML)
|
322
|
+
|
323
|
+
Awsymandias::EC2.stub!(:connection).and_return mock_connection
|
324
|
+
|
325
|
+
Awsymandias::EC2::Instance.launch(
|
326
|
+
:image_id => "an_id",
|
327
|
+
:key_name => "gsg-keypair",
|
328
|
+
:instance_type => Awsymandias::EC2::InstanceTypes::M1_SMALL,
|
329
|
+
:availability_zone => Awsymandias::EC2::AvailabilityZones::US_EAST_1A
|
330
|
+
).instance_id.should == "i-some-instance"
|
331
|
+
end
|
332
|
+
|
333
|
+
it "should convert the instance type it's given to a string as needed" do
|
334
|
+
mock_connection = mock("a connection")
|
335
|
+
mock_connection.should_receive(:run_instances).with(hash_including(
|
336
|
+
:instance_type => "m1.small"
|
337
|
+
)).and_return(RUN_INSTANCES_SINGLE_RESULT_XML)
|
338
|
+
mock_connection.should_receive(:describe_instances).and_return(stub("response").as_null_object)
|
339
|
+
Awsymandias::EC2.stub!(:connection).and_return mock_connection
|
340
|
+
|
341
|
+
Awsymandias::EC2::Instance.launch(:instance_type => Awsymandias::EC2::InstanceTypes::M1_SMALL)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
describe "terminate!" do
|
346
|
+
it "should terminate a running instance" do
|
347
|
+
mock_connection = mock("a connection")
|
348
|
+
mock_connection.should_receive(:describe_instances).and_return(
|
349
|
+
DESCRIBE_INSTANCES_SINGLE_RESULT_RUNNING_XML,
|
350
|
+
DESCRIBE_INSTANCES_SINGLE_RESULT_TERMINATED_XML
|
351
|
+
)
|
352
|
+
mock_connection.should_receive(:terminate_instances).and_return(
|
353
|
+
TERMINATE_INSTANCES_SINGLE_RESULT_XML
|
354
|
+
)
|
355
|
+
|
356
|
+
Awsymandias::EC2.stub!(:connection).and_return mock_connection
|
357
|
+
|
358
|
+
instance = Awsymandias::EC2::Instance.find("a result id")
|
359
|
+
instance.should be_running
|
360
|
+
instance.terminate!
|
361
|
+
instance.should_not be_running
|
362
|
+
instance.should be_terminated
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
describe "instance_type" do
|
367
|
+
it "should return its instance_type attribute as an InstanceType object" do
|
368
|
+
stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_RUNNING_XML
|
369
|
+
Instance.find("i-some-instance").instance_type.should == Awsymandias::EC2::InstanceTypes::M1_LARGE
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
describe "launch_time" do
|
374
|
+
it "should return its launch_time attribute as an instance of Time" do
|
375
|
+
stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_PENDING_XML
|
376
|
+
Awsymandias::EC2::Instance.find("i-some-instance").launch_time.should == Time.parse("2009-04-20T01:30:35.000Z")
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
describe "uptime" do
|
381
|
+
it "should be zero seconds if it is not yet running" do
|
382
|
+
stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_PENDING_XML
|
383
|
+
Awsymandias::EC2::Instance.find("i-some-instance").uptime.should == 0.seconds
|
384
|
+
end
|
385
|
+
|
386
|
+
it "should calculate the uptime of a running instance in terms of its launch time" do
|
387
|
+
time_now = Time.now
|
388
|
+
Time.stub!(:now).and_return time_now
|
389
|
+
stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_RUNNING_XML
|
390
|
+
instance = Awsymandias::EC2::Instance.find("i-some-instance")
|
391
|
+
instance.uptime.should == (time_now - instance.launch_time)
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
describe "running_cost" do
|
396
|
+
it "should be zero if the instance has not yet been launched" do
|
397
|
+
stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_PENDING_XML
|
398
|
+
Awsymandias::EC2::Instance.find("i-some-instance").running_cost.should == Money.new(0)
|
399
|
+
end
|
400
|
+
|
401
|
+
it "should be a single increment if the instance was launched 5 minutes ago" do
|
402
|
+
stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_RUNNING_XML
|
403
|
+
instance = Awsymandias::EC2::Instance.find("i-some-instance")
|
404
|
+
instance.attributes['launch_time'] = 5.minutes.ago.to_s
|
405
|
+
expected_cost = instance.instance_type.price_per_hour
|
406
|
+
instance.running_cost.should == expected_cost
|
407
|
+
end
|
408
|
+
|
409
|
+
it "should be a single increment if the instance was launched 59 minutes ago" do
|
410
|
+
stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_RUNNING_XML
|
411
|
+
instance = Awsymandias::EC2::Instance.find("i-some-instance")
|
412
|
+
instance.attributes['launch_time'] = 59.minutes.ago.to_s
|
413
|
+
expected_cost = instance.instance_type.price_per_hour
|
414
|
+
instance.running_cost.should == expected_cost
|
415
|
+
end
|
416
|
+
|
417
|
+
it "should be two increments if the instance was launched 61 minutes ago" do
|
418
|
+
stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_RUNNING_XML
|
419
|
+
instance = Awsymandias::EC2::Instance.find("i-some-instance")
|
420
|
+
instance.attributes['launch_time'] = 61.minutes.ago.to_s
|
421
|
+
expected_cost = instance.instance_type.price_per_hour * 2
|
422
|
+
instance.running_cost.should == expected_cost
|
423
|
+
end
|
424
|
+
|
425
|
+
it "should be three increments if the instance was launched 150 minutes ago" do
|
426
|
+
stub_connection_with DESCRIBE_INSTANCES_SINGLE_RESULT_RUNNING_XML
|
427
|
+
instance = Awsymandias::EC2::Instance.find("i-some-instance")
|
428
|
+
instance.attributes['launch_time'] = 150.minutes.ago.to_s
|
429
|
+
expected_cost = instance.instance_type.price_per_hour * 3
|
430
|
+
instance.running_cost.should == expected_cost
|
431
|
+
end
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
describe ApplicationStack = Awsymandias::EC2::ApplicationStack do
|
436
|
+
class SimpleDBStub
|
437
|
+
def initialize
|
438
|
+
@store = {}
|
439
|
+
end
|
440
|
+
|
441
|
+
def list_domains
|
442
|
+
[ @store.keys ]
|
443
|
+
end
|
444
|
+
|
445
|
+
def put_attributes(domain, name, attributes)
|
446
|
+
@store[domain][name] = attributes
|
447
|
+
end
|
448
|
+
|
449
|
+
def get_attributes(domain, name)
|
450
|
+
@store[domain][name]
|
451
|
+
end
|
452
|
+
|
453
|
+
def delete_attributes(domain, name)
|
454
|
+
@store[domain][name] = nil
|
455
|
+
end
|
456
|
+
|
457
|
+
def create_domain(domain)
|
458
|
+
@store[domain] = {}
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
attr_accessor :simpledb
|
463
|
+
|
464
|
+
def stub_instance(stubs={})
|
465
|
+
Instance.new({:instance_id => "i-12345a3c"}.merge(stubs))
|
466
|
+
end
|
467
|
+
|
468
|
+
before :each do
|
469
|
+
@simpledb = SimpleDBStub.new
|
470
|
+
Awsymandias::SimpleDB.stub!(:connection).and_return @simpledb
|
471
|
+
end
|
472
|
+
|
473
|
+
it "should have a name" do
|
474
|
+
ApplicationStack.new("foo").name.should == "foo"
|
475
|
+
end
|
476
|
+
|
477
|
+
describe "roles" do
|
478
|
+
it "should be empty by default" do
|
479
|
+
ApplicationStack.new("foo").roles.should be_empty
|
480
|
+
end
|
481
|
+
|
482
|
+
it "should be settable through the initializer" do
|
483
|
+
stack = ApplicationStack.new("foo", :roles => { :app1 => {} })
|
484
|
+
stack.roles[:app1].should == {}
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
describe "role" do
|
489
|
+
it "should allow the definition of a basic, empty role" do
|
490
|
+
stack = ApplicationStack.new("foo") do |s|
|
491
|
+
s.role :app1
|
492
|
+
end
|
493
|
+
stack.roles[:app1].should == {}
|
494
|
+
end
|
495
|
+
|
496
|
+
it "should use the parameters given to the role definition" do
|
497
|
+
stack = ApplicationStack.new("foo") do |s|
|
498
|
+
s.role :app1, :foo => "bar"
|
499
|
+
end
|
500
|
+
stack.roles[:app1].should == { :foo => "bar" }
|
501
|
+
end
|
502
|
+
|
503
|
+
it "should allow for the creation of multiple roles" do
|
504
|
+
stack = ApplicationStack.new("foo") do |s|
|
505
|
+
s.role :app1, :foo => "bar"
|
506
|
+
s.role :app2, :foo => "baz"
|
507
|
+
end
|
508
|
+
stack.roles[:app1].should == { :foo => "bar" }
|
509
|
+
stack.roles[:app2].should == { :foo => "baz" }
|
510
|
+
end
|
511
|
+
|
512
|
+
it "should map multiple roles to the same set of parameters" do
|
513
|
+
stack = ApplicationStack.new("foo") do |s|
|
514
|
+
s.role :app1, :app2, :foo => "bar"
|
515
|
+
end
|
516
|
+
stack.roles[:app1].should == { :foo => "bar" }
|
517
|
+
stack.roles[:app2].should == { :foo => "bar" }
|
518
|
+
end
|
519
|
+
|
520
|
+
it "should create an accessor mapped to the new role, nil by default" do
|
521
|
+
stack = ApplicationStack.new("foo") do |s|
|
522
|
+
s.role :app1, :foo => "bar"
|
523
|
+
end
|
524
|
+
stack.app1.should be_nil
|
525
|
+
end
|
526
|
+
end
|
527
|
+
|
528
|
+
describe "sdb_domain" do
|
529
|
+
it "should map to ApplicationStack::DEFAULT_SDB_DOMAIN upon creation" do
|
530
|
+
ApplicationStack.new("foo").sdb_domain.should == ApplicationStack::DEFAULT_SDB_DOMAIN
|
531
|
+
end
|
532
|
+
|
533
|
+
it "should be configurable" do
|
534
|
+
ApplicationStack.new("foo", :sdb_domain => "a domain").sdb_domain.should == "a domain"
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
538
|
+
describe "launch" do
|
539
|
+
it "should launch its roles when launched" do
|
540
|
+
s = ApplicationStack.new("test") do |s|
|
541
|
+
s.role "db1", :instance_type => Awsymandias::EC2::InstanceTypes::C1_XLARGE
|
542
|
+
s.role "app1", :instance_type => Awsymandias::EC2::InstanceTypes::M1_LARGE
|
543
|
+
end
|
544
|
+
|
545
|
+
Instance.should_receive(:launch).with({ :instance_type => Awsymandias::EC2::InstanceTypes::C1_XLARGE }).and_return(mock("instance1", :instance_id => "a"))
|
546
|
+
Instance.should_receive(:launch).with({ :instance_type => Awsymandias::EC2::InstanceTypes::M1_LARGE }).and_return(mock("instance2", :instance_id => "b"))
|
547
|
+
|
548
|
+
s.launch
|
549
|
+
end
|
550
|
+
|
551
|
+
it "should set the getter for the particular instance to the return value of launching the instance" do
|
552
|
+
s = ApplicationStack.new("test") do |s|
|
553
|
+
s.role "db1", :instance_type => Awsymandias::EC2::InstanceTypes::C1_XLARGE
|
554
|
+
s.role "app1", :instance_type => Awsymandias::EC2::InstanceTypes::M1_LARGE
|
555
|
+
end
|
556
|
+
|
557
|
+
instances = [ stub_instance, stub_instance ]
|
558
|
+
|
559
|
+
Instance.stub!(:launch).with({ :instance_type => Awsymandias::EC2::InstanceTypes::C1_XLARGE }).and_return instances.first
|
560
|
+
Instance.stub!(:launch).with({ :instance_type => Awsymandias::EC2::InstanceTypes::M1_LARGE }).and_return instances.last
|
561
|
+
|
562
|
+
s.db1.should be_nil
|
563
|
+
s.app1.should be_nil
|
564
|
+
|
565
|
+
s.launch
|
566
|
+
|
567
|
+
s.db1.should == instances.first
|
568
|
+
s.app1.should == instances.last
|
569
|
+
end
|
570
|
+
|
571
|
+
it "should store details about the newly launched instances" do
|
572
|
+
Awsymandias::EC2::Instance.stub!(:launch).and_return stub_instance(:instance_id => "abc123")
|
573
|
+
Awsymandias::EC2::ApplicationStack.new("test") do |s|
|
574
|
+
s.role "db1", :instance_type => Awsymandias::EC2::InstanceTypes::C1_XLARGE
|
575
|
+
end.launch
|
576
|
+
|
577
|
+
simpledb.get_attributes(ApplicationStack::DEFAULT_SDB_DOMAIN, "test").should == { "db1" => "abc123" }
|
578
|
+
end
|
579
|
+
end
|
580
|
+
|
581
|
+
describe "launched?" do
|
582
|
+
it "should be false initially" do
|
583
|
+
s = ApplicationStack.new("test") {|s| s.role "db1", :instance_type => Awsymandias::EC2::InstanceTypes::M1_LARGE}
|
584
|
+
s.should_not be_launched
|
585
|
+
end
|
586
|
+
|
587
|
+
it "should be true if launched and instances are non-empty" do
|
588
|
+
s = ApplicationStack.new("test") { |s| s.role "db1" }
|
589
|
+
Awsymandias::EC2::Instance.stub!(:launch).and_return stub_instance
|
590
|
+
s.launch
|
591
|
+
s.should be_launched
|
592
|
+
end
|
593
|
+
|
594
|
+
it "should attempt to determine whether or not it's been previously launched" do
|
595
|
+
Awsymandias::SimpleDB.put ApplicationStack::DEFAULT_SDB_DOMAIN, "test", "db1" => ["instance_id"]
|
596
|
+
an_instance = stub_instance :instance_id => "instance_id"
|
597
|
+
Instance.should_receive(:find).with(:all, :instance_ids => [ "instance_id" ]).and_return [ an_instance ]
|
598
|
+
s = ApplicationStack.new("test") { |s| s.role "db1" }
|
599
|
+
s.should be_launched
|
600
|
+
s.db1.should == an_instance
|
601
|
+
end
|
602
|
+
end
|
603
|
+
|
604
|
+
describe "running?" do
|
605
|
+
it "should be false initially" do
|
606
|
+
ApplicationStack.new("test") {|s| s.role "db1"}.should_not be_running
|
607
|
+
end
|
608
|
+
|
609
|
+
it "should be false if launched but all instances are pended" do
|
610
|
+
Instance.stub!(:launch).and_return stub_instance(:instance_state => { :name => "pending" })
|
611
|
+
ApplicationStack.new("test") {|s| s.role "db1"}.launch.should_not be_running
|
612
|
+
end
|
613
|
+
|
614
|
+
it "should be false if launched and some instances are pended" do
|
615
|
+
s = ApplicationStack.new("test") do |s|
|
616
|
+
s.role "db1", :instance_type => Awsymandias::EC2::InstanceTypes::C1_XLARGE
|
617
|
+
s.role "app1", :instance_type => Awsymandias::EC2::InstanceTypes::M1_LARGE
|
618
|
+
end
|
619
|
+
|
620
|
+
Instance.stub!(:launch).
|
621
|
+
with({ :instance_type => Awsymandias::EC2::InstanceTypes::C1_XLARGE }).
|
622
|
+
and_return stub_instance(:instance_state => { :name => "pending" })
|
623
|
+
|
624
|
+
Instance.stub!(:launch).
|
625
|
+
with({ :instance_type => Awsymandias::EC2::InstanceTypes::M1_LARGE }).
|
626
|
+
and_return stub_instance(:instance_state => { :name => "running" })
|
627
|
+
|
628
|
+
s.launch
|
629
|
+
s.should_not be_running
|
630
|
+
end
|
631
|
+
|
632
|
+
it "should be true if launched and all instances are running" do
|
633
|
+
s = ApplicationStack.new("test") do |s|
|
634
|
+
s.role "db1", :instance_type => Awsymandias::EC2::InstanceTypes::C1_XLARGE
|
635
|
+
s.role "app1", :instance_type => Awsymandias::EC2::InstanceTypes::M1_LARGE
|
636
|
+
end
|
637
|
+
|
638
|
+
Instance.stub!(:launch).
|
639
|
+
with({ :instance_type => Awsymandias::EC2::InstanceTypes::C1_XLARGE }).
|
640
|
+
and_return stub_instance(:instance_state => { :name => "running" })
|
641
|
+
|
642
|
+
Instance.stub!(:launch).
|
643
|
+
with({ :instance_type => Awsymandias::EC2::InstanceTypes::M1_LARGE }).
|
644
|
+
and_return stub_instance(:instance_state => { :name => "running" })
|
645
|
+
|
646
|
+
s.launch
|
647
|
+
s.should be_running
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
describe "terminate!" do
|
652
|
+
it "should not do anything if not running" do
|
653
|
+
s = ApplicationStack.new("test") { |s| s.role "db1" }
|
654
|
+
Awsymandias::EC2.should_receive(:connection).never
|
655
|
+
s.terminate!
|
656
|
+
end
|
657
|
+
|
658
|
+
it "should terminate all instances if running" do
|
659
|
+
s = ApplicationStack.new("test") { |s| s.role "db1" }
|
660
|
+
mock_instance = mock("an_instance", :instance_id => "i-foo")
|
661
|
+
mock_instance.should_receive(:terminate!)
|
662
|
+
Instance.stub!(:launch).and_return(mock_instance)
|
663
|
+
s.launch
|
664
|
+
s.terminate!
|
665
|
+
end
|
666
|
+
|
667
|
+
it "should remove any stored role name mappings" do
|
668
|
+
Awsymandias::SimpleDB.put ApplicationStack::DEFAULT_SDB_DOMAIN, "test", "db1" => ["instance_id"]
|
669
|
+
s = ApplicationStack.new("test") { |s| s.role "db1" }
|
670
|
+
Instance.stub!(:launch).and_return stub('stub').as_null_object
|
671
|
+
s.launch
|
672
|
+
s.terminate!
|
673
|
+
Awsymandias::SimpleDB.get(ApplicationStack::DEFAULT_SDB_DOMAIN, "test").should be_blank
|
674
|
+
end
|
675
|
+
end
|
676
|
+
end
|
677
|
+
end
|
678
|
+
end
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bguthrie-awsymandias
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian Guthrie
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-06-07 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activesupport
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.3.0
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: activeresource
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.3.0
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: hungryblank-aws-sdb
|
37
|
+
type: :runtime
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.4.0
|
44
|
+
version:
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: grempe-amazon-ec2
|
47
|
+
type: :runtime
|
48
|
+
version_requirement:
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.4.2
|
54
|
+
version:
|
55
|
+
description: A library for helping you set up, track, and tear down complicated deployment configurations in Amazon EC2.
|
56
|
+
email: btguthrie@gmail.com
|
57
|
+
executables: []
|
58
|
+
|
59
|
+
extensions: []
|
60
|
+
|
61
|
+
extra_rdoc_files:
|
62
|
+
- README.rdoc
|
63
|
+
files:
|
64
|
+
- .gitignore
|
65
|
+
- README.rdoc
|
66
|
+
- Rakefile
|
67
|
+
- VERSION
|
68
|
+
- lib/awsymandias.rb
|
69
|
+
- spec/awsymandias_spec.rb
|
70
|
+
has_rdoc: true
|
71
|
+
homepage: http://github.com/bguthrie/awsymandias
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options:
|
74
|
+
- --charset=UTF-8
|
75
|
+
require_paths:
|
76
|
+
- lib
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: "0"
|
82
|
+
version:
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: "0"
|
88
|
+
version:
|
89
|
+
requirements: []
|
90
|
+
|
91
|
+
rubyforge_project:
|
92
|
+
rubygems_version: 1.2.0
|
93
|
+
signing_key:
|
94
|
+
specification_version: 2
|
95
|
+
summary: A library for helping you set up, track, and tear down complicated deployment configurations in Amazon EC2.
|
96
|
+
test_files:
|
97
|
+
- spec/awsymandias_spec.rb
|