bguthrie-awsymandias 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ coverage/
2
+ .tmtags
@@ -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
+
@@ -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
@@ -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