bguthrie-awsymandias 0.2.1 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/.specification +57 -0
  2. data/README.rdoc +25 -21
  3. data/Rakefile +20 -4
  4. data/VERSION +1 -1
  5. data/awsymandias.gemspec +37 -12
  6. data/lib/awsymandias.rb +36 -331
  7. data/lib/awsymandias/addons/right_elb_interface.rb +375 -0
  8. data/lib/awsymandias/ec2.rb +49 -0
  9. data/lib/awsymandias/ec2/application_stack.rb +261 -0
  10. data/lib/awsymandias/extensions/class_extension.rb +18 -0
  11. data/lib/awsymandias/extensions/net_http_extension.rb +9 -0
  12. data/lib/awsymandias/instance.rb +149 -0
  13. data/lib/awsymandias/load_balancer.rb +157 -0
  14. data/lib/awsymandias/right_aws.rb +73 -0
  15. data/lib/awsymandias/right_elb.rb +16 -0
  16. data/lib/awsymandias/simple_db.rb +46 -0
  17. data/lib/awsymandias/snapshot.rb +33 -0
  18. data/lib/awsymandias/stack_definition.rb +60 -0
  19. data/lib/awsymandias/volume.rb +70 -0
  20. data/spec/integration/instance_spec.rb +37 -0
  21. data/spec/unit/addons/right_elb_interface_spec.rb +45 -0
  22. data/spec/unit/awsymandias_spec.rb +61 -0
  23. data/spec/unit/ec2/application_stack_spec.rb +634 -0
  24. data/spec/unit/instance_spec.rb +365 -0
  25. data/spec/unit/load_balancer_spec.rb +250 -0
  26. data/spec/unit/right_aws_spec.rb +90 -0
  27. data/spec/unit/simple_db_spec.rb +63 -0
  28. data/spec/unit/snapshot_spec.rb +39 -0
  29. data/spec/unit/stack_definition_spec.rb +113 -0
  30. data/tags +368 -0
  31. metadata +39 -13
  32. data/spec/awsymandias_spec.rb +0 -815
  33. data/vendor/aws-sdb/LICENSE +0 -19
  34. data/vendor/aws-sdb/README +0 -1
  35. data/vendor/aws-sdb/Rakefile +0 -20
  36. data/vendor/aws-sdb/lib/aws_sdb.rb +0 -3
  37. data/vendor/aws-sdb/lib/aws_sdb/error.rb +0 -42
  38. data/vendor/aws-sdb/lib/aws_sdb/service.rb +0 -191
  39. data/vendor/aws-sdb/spec/aws_sdb/service_spec.rb +0 -183
  40. data/vendor/aws-sdb/spec/spec_helper.rb +0 -4
@@ -0,0 +1,375 @@
1
+ #
2
+ # Copyright (c) 2008 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #
23
+
24
+ require "right_aws"
25
+
26
+ module RightAws
27
+
28
+ class ElbInterface < RightAwsBase
29
+
30
+ include RightAwsBaseInterface
31
+
32
+ DEFAULT_HOST = 'elasticloadbalancing.amazonaws.com'
33
+ DEFAULT_PORT = 443
34
+ DEFAULT_PROTOCOL = 'https'
35
+ API_VERSION = '2009-05-15'
36
+ DEFAULT_NIL_REPRESENTATION = 'nil'
37
+
38
+ @@bench = AwsBenchmarkingBlock.new
39
+ def self.bench_xml; @@bench.xml; end
40
+ def self.bench_elb; @@bench.service; end
41
+
42
+ attr_reader :last_query_expression
43
+
44
+ def on_exception
45
+ super
46
+ rescue RightAws::AwsError => e
47
+ error = Hash.from_xml(last_response.body)['ErrorResponse']['Error']
48
+ raise RightAws::AwsError, "#{error['Code']}: #{error['Message']}"
49
+ end
50
+
51
+ # Creates new RightElb instance.
52
+ #
53
+ # Params:
54
+ # { :server => 'elasticloadbalancing.amazonaws.com'
55
+ # :port => 443 # Amazon service port: 80 or 443(default)
56
+ # :protocol => 'https' # Amazon service protocol: 'http' or 'https'(default)
57
+ # :signature_version => '0' # The signature version : '0' or '1'(default)
58
+ # :multi_thread => true|false # Multi-threaded (connection per each thread): true or false(default)
59
+ # :logger => Logger Object # Logger instance: logs to STDOUT if omitted
60
+ # :nil_representation => 'mynil'} # interpret Ruby nil as this string value; i.e. use this string in SDB to represent Ruby nils (default is the string 'nil')
61
+ #
62
+ # Example:
63
+ #
64
+ # elb = RightAws::ElbInterface.new('1E3GDYEOGFJPIT7XXXXXX','hgTHt68JY07JKUY08ftHYtERkjgtfERn57XXXXXX', {:multi_thread => true, :logger => Logger.new('/tmp/x.log')}) #=> #<RightElb:0xa6b8c27c>
65
+ #
66
+ # see: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/
67
+ #
68
+ def initialize(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
69
+ @nil_rep = params[:nil_representation] ? params[:nil_representation] : DEFAULT_NIL_REPRESENTATION
70
+ params.delete(:nil_representation)
71
+ init({ :name => 'ELB',
72
+ :default_host => ENV['ELB_URL'] ? URI.parse(ENV['ELB_URL']).host : DEFAULT_HOST,
73
+ :default_port => ENV['ELB_URL'] ? URI.parse(ENV['ELB_URL']).port : DEFAULT_PORT,
74
+ :default_protocol => ENV['ELB_URL'] ? URI.parse(ENV['ELB_URL']).scheme : DEFAULT_PROTOCOL },
75
+ aws_access_key_id || ENV['AWS_ACCESS_KEY_ID'],
76
+ aws_secret_access_key || ENV['AWS_SECRET_ACCESS_KEY'],
77
+ params)
78
+ end
79
+
80
+ #-----------------------------------------------------------------
81
+ # API METHODS:
82
+ #-----------------------------------------------------------------
83
+
84
+ # elb.configure_health_check lb_name, {:healthy_threshold=>10, :unhealthy_threshold=>3,
85
+ # :target=>"TCP:3081", :interval=>31, :timeout=>6}
86
+ # => {:healthy_threshold=>"10", :unhealthy_threshold=>"3", :interval=>"31", :target=>"TCP:3081", :timeout=>"6"}
87
+ def configure_health_check(lb_name, health_check)
88
+ link = generate_request("ConfigureHealthCheck",
89
+ :load_balancer_name => lb_name, :health_check => health_check)
90
+ request_info(link, QElbConfigureHealthCheckParser.new)
91
+ rescue Exception
92
+ on_exception
93
+ end
94
+
95
+ # elb.create_lb lb_name, ['us-east-1b'], [{:load_balancer_port=>80, :instance_port=>3080, :protocol=>"HTTP"},
96
+ # {:load_balancer_port=>8080, :instance_port=>3081, :protocol=>"HTTP"}]
97
+ # => {:dns_name=>"RobTest-883635706.us-east-1.elb.amazonaws.com"}
98
+ def create_lb(lb_name, availability_zones, listeners)
99
+ link = generate_request("CreateLoadBalancer",
100
+ :load_balancer_name => lb_name,
101
+ :availability_zones => availability_zones,
102
+ :listeners => listeners
103
+ )
104
+ request_info(link, QElbSimpleParser.new(['DNSName']))
105
+ rescue Exception
106
+ on_exception
107
+ end
108
+
109
+ # elb.delete_lb lb_name
110
+ # => {}
111
+ def delete_lb(lb_name)
112
+ link = generate_request("DeleteLoadBalancer",
113
+ :load_balancer_name => lb_name)
114
+ request_info(link, QElbSimpleParser.new)
115
+ rescue Exception
116
+ on_exception
117
+ end
118
+
119
+ # elb.deregister_instances_from_lb lb_name, "i-5552453c"
120
+ # => ["i-5752453e"]
121
+ def deregister_instances_from_lb(lb_name, instance_ids)
122
+ instances = instance_ids.map { |instance_id| { :instance_id => instance_id } }
123
+ link = generate_request("DeregisterInstancesFromLoadBalancer",
124
+ :load_balancer_name => lb_name, :instances => instances
125
+ )
126
+ request_info(link, QElbInstancesParser.new)
127
+ rescue Exception
128
+ on_exception
129
+ end
130
+
131
+ # elb.describe_lbs
132
+ # => [{:aws_created_at=>Tue Aug 04 11:14:27 UTC 2009,
133
+ # :availability_zones=>["us-east-1b"],
134
+ # :dns_name=>"RobTest-883635706.us-east-1.elb.amazonaws.com",
135
+ # :name=>"RobTest",
136
+ # :instances=>["i-5752453e"],
137
+ # :listeners=> [{:protocol=>"HTTP", :load_balancer_port=>80, :instance_port=>3080},
138
+ # {:protocol=>"HTTP", :load_balancer_port=>8080, :instance_port=>3081}
139
+ # ],
140
+ # :health_check=> { :healthy_threshold=>10,
141
+ # :unhealthy_threshold=>3,
142
+ # :interval=>31,
143
+ # :target=>"TCP:3081",
144
+ # :timeout=>6
145
+ # }
146
+ # }]
147
+ def describe_lbs(lb_names = nil)
148
+ link = generate_request("DescribeLoadBalancers",
149
+ :load_balancer_names => lb_names)
150
+ request_info(link, QElbDescribeLbsParser.new)
151
+ rescue Exception
152
+ on_exception
153
+ end
154
+
155
+ # elb.describe_instance_health lb_name
156
+ # => {"i-5752453e"=>{:description=>"Instance registration is still in progress.",
157
+ # :reason_code=>"ELB",
158
+ # :state=>"OutOfService"}}
159
+ def describe_instance_health(lb_name, instances = nil)
160
+ link = generate_request("DescribeInstanceHealth", :load_balancer_name => lb_name, :instances => instances)
161
+ request_info(link, QElbDescribeInstanceHealthParser.new)
162
+ rescue Exception
163
+ on_exception
164
+ end
165
+
166
+ # elb.disable_availability_zones_for_lb lb_name, ['us-east-1c']
167
+ # => ["us-east-1b", "us-east-1a"]
168
+ def disable_availability_zones_for_lb(lb_name, availability_zones)
169
+ link = generate_request("DisableAvailabilityZonesForLoadBalancer",
170
+ :load_balancer_name => lb_name, :availability_zones => availability_zones)
171
+ request_info(link, QElbAvailabilityZonesParser.new)
172
+ rescue Exception
173
+ on_exception
174
+ end
175
+
176
+ # elb.enable_availability_zones_for_lb lb_name, ['us-east-1a', 'us-east-1c']
177
+ # => ["us-east-1b", "us-east-1c", "us-east-1a"]
178
+ def enable_availability_zones_for_lb(lb_name, availability_zones)
179
+ link = generate_request("EnableAvailabilityZonesForLoadBalancer",
180
+ :load_balancer_name => lb_name, :availability_zones => availability_zones)
181
+ request_info(link, QElbAvailabilityZonesParser.new)
182
+ rescue Exception
183
+ on_exception
184
+ end
185
+
186
+ # elb.register_instances_with_lb lb_name, ["i-5552453c", "i-5752453e"]
187
+ # => ["i-5552453c", "i-5752453e"]
188
+ def register_instances_with_lb(lb_name, instance_ids)
189
+ instances = instance_ids.map { |instance_id| { :instance_id => instance_id } }
190
+ link = generate_request("RegisterInstancesWithLoadBalancer",
191
+ :load_balancer_name => lb_name, :instances => instances)
192
+ request_info(link, QElbInstancesParser.new)
193
+ rescue Exception
194
+ on_exception
195
+ end
196
+
197
+
198
+ private
199
+
200
+ #-----------------------------------------------------------------
201
+ # Requests
202
+ #-----------------------------------------------------------------
203
+ def generate_request(action, params={}) #:nodoc:
204
+ # remove empty params from request
205
+ params.delete_if {|key,value| value.nil? }
206
+ params = rehash_params_for_request(params)
207
+
208
+ # prepare service data
209
+ service = '/'
210
+ service_hash = {"Action" => action,
211
+ "AWSAccessKeyId" => @aws_access_key_id,
212
+ "Version" => API_VERSION }
213
+ service_hash.update(params)
214
+ service_params = signed_service_params(@aws_secret_access_key, service_hash, :get, @params[:server], service)
215
+ #
216
+ request = Net::HTTP::Get.new("#{service}?#{service_params}")
217
+
218
+ # prepare output hash
219
+ { :request => request,
220
+ :server => @params[:server],
221
+ :port => @params[:port],
222
+ :protocol => @params[:protocol] }
223
+ end
224
+
225
+ # Sends request to Amazon and parses the response
226
+ # Raises AwsError if any banana happened
227
+ def request_info(request, parser) #:nodoc:
228
+ thread = @params[:multi_thread] ? Thread.current : Thread.main
229
+ thread[:elb_connection] ||= Rightscale::HttpConnection.new(:exception => AwsError, :logger => @logger)
230
+ request_info_impl(thread[:elb_connection], @@bench, request, parser)
231
+ end
232
+
233
+ def rehash_params_for_request(parameters = {})
234
+ new_params = {}
235
+ parameters.each_pair do |param_name, value|
236
+ case value.class.name
237
+ when 'Array'
238
+ value.each_with_index do |element, index|
239
+ if element.is_a? Hash
240
+ element.each_pair do |key, val|
241
+ new_params["#{param_name.to_s.camelize}.member.#{index + 1}.#{key.to_s.camelize}"] = val
242
+ end
243
+ else
244
+ new_params["#{param_name.to_s.camelize}.member.#{index + 1}"] = element
245
+ end
246
+ end
247
+ when 'Hash'
248
+ value.each_pair do |key, val|
249
+ new_params["#{param_name.to_s.camelize}.#{key.to_s.camelize}"] = val
250
+ end
251
+ else
252
+ new_params[param_name.to_s.camelize] = value
253
+ end
254
+ end
255
+ new_params
256
+ end
257
+
258
+ #-----------------------------------------------------------------
259
+ # PARSERS:
260
+ #-----------------------------------------------------------------
261
+ class QElbDescribeLbsParser < RightAWSParser #:nodoc:
262
+ def tagstart(name, attributes)
263
+ case name
264
+ when 'HealthCheck' then @health_check = {}
265
+ when 'member'
266
+ case @xmlpath
267
+ when 'DescribeLoadBalancersResponse/DescribeLoadBalancersResult/LoadBalancerDescriptions' then @lb = {:listeners => [], :availability_zones => [], :instances => []}
268
+ when 'DescribeLoadBalancersResponse/DescribeLoadBalancersResult/LoadBalancerDescriptions/member/Listeners' then @listener = {}
269
+ when 'DescribeLoadBalancersResponse/DescribeLoadBalancersResult/LoadBalancerDescriptions/member/Instances' then @instance = {}
270
+ end
271
+ end
272
+ end
273
+ def tagend(name)
274
+ case name
275
+ when 'LoadBalancerName' then @lb[:name] = @text
276
+ when 'CreatedTime' then @lb[:aws_created_at] = Time.parse(@text)
277
+ when 'DNSName' then @lb[:dns_name] = @text
278
+
279
+ when 'Protocol' then @listener[:protocol] = @text
280
+ when 'LoadBalancerPort' then @listener[:load_balancer_port] = @text.to_i
281
+ when 'InstancePort' then @listener[:instance_port] = @text.to_i
282
+
283
+ when 'HealthCheck' then @lb[:health_check] = @health_check
284
+ when 'Interval' then @health_check[:interval] = @text.to_i
285
+ when 'Target' then @health_check[:target] = @text
286
+ when 'HealthyThreshold' then @health_check[:healthy_threshold] = @text.to_i
287
+ when 'Timeout' then @health_check[:timeout] = @text.to_i
288
+ when 'UnhealthyThreshold' then @health_check[:unhealthy_threshold] = @text.to_i
289
+
290
+ when 'member'
291
+ case @xmlpath
292
+ when 'DescribeLoadBalancersResponse/DescribeLoadBalancersResult/LoadBalancerDescriptions' then @result << @lb
293
+ when 'DescribeLoadBalancersResponse/DescribeLoadBalancersResult/LoadBalancerDescriptions/member/Listeners' then @lb[:listeners] << @listener
294
+ when 'DescribeLoadBalancersResponse/DescribeLoadBalancersResult/LoadBalancerDescriptions/member/Listeners' then @lb[:instances] << @instance
295
+ when 'DescribeLoadBalancersResponse/DescribeLoadBalancersResult/LoadBalancerDescriptions/member/AvailabilityZones' then @lb[:availability_zones] << @text
296
+ when 'DescribeLoadBalancersResponse/DescribeLoadBalancersResult/LoadBalancerDescriptions/member/Instances' then @lb[:instances] << @text.strip
297
+ end
298
+ end
299
+ end
300
+ def reset
301
+ @result = []
302
+ end
303
+ end
304
+
305
+ class QElbDescribeInstanceHealthParser < RightAWSParser #:nodoc:
306
+ def reset
307
+ @result = {}
308
+ end
309
+ def tagstart(name, attributes)
310
+ @instance_health = {} if name == 'member'
311
+ end
312
+ def tagend(name)
313
+ case name
314
+ when 'Description' then @instance_health[:description] = @text
315
+ when 'State' then @instance_health[:state] = @text
316
+ when 'ReasonCode' then @instance_health[:reason_code] = @text
317
+ when 'InstanceId' then @instance_id = @text
318
+
319
+ when 'member' then @result[@instance_id] = @instance_health
320
+ end
321
+ end
322
+ end
323
+
324
+ class QElbConfigureHealthCheckParser < RightAWSParser #:nodoc:
325
+ def reset
326
+ @result = {}
327
+ end
328
+ def tagstart(name, attributes)
329
+ case name
330
+ when 'ConfigureHealthCheckResult' then @healthcheck = {}
331
+ end
332
+ end
333
+ def tagend(name)
334
+ case name
335
+ when 'ConfigureHealthCheckResult' then @result = @healthcheck
336
+ when 'HealthyThreshold', 'UnhealthyThreshold', 'Target', 'Interval', 'Timeout'
337
+ @healthcheck[name.underscore.to_sym] = @text
338
+ end
339
+ end
340
+ end
341
+
342
+ class QElbAvailabilityZonesParser < RightAWSParser #:nodoc:
343
+ def reset
344
+ @result = []
345
+ end
346
+ def tagend(name)
347
+ @result << @text if name == 'member'
348
+ end
349
+ end
350
+
351
+ class QElbInstancesParser < RightAWSParser #:nodoc:
352
+ def reset
353
+ @result = []
354
+ end
355
+ def tagend(name)
356
+ @result << @text if name == 'InstanceId'
357
+ end
358
+ end
359
+
360
+ class QElbSimpleParser < RightAWSParser #:nodoc:
361
+ def initialize(names_to_parse = ['InstanceId'])
362
+ super()
363
+ @names_to_parse = names_to_parse
364
+ end
365
+ def reset
366
+ @result = {}
367
+ end
368
+ def tagend(name)
369
+ @result[name.underscore.to_sym] = @text if @names_to_parse.include?(name)
370
+ end
371
+ end
372
+
373
+ end
374
+
375
+ end
@@ -0,0 +1,49 @@
1
+ module Awsymandias
2
+ module EC2
3
+ class << self
4
+ # Define the values for AMAZON_ACCESS_KEY_ID and AMAZON_SECRET_ACCESS_KEY_ID to allow for automatic
5
+ # connection creation.
6
+ def connection
7
+ @connection ||= ::EC2::Base.new(
8
+ :access_key_id => Awsymandias.access_key_id || ENV['AMAZON_ACCESS_KEY_ID'],
9
+ :secret_access_key => Awsymandias.secret_access_key || ENV['AMAZON_SECRET_ACCESS_KEY']
10
+ )
11
+ end
12
+
13
+ def instance_types
14
+ [
15
+ Awsymandias::EC2::InstanceTypes::M1_SMALL,
16
+ Awsymandias::EC2::InstanceTypes::M1_LARGE,
17
+ Awsymandias::EC2::InstanceTypes::M1_XLARGE,
18
+ Awsymandias::EC2::InstanceTypes::C1_MEDIUM,
19
+ Awsymandias::EC2::InstanceTypes::C1_XLARGE
20
+ ].index_by(&:name)
21
+ end
22
+ end
23
+
24
+ InstanceType = Struct.new(:name, :price_per_hour)
25
+
26
+ # All currently available instance types.
27
+ # TODO Generate dynamically.
28
+ module InstanceTypes
29
+ M1_SMALL = InstanceType.new("m1.small", Money.new(10))
30
+ M1_LARGE = InstanceType.new("m1.large", Money.new(40))
31
+ M1_XLARGE = InstanceType.new("m1.xlarge", Money.new(80))
32
+
33
+ C1_MEDIUM = InstanceType.new("c1.medium", Money.new(20))
34
+ C1_XLARGE = InstanceType.new("c1.xlarge", Money.new(80))
35
+ end
36
+
37
+ # All currently availability zones.
38
+ # TODO Generate dynamically.
39
+ module AvailabilityZones
40
+ US_EAST_1A = "us_east_1a"
41
+ US_EAST_1B = "us_east_1b"
42
+ US_EAST_1C = "us_east_1c"
43
+
44
+ EU_WEST_1A = "eu_west_1a"
45
+ EU_WEST_1B = "eu_west_1b"
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,261 @@
1
+ module Awsymandias
2
+ module EC2
3
+ class ApplicationStack
4
+ attr_reader :name, :simpledb_domain, :unlaunched_instances, :instances, :volumes, :roles, :unlaunched_load_balancers, :load_balancers
5
+
6
+ DEFAULT_SIMPLEDB_DOMAIN = "application-stack"
7
+
8
+ class << self
9
+ def find(name)
10
+ returning(new(name)) do |stack|
11
+ stack.send(:reload_from_metadata!)
12
+ return nil unless stack.launched?
13
+ end
14
+ end
15
+
16
+ def launch(name, opts={})
17
+ returning(new(name, opts)) do |stack|
18
+ stack.launch
19
+ end
20
+ end
21
+ end
22
+
23
+ def initialize(name, opts={})
24
+ opts.assert_valid_keys :instances, :simpledb_domain, :volumes, :roles, :load_balancers
25
+
26
+ @name = name
27
+ @simpledb_domain = opts[:simpledb_domain] || DEFAULT_SIMPLEDB_DOMAIN
28
+ @instances = {}
29
+ @unlaunched_instances = {}
30
+ @volumes = {}
31
+ @roles = {}
32
+ @load_balancers = {}
33
+ @unlaunched_load_balancers = {}
34
+ @terminating = false
35
+
36
+ if opts[:roles]
37
+ @roles = opts[:roles]
38
+ @roles.keys.each { |role| define_methods_for_role(role) }
39
+ end
40
+
41
+ if opts[:instances]
42
+ @unlaunched_instances = opts[:instances]
43
+ opts[:instances].each { |name, configuration| define_methods_for_instance(name) }
44
+ end
45
+
46
+ opts[:volumes].each { |name, configuration| volume(name, configuration) } if opts[:volumes]
47
+
48
+ if opts[:load_balancers]
49
+ opts[:load_balancers].each_pair { |lb_name, config| @unlaunched_load_balancers[lb_name] = config }
50
+ end
51
+ end
52
+
53
+ def self.define(name, &block)
54
+ definition = StackDefinition.new(name)
55
+ yield definition if block_given?
56
+ definition.build_stack
57
+ end
58
+
59
+ def instances
60
+ !@instances.empty? ? @instances.values : {}
61
+ end
62
+
63
+ def volume(name, opts = {})
64
+ opts.assert_valid_keys :volume_id, :instance, :unix_device, :snapshot_id, :role, :all_instances
65
+ @volumes[name] = opts
66
+ end
67
+
68
+ def define_methods_for_instance(instance_name)
69
+ if !self.metaclass.respond_to?(instance_name)
70
+ self.metaclass.send(:define_method, instance_name) { @instances[instance_name] }
71
+ end
72
+ end
73
+
74
+ def define_methods_for_role(role_name)
75
+ self.metaclass.send(:define_method, role_name) do
76
+ @roles[role_name].map { |instance_name| @instances[instance_name] }
77
+ end
78
+ end
79
+
80
+ def launch
81
+ store_app_stack_metadata!
82
+
83
+ @unlaunched_instances.each_pair do |instance_name, params|
84
+ @instances[instance_name] = Awsymandias::Instance.launch(params)
85
+ @instances[instance_name].name = instance_name
86
+ @unlaunched_instances.delete instance_name
87
+ end
88
+ store_app_stack_metadata!
89
+
90
+ @unlaunched_load_balancers.each_pair do |lb_name, params|
91
+ instance_names = params[:instances]
92
+ params[:instances] = params.delete(:instances).map { |instance_name| @instances[instance_name].instance_id } if params[:instances]
93
+ params[:name] = lb_name
94
+ @load_balancers[lb_name] = Awsymandias::LoadBalancer.launch(params)
95
+ @unlaunched_load_balancers.delete lb_name
96
+ end
97
+ store_app_stack_metadata!
98
+
99
+ attach_volumes
100
+ store_app_stack_metadata!
101
+
102
+ self
103
+ end
104
+
105
+ def attach_volumes
106
+ @volumes.each do |volume, options|
107
+ if options[:instance]
108
+ attach_volume_to_instance options
109
+ elsif options[:role]
110
+ create_and_attach_volumes_to_instances send(options[:role]), options
111
+ elsif options[:all_instances]
112
+ create_and_attach_volumes_to_instances instances, options
113
+ else
114
+ raise "Neither role, instance, or all_instances was specified for #{volume} volume"
115
+ end
116
+ end
117
+ end
118
+
119
+ def attach_volume_to_instance(options)
120
+ volume = Awsymandias::RightAws.describe_volumes([options[:volume_id]]).first
121
+ volume.attach_to_once_running @instances[options[:instance]], options[:unix_device]
122
+ end
123
+
124
+ def create_and_attach_volumes_to_instances(instances, options)
125
+ volumes = instances.map do |i|
126
+ if already_attached_volume = i.volume_attached_to_unix_device(options[:unix_device])
127
+ raise "Another volume (#{already_attached_volume.aws_id}) is already attached to " +
128
+ "instance #{i.instance_id} at #{options[:unix_device]}."
129
+ end
130
+
131
+ Awsymandias::RightAws.wait_for_create_volume(options[:snapshot_id], i.aws_availability_zone)
132
+ end
133
+
134
+ sleep 5 # There seems to be a race condition between when the volume says it is available and actually being able to attach it
135
+
136
+ instances.zip(volumes).each do |i, volume|
137
+ volume.attach_to_once_running i, options[:unix_device]
138
+ end
139
+ end
140
+
141
+ def reload
142
+ raise "Can't reload unless launched" unless (launched? || terminating?)
143
+ @instances.values.each(&:reload)
144
+ @load_balancers.values.each(&:reload)
145
+ self
146
+ end
147
+
148
+ def terminate!
149
+ @terminating = true
150
+ store_app_stack_metadata!
151
+ instances.each do |instance|
152
+ instance.terminate! if instance.running?
153
+ @instances.delete(instance.name)
154
+ end
155
+
156
+ load_balancers.values.each do |load_balancer|
157
+ load_balancer.terminate! if load_balancer.launched?
158
+ @load_balancers.delete(load_balancer.name)
159
+ end
160
+
161
+ remove_app_stack_metadata!
162
+ self
163
+ end
164
+
165
+ def terminating?
166
+ @terminating
167
+ end
168
+
169
+ def launched?
170
+ instances.any?
171
+ end
172
+
173
+ def running?
174
+ launched? && @instances.values.all?(&:running?)
175
+ end
176
+
177
+ def terminated?
178
+ launched? && @instances.values.all?(&:terminated?)
179
+ end
180
+
181
+ def port_open?(port)
182
+ instances.all? { |instance| instance.port_open?(port) }
183
+ end
184
+
185
+ def running_cost
186
+ return Money.new(0) unless launched?
187
+ @instances.values.sum { |instance| instance.running_cost }
188
+ end
189
+
190
+ def summarize
191
+ output = []
192
+ output << "Stack '#{name}'"
193
+ @instances.each_pair do |name, instance|
194
+ output << instance.summarize
195
+ output << ''
196
+ end
197
+ @load_balancers.each_pair do |lb_name, lb|
198
+ output << lb.summarize
199
+ output << ''
200
+ end
201
+ output.flatten.join("\n")
202
+ end
203
+
204
+ private
205
+
206
+ def store_app_stack_metadata!
207
+ metadata = {}
208
+
209
+ [:unlaunched_instances, :unlaunched_load_balancers, :roles].each do |item_name|
210
+ metadata[item_name] = instance_variable_get "@#{item_name}"
211
+ end
212
+
213
+ [:instances, :load_balancers].each do |collection|
214
+ metadata[collection] = {}
215
+ instance_variable_get("@#{collection}").each_pair do |item_name, item|
216
+ metadata[collection][item_name] = item.to_simpledb
217
+ end
218
+ end
219
+
220
+ Awsymandias::SimpleDB.put @simpledb_domain, @name, metadata
221
+ end
222
+
223
+ def remove_app_stack_metadata!
224
+ Awsymandias::SimpleDB.delete @simpledb_domain, @name
225
+ end
226
+
227
+ def reload_from_metadata!
228
+ metadata = Awsymandias::SimpleDB.get @simpledb_domain, @name
229
+
230
+ unless metadata.empty?
231
+ metadata[:unlaunched_load_balancers] ||= []
232
+ metadata[:load_balancers] ||= []
233
+ @unlaunched_load_balancers = metadata[:unlaunched_load_balancers]
234
+ unless metadata[:load_balancers].empty?
235
+ live_lbs = Awsymandias::LoadBalancer.find( metadata[:load_balancers].keys ).index_by(&:name)
236
+ metadata[:load_balancers].each_pair do |lb_name, lb|
237
+ @load_balancers[lb_name] = live_lbs[lb_name]
238
+ end
239
+ end
240
+
241
+ @unlaunched_instances = metadata[:unlaunched_instances]
242
+
243
+ unless metadata[:instances].empty?
244
+ live_instances = Awsymandias::Instance.find(:all, :instance_ids =>
245
+ metadata[:instances].values.map { |inst| inst[:aws_instance_id] }
246
+ ).index_by(&:instance_id)
247
+ metadata[:instances] = metadata[:instances]
248
+ metadata[:instances].each_pair do |instance_name, instance_metadata|
249
+ @instances[instance_name] = live_instances[instance_metadata[:aws_instance_id]]
250
+ @instances[instance_name].name = instance_name
251
+ define_methods_for_instance(instance_name)
252
+ end
253
+ end
254
+
255
+ @roles = metadata[:roles]
256
+ @roles.keys.each { |role| define_methods_for_role(role) }
257
+ end
258
+ end
259
+ end
260
+ end
261
+ end