aws-tools 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/bin/aws_manager.rb +8 -2
  3. data/lib/aws_region.rb +756 -755
  4. metadata +38 -50
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ca067a22cebe6f3a1fce85ce9feed048ff268a45
4
+ data.tar.gz: 1ec0ba2eb6779f228be4e08f47e4d3209afa00f9
5
+ SHA512:
6
+ metadata.gz: e641830b7260a47d7eeaa45c265f7957f13f8a4d5538021cab4bec00b6c054c705e7513862ec793c30af2e25774fcf2e7497884718b82de4f8b70f91dd212d5e
7
+ data.tar.gz: 945cd6a46e045496e1924e1a4c0387564252422a85170a1b09cd7cddb2317c76bc7a0b71d6a5772d01d2505b08015228ae73ae3d9402892bf3ad5b209cd29d01
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'aws-sdk-core'
3
+ require 'aws-sdk'
4
4
  require 'yaml'
5
5
  require 'optparse'
6
6
  require 'erb'
@@ -96,6 +96,7 @@ def syntax
96
96
  puts "Syntax:"
97
97
  puts " EC2 Instance commands:"
98
98
  puts " aws_manager.rb --region region run_instance <instance template file> "
99
+ puts " aws_manager.rb --region region [--mount volume:device] run_instance <instance template file> "
99
100
  puts " aws_manager.rb --region region [--environment environment] [--purpose purpose] [--choose first|oldest|newest] connect [id]"
100
101
  puts " aws_manager.rb --region region [--environment environment] [--purpose purpose] [--choose first|oldest|newest] start [id]"
101
102
  puts " aws_manager.rb --region region [--environment environment] [--purpose purpose] [--choose first|oldest|newest] [--keep-one] stop [id]"
@@ -129,13 +130,14 @@ def syntax
129
130
  end
130
131
  def main
131
132
  syntax if ARGV.length <= 0
132
- params = ARGV.getopts("hr:e:p:fkc:", "choose:", "keep-one", "help", "region:", "environment:", "purpose:")
133
+ params = ARGV.getopts("hr:e:p:fkc:m:", "choose:", "mount:", "keep-one", "help", "region:", "environment:", "purpose:")
133
134
  syntax if params['h'] or params['help']
134
135
  purpose = params['p'] || params['purpose']
135
136
  environment = params['e'] || params['environment']
136
137
  region = params['r'] || params['region']
137
138
  keep_one = params['k'] || params['keep-one']
138
139
  selection_criteria = params['c'] || params['choose']
140
+ mount = params['m'] || params['mount']
139
141
  selection_criteria = selection_criteria.downcase.to_sym if selection_criteria
140
142
  syntax if selection_criteria and !([:first,:oldest, :newest].include?(selection_criteria))
141
143
  syntax if ARGV.length <= 0
@@ -204,6 +206,10 @@ def main
204
206
  end
205
207
  image_options = YAML.load(ERB.new(File.read(instance_template)).result)
206
208
  instance = region.create_instance(image_options)
209
+ if mount
210
+ (vol,dev) = mount.split(":")
211
+ instance.mount(vol, dev)
212
+ end
207
213
  elsif command == 'terminate_instance'
208
214
  if keep_one and filter_instances(region.find_instances({environment: environment, purpose: purpose }), ['running']).length < 2
209
215
  puts "There are less than 2 instances, and keep_one flag set. Exiting."
@@ -1,755 +1,756 @@
1
- require 'aws-sdk-core'
2
- require 'yaml'
3
- require 'json'
4
-
5
- class AwsBase
6
- def log(msg)
7
- @logger.write("#{Time.now.strftime("%b %D, %Y %H:%S:%M:")} #{msg}\n") if @logger
8
- end
9
- end
10
-
11
- # AwsRegion is a simplified wrapper on top of a few of the Aws core objects
12
- # The main goal is to expose a extremely simple interface for some our most
13
- # frequently used Aws facilities.
14
- class AwsRegion < AwsBase
15
- attr_accessor :ec2, :region, :rds, :account_id, :elb, :cw, :s3, :sns
16
- REGIONS = {'or' => "us-west-2", 'ca' => "us-west-1", 'va' => 'us-east-1'}
17
-
18
- # @param region [String] must be one of the keys of the {AwsRegion::REGIONS REGIONS} static hash
19
- # @param account_id [String] Aws account id
20
- # @param access_key_id [String] Aws access key id
21
- # @param secret_access_key [String] Aws secret access key
22
- def initialize(region, account_id, access_key_id, secret_access_key, logger = nil)
23
- @logger = logger
24
- @region = REGIONS[region]
25
- @account_id = account_id
26
- Aws.config = {:access_key_id => access_key_id,
27
- :secret_access_key => secret_access_key}
28
- @ec2 = Aws::EC2.new({:region => @region})
29
- @rds = Aws::RDS.new({:region => @region})
30
- @elb = Aws::ElasticLoadBalancing.new({:region => @region})
31
- @cw = Aws::CloudWatch.new({:region => @region})
32
- @s3 = Aws::S3.new({:region => @region})
33
- @sns = Aws::SNS.new({:region => @region})
34
- end
35
-
36
- # Simple EC2 Instance finder. Can find using instance_id, or using
37
- # :environment and :purpose instance tags which must both match.
38
- #
39
- # @param options [Hash] containing search criteria. Values can be:
40
- # * :instance_id - identifies an exact instance
41
- # * :environment - instance tag
42
- # * :purpose - instance tag
43
- # @return [Array<AwsInstance>] instances found to match criteria
44
- def find_instances(options={})
45
- instances = []
46
- @ec2.describe_instances[:reservations].each do |i|
47
- i.instances.each do |y|
48
- instance = AwsInstance.new(self, {:instance => y})
49
- if instance.state != 'terminated'
50
- if options.has_key?(:environment) and options.has_key?(:purpose)
51
- instances << instance if instance.tags[:environment] == options[:environment] and instance.tags[:purpose] == options[:purpose]
52
- elsif options.has_key?(:instance_id)
53
- instances << instance if instance.id == options[:instance_id]
54
- end
55
- end
56
- end
57
- end
58
- return instances
59
- end
60
-
61
- # Simple DB Instance finder. Can find using instance_id, or using
62
- # :environment and :purpose instance tags which must both match.
63
- #
64
- # @param options [Hash] containing search criteria. Values can be:
65
- # * :instance_id - identifies an exact instance
66
- # * :environment - instance tag
67
- # * :purpose - instance tag
68
- # @return [Array<AwsDbInstance>] instances found to match criteria
69
- def find_db_instances(options={})
70
- instances = []
71
- @rds.describe_db_instances[:db_instances].each do |i|
72
- instance = AwsDbInstance.new(self, {:instance => i})
73
- if options.has_key?(:instance_id)
74
- instance.id == options[:instance_id]
75
- instances << instance
76
- elsif instance.tags[:environment] == options[:environment] and
77
- instance.tags[:purpose] == options[:purpose]
78
- instances << instance
79
- end
80
- end
81
- instances
82
- end
83
-
84
-
85
- # Search region for a bucket by name
86
- #
87
- # @param options [Hash] containing search criteria. Values can be:
88
- # * :bucket - Bucket name
89
- # @return [Array<AwsBucket>] instances found to match criteria
90
- def find_buckets(options={})
91
- buckets = []
92
- _buckets = @s3.list_buckets()
93
- _buckets[:buckets].each do |b|
94
- buckets << AwsBucket.new(self, {id: b[:name]}) if b[:name] == options[:bucket]
95
- end
96
- buckets
97
- end
98
-
99
- # Construct new EC2 instance
100
- #
101
- # @param options [Hash] containing initialization parameters. See {AwsInstance#initialize}
102
- # @return [AwsInstance]
103
- def create_instance(options={})
104
- AwsInstance.new(self, options)
105
- end
106
-
107
- # Construct new DB instance
108
- #
109
- # @param options [Hash] containing initialization parameters. See {AwsDbInstance#initialize}
110
- # @return [AwsDbInstance]
111
- def create_db_instance(options={})
112
- AwsDbInstance.new(self, options)
113
- end
114
-
115
- # Construct new CloudWatch instance
116
- #
117
- # @param options [Hash] containing initialization parameters. See {AwsCw#initialize}
118
- # @return [AwsCw]
119
- def create_cw_instance(options={})
120
- AwsCw.new(self, options)
121
- end
122
-
123
- # Construct new AwsBucket instance
124
- #
125
- # @param options [Hash] containing initialization parameters. See {AwsBucket#initialize}
126
- # @return [AwsBucket]
127
- def create_bucket(options={})
128
- AwsBucket.new(self, options)
129
- end
130
-
131
- def create_sns_instance
132
- AwsSns.new(self)
133
- end
134
-
135
- class AwsSns
136
- attr_accessor :region
137
- def initialize(region)
138
- @region = region
139
- end
140
- def publish(topic_arn, subject) #, message)
141
- @region.sns.publish(topic_arn: topic_arn, message: "unused for texts", subject: subject)
142
- end
143
- end
144
-
145
- # Methods for dealing with CloudWatch
146
- class AwsCw < AwsBase
147
- attr_accessor :region
148
-
149
- # @param region [String] - Value from REGION static hash
150
- def initialize(region, options={})
151
- @region = region
152
- end
153
-
154
- # Put a cw metric
155
- # @param arg_csv [String] - CSV row: "namespace,name,value,dims"
156
- # * Note that dims is formatted as an arbitrary semicolon separated list of name:value dimensions. For example:
157
- # * "activeservers,count,10,env:prod;purp:test"
158
- # @return [Aws::PageableResponse]
159
- def put_metric(arg_csv)
160
- (namespace, name, value, dims) = arg_csv.split(",")
161
- dimensions = []
162
- dims.split(";").each do |d|
163
- (n, v) = d.split(":")
164
- dimensions << {:name => n, :value => v}
165
- end
166
- args = {:namespace => namespace}
167
- metric ={:metric_name => name, :value => value.to_f, :timestamp => Time.now, :dimensions => dimensions}
168
- args[:metric_data] = [metric]
169
- @region.cw.put_metric_data(args)
170
- end
171
- end
172
-
173
- # Methods for dealing with S3 buckets
174
- class AwsBucket < AwsBase
175
- attr_accessor :region
176
-
177
- # Constructs a bucket instance from an existing bucket, or creates a new one with the name
178
- # @param region [String] - Value from REGION static hash
179
- # @param options [Hash] - Possible values:
180
- # * :id - id of existing bucket
181
- # * :bucket - Name of bucket to find or create
182
- def initialize(region, options={})
183
- @region = region
184
- if options.has_key?(:id)
185
- @id = options[:id]
186
- elsif options.has_key?(:bucket)
187
- bucket = options[:bucket]
188
- if @region.find_buckets({bucket: bucket}).length <= 0
189
- @region.s3.create_bucket({:bucket => bucket,
190
- :create_bucket_configuration => {:location_constraint => @region.region}})
191
- if @region.find_buckets({bucket: bucket}).length <= 0
192
- raise "Error creating bucket: #{bucket} in region: #{@region.region}"
193
- end
194
- end
195
- @id = bucket
196
- end
197
- end
198
-
199
- # Delete this bucket instance
200
- # @return [AwsPageableResponse]]
201
- def delete
202
- @region.s3.delete_bucket({bucket: @id})
203
- end
204
-
205
- # Put a local file to this bucket
206
- # @param filename [String] - local file name
207
- # @param file_identity [String] - S3 file path
208
- # @return [AwsPageableResponse]
209
- def put_file(filename, file_identity)
210
- File.open(filename, 'r') do |reading_file|
211
- resp = @region.s3.put_object(
212
- acl: "bucket-owner-full-control",
213
- body: reading_file,
214
- bucket: @id,
215
- key: file_identity
216
- )
217
- end
218
- end
219
-
220
- # puts a local file to an s3 object in bucket on path
221
- # example: put_local_file(:bucket=>"bucket", :local_file_path=>"/tmp/bar/foo.txt", :aws_path=>"b")
222
- # would make an s3 object named foo.txt in bucket/b
223
- # @param local_file_path [String] - Location of file to put
224
- # @param aws_path [String] - S3 path to put the file
225
- # @param options [Hash] - Can contain any valid S3 bucket options see [docs](http://docs.aws.amazon.com/sdkforruby/api/frames.html)
226
- def put(local_file_path, aws_path, options={})
227
- aws_path = aws_path[0..-2] if aws_path[-1..-1] == '/'
228
- s3_path = "#{aws_path}/#{File.basename(local_file_path)}"
229
- log "s3 writing #{local_file_path} to bucket #{@id} path: #{aws_path} s3 path: #{s3_path}"
230
- f = File.open local_file_path, 'rb'
231
- options[:bucket] = @id
232
- options[:key] = s3_path
233
- options[:body] = f
234
- options[:storage_class] = 'REDUCED_REDUNDANCY'
235
- result = @region.s3.put_object(params=options)
236
- f.close
237
- result
238
- end
239
-
240
- # prefix is something like: hchd-A-A-Items
241
- # This will return in an array of strings the names of all objects in s3 in
242
- # the :aws_path under :bucket starting with passed-in prefix
243
- # example: :aws_path=>'development', :prefix=>'broadhead'
244
- # would return array of names of objects in said bucket
245
- # matching (in regex terms) development/broadhead.*
246
- # @param options [Hash] - Can contain:
247
- # * :aws_path - first part of S3 path to search
248
- # * :prefix - Actually suffix of path to search.
249
- # @return [Array<Hash>] - 0 or more objects
250
- def find(options={})
251
- aws_path = options[:aws_path]
252
- prefix = options[:prefix]
253
- aws_path = '' if aws_path.nil?
254
- aws_path = aws_path[0..-2] if aws_path[-1..-1] == '/'
255
- log "s3 searching bucket:#{@id} for #{aws_path}/#{prefix}"
256
- objects = @region.s3.list_objects(:bucket => @id,
257
- :prefix => "#{aws_path}/#{prefix}")
258
- f = objects.contents.collect(&:key)
259
- log "s3 searched got: #{f.inspect}"
260
- f
261
- end
262
-
263
- # writes contents of S3 object to local file
264
- # example: get( :s3_path_to_object=>development/myfile.txt',
265
- # :dest_file_path=>'/tmp/foo.txt')
266
- # would write to local /tmp/foo.txt a file retrieved from s3
267
- # at development/myfile.txt
268
- # @param options [Hash] - Can contain:
269
- # * :s3_path_to_object - S3 object path
270
- # * :dest_file_path - local file were file will be written
271
- # @return [Boolean]
272
- def get(options={})
273
- begin
274
- s3_path_to_object = options[:s3_path_to_object]
275
- dest_file_path = options[:dest_file_path]
276
- File.delete dest_file_path if File.exists?(dest_file_path)
277
- log "s3 get bucket:#{@id} path:#{s3_path_to_object} dest:#{dest_file_path}"
278
- response = @region.s3.get_object(:bucket => @id,
279
- :key => s3_path_to_object)
280
- response.body.rewind
281
- File.open(dest_file_path, 'wb') do |file|
282
- response.body.each { |chunk| file.write chunk }
283
- end
284
- rescue Exception => e
285
- return false
286
- end
287
- true
288
- end
289
-
290
- # deletes from s3 an object at :s3_path_to_object
291
- # @param options [Hash] - Can be:
292
- # * :s3_path_to_object
293
- # @return [Boolean]
294
- def delete_object(options={})
295
- begin
296
- s3_path_to_object = options[:s3_path_to_object]
297
- log "s3 delete #{s3_path_to_object}"
298
- @region.s3.delete_object(:bucket => @id,
299
- :key => s3_path_to_object)
300
- rescue Exception => e
301
- return false
302
- end
303
- true
304
- end
305
-
306
- # delete all objects in a bucket
307
- # @return [Boolean]
308
- def delete_all_objects
309
- begin
310
- response = @region.s3.list_objects({:bucket => @id})
311
- response[:contents].each do |obj|
312
- @region.s3.delete_object(:bucket => @id,
313
- :key => obj[:key])
314
- end
315
- rescue Exception => e
316
- return false
317
- end
318
- true
319
- end
320
- end
321
-
322
- # Class to handle RDS Db instances
323
- class AwsDbInstance < AwsBase
324
- attr_accessor :id, :tags, :region, :endpoint
325
-
326
- # Creates an AwsDbInstance for an existing instance or creates a new database
327
- # @param region [String] - - Value from REGION static hash
328
- # @param options [Hash] - Can contain:
329
- # * :instance - If specified, create an instance of this class using this RDS instance.
330
- # * :opts - [Hash] - Includes parameters for constructing the database. The format is:
331
- # * :db_instance_identifier - RDS instance identifier
332
- # * :db_subnet_group_name - DB Subgroup name
333
- # * :publicly_accessible - [true|false]
334
- # * :db_instance_class - RDS db instance class
335
- # * :availability_zone - RDS/Aws availability zone
336
- # * :multi_az - [true|false]
337
- # * :engine - RDS engine (Only tested with Mysql at this point)
338
- # * :tags - Tags to be applied to RDS instance. The follow are required. Arbitrary tags may also be added.
339
- # * :environment - Environment designation - can be anything. Used to locate instance with other aws-tools
340
- # * :purpose - Purpose designation - can be anything. Used to locate instance with other aws-tools
341
- # * :name - Name will appear in the Aws web page if you set this
342
- # * :snapshot_name - Name of the snapshot that will be used to construct the new instance. This name will be matched with the RDS db_instance_identifier. The latest snapshot will be used.
343
- # * :vpc_security_group_ids: - Comma separated list of security groups that will be applied to this instance
344
- def initialize(region, options = {})
345
- @region = region
346
- opts = options[:opts]
347
- if !options.has_key?(:instance)
348
- @id = opts[:db_instance_identifier]
349
- snapshot_name = options[:snapshot_name]
350
- if 0 < @region.find_db_instances({:instance_id => @id}).length
351
- log "Error, instance: #{@id} already exists"
352
- return
353
- end
354
- last = self.get_latest_db_snapshot({:snapshot_name => snapshot_name})
355
- log "Restoring: #{last.db_instance_identifier}, snapshot: #{last.db_instance_identifier} from : #{last.snapshot_create_time}"
356
- opts[:db_snapshot_identifier] = last.db_snapshot_identifier
357
- response = @region.rds.restore_db_instance_from_db_snapshot(opts)
358
- @_instance = response[:db_instance]
359
- @region.rds.add_tags_to_resource({:resource_name => "arn:aws:rds:#{@region.region}:#{@region.account_id}:db:#{@id}",
360
- :tags => [{:key => "environment", :value => options[:environment]},
361
- {:key => "purpose", :value => options[:purpose]}]})
362
-
363
- self.wait
364
-
365
- opts = {:db_instance_identifier => @id,
366
- :vpc_security_group_ids => options[:vpc_security_group_ids]}
367
- @region.rds.modify_db_instance(opts)
368
- else
369
- @_instance = options[:instance]
370
- @id = @_instance[:db_instance_identifier]
371
- end
372
- @tags = {}
373
- _tags = @region.rds.list_tags_for_resource({:resource_name => "arn:aws:rds:#{@region.region}:#{@region.account_id}:db:#{@id}"})
374
- _tags[:tag_list].each do |t|
375
- @tags[t[:key].to_sym] = t[:value]
376
- end
377
- @endpoint = @_instance.endpoint[:address]
378
- end
379
-
380
- # Delete a database and be sure to capture a final stapshot
381
- # @return [Boolean] - A return value of true only means that the command was issued. The caller should follow up later with a call to determine status in order to know when the delete has been completed
382
- def delete(options={})
383
- log "Deleting database: #{@id}"
384
- opts = {:db_instance_identifier => @id,
385
- :skip_final_snapshot => false,
386
- :final_db_snapshot_identifier => "#{@id}-#{Time.now.strftime("%Y-%m-%d-%H-%M-%S")}"}
387
- begin
388
- i = @region.rds.delete_db_instance(opts)
389
- rescue Exception => e
390
- return false
391
- end
392
- true
393
- end
394
-
395
- # Purge db snapshots, keep just one - the latest
396
- # @return [Boolean]
397
- def purge_db_snapshots
398
- latest = 0
399
- @region.rds.describe_db_snapshots[:db_snapshots].each do |i|
400
- if i.snapshot_type == "manual" and i.db_instance_identifier == @id
401
- if i.snapshot_create_time.to_i > latest
402
- latest = i.snapshot_create_time.to_i
403
- end
404
- end
405
- end
406
- @region.rds.describe_db_snapshots[:db_snapshots].each do |i|
407
- if i.snapshot_type == "manual" and i.db_instance_identifier == @id
408
- if i.snapshot_create_time.to_i != latest
409
- log "Removing snapshot: #{i.db_snapshot_identifier}/#{i.snapshot_create_time.to_s}"
410
- begin
411
- @region.rds.delete_db_snapshot({:db_snapshot_identifier => i.db_snapshot_identifier})
412
- rescue
413
- log "Error removing snapshot: #{i.db_snapshot_identifier}/#{i.snapshot_create_time.to_s}"
414
- return false
415
- end
416
- else
417
- log "Keeping snapshot: #{i.db_snapshot_identifier}/#{i.snapshot_create_time.to_s}"
418
- end
419
- end
420
- end
421
- true
422
- end
423
-
424
- # Wait for the database to get to a state - we are usually waiting for it to be "available"
425
- # @param options [Hash] - Can be:
426
- # * :desired_status - Default: "available" - The RDS Status that is sought
427
- # * :timeout - Default: 600 seconds. - The time to wait for the status before returning failure
428
- # @return [Boolean]
429
- def wait(options = {:desired_status => "available",
430
- :timeout => 600})
431
- inst = @region.find_db_instances({:instance_id => @id})[0]
432
- if !inst
433
- log "Error, instance: #{@id} not found"
434
- return false
435
- end
436
- t0 = Time.now.to_i
437
- while inst.status != options[:desired_status]
438
- inst = @region.find_db_instances({:instance_id => @id})[0]
439
- log "Database: #{@id} at #{@endpoint}. Current status: #{inst.status}"
440
- if Time.now.to_i - t0 > options[:timeout]
441
- log "Timed out waiting for database: #{@id} at #{@endpoint} to move into status: #{options[:desired_status]}. Current status: #{inst.status}"
442
- return false
443
- end
444
- sleep 20
445
- end
446
- return true
447
- end
448
-
449
- # Get the status of a database
450
- # @return [String] - Current status of this database
451
- def status
452
- @_instance.db_instance_status
453
- end
454
-
455
- # Get the name of the latest snapshot
456
- # @return [Hash] - Hash describing RDS Snapshot. See [RDS Tech Docs](http://docs.aws.amazon.com/sdkforruby/api/Aws/RDS/V20130909.html)
457
- def get_latest_db_snapshot(options={})
458
- snapshot_name = options.has_key?(:snapshot_name) ? options[:snapshot_name] : @id
459
-
460
- last = nil
461
- last_t = 0
462
- @region.rds.describe_db_snapshots[:db_snapshots].each do |i|
463
- if i.db_instance_identifier == snapshot_name and (last.nil? or i.snapshot_create_time > last_t)
464
- last = i
465
- last_t = i.snapshot_create_time
466
- end
467
- end
468
- last
469
- end
470
-
471
- end
472
-
473
- # Class to handle EC2 Instances
474
- class AwsInstance < AwsBase
475
- attr_accessor :id, :tags, :region, :private_ip, :public_ip, :_instance
476
-
477
- def initialize(region, options = {})
478
- @tags = {}
479
- @region = region
480
- if options.has_key?(:instance)
481
- @_instance = options[:instance]
482
- @id = @_instance[:instance_id]
483
- @public_ip = @_instance[:public_ip_address]
484
- @private_ip = @_instance[:private_ip_address]
485
- else
486
- resp = @region.ec2.run_instances(options[:template])
487
- raise "Error creating instance using options" if resp.nil? or resp[:instances].length <= 0
488
- @_instance = resp[:instances][0]
489
- @id = @_instance[:instance_id]
490
- @tags = options[:tags]
491
- self.add_tags(@tags)
492
- self.wait
493
- instance = @region.ec2.describe_instances(:instance_ids => [@id])[0][0].instances[0]
494
- @public_ip = instance[:public_ip_address]
495
- @private_ip = instance[:private_ip_address]
496
- raise "could not get ip address" if @public_ip.nil? && @private_ip.nil?
497
- self.inject_into_environment
498
- end
499
- @_instance.tags.each do |t|
500
- @tags[t[:key].to_sym] = t[:value]
501
- end
502
- end
503
-
504
-
505
- # Determine the state of an ec2 instance
506
- # @param use_cached_state [Boolean] - When true will use a cached version of the state rather than querying EC2 directly
507
- def state(use_cached_state=true)
508
- if !use_cached_state
509
- response = @region.ec2.describe_instances({instance_ids: [@id]})
510
- response[:reservations].each do |res|
511
- res[:instances].each do |inst|
512
- if inst[:instance_id] == @id
513
- return inst[:state][:name].strip()
514
- end
515
- end
516
- end
517
- return ""
518
- else
519
- @_instance.state[:name].strip()
520
- end
521
- end
522
-
523
- # Start an EC2 instance
524
- # @param wait [Boolean] - When true, will wait for instance to move into "running" state before returning
525
- def start(wait=false)
526
- if self.state(use_cached_state = false) != "stopped"
527
- log "Instance cannot be started - #{@region.region}://#{@id} is in the state: #{self.state}"
528
- return
529
- end
530
- log "Starting instance: #{@region.region}://#{@id}"
531
- @region.ec2.start_instances({:instance_ids => [@id]})
532
- if wait
533
- begin
534
- sleep 10
535
- log "Starting instance: #{@region.region}://#{@id} - state: #{self.state}"
536
- end while self.state(use_cached_state = false) != "running"
537
- end
538
- if @tags.has_key?("elastic_ip")
539
- @region.ec2.associate_address({:instance_id => @id, :public_ip => @tags['elastic_ip']})
540
- log "Associated ip: #{@tags['elastic_ip']} with instance: #{@id}"
541
- elsif @tags.has_key?("elastic_ip_allocation_id")
542
- @region.ec2.associate_address({:instance_id => @id, :allocation_id => @tags['elastic_ip_allocation_id']})
543
- log "Associated allocation id: #{@tags['elastic_ip_allocation_id']} with instance: #{@id}"
544
- end
545
- if @tags.has_key?("elastic_lb")
546
- self.add_to_lb(@tags["elastic_lb"])
547
- log "Adding instance: #{@id} to '#{@tags['elastic_lb']}' load balancer"
548
- end
549
- end
550
-
551
- # Add tags to an instance
552
- # @param h_tags [Hash] - Hash of tags to add to instance
553
- def add_tags(h_tags)
554
- tags = []
555
- h_tags.each do |k, v|
556
- tags << {:key => k.to_s, :value => v}
557
- end
558
- resp = @region.ec2.create_tags({:resources => [@id],
559
- :tags => tags})
560
- end
561
-
562
- # Add an instance to an elastic lb
563
- # @param lb_name [String] - Name of elastic load balancer
564
- def add_to_lb(lb_name)
565
- @region.elb.register_instances_with_load_balancer({:load_balancer_name => lb_name,
566
- :instances => [{:instance_id => @id}]})
567
- end
568
-
569
- # Remove instance from elastic lb
570
- # @param instance [AwsInstance] Instance to remove from lb
571
- # @param lb_name [String] Lb name from which the instance is to be removed
572
- # @return [Aws::PageableResponse]
573
- def remove_from_lb(lb_name)
574
- lb = @region.elb.describe_load_balancers({:load_balancer_names => [lb_name]})
575
- if lb and lb[:load_balancer_descriptions].length > 0
576
- lb[:load_balancer_descriptions][0][:instances].each do |lb_i|
577
- if lb_i[:instance_id] == @id
578
- @elb.deregister_instances_from_load_balancer({:load_balancer_name => lb_name,
579
- :instances => [{:instance_id => @id}]})
580
- sleep 30
581
- end
582
- end
583
- end
584
- end
585
-
586
- # Terminates ec2 instance
587
- def terminate()
588
- eject_from_environment
589
- @region.ec2.terminate_instances({:instance_ids => [@id]})
590
- end
591
-
592
- # Stops an ec2 instance
593
- # @param wait [Boolean] - When true, will wait for the instance to be completely stopped before returning
594
- def stop(wait=false)
595
- if self.state(use_cached_state = false) != "running"
596
- log "Instance cannot be stopped - #{@region.region}://#{@id} is in the state: #{self.state}"
597
- return
598
- end
599
- self.eject_from_environment
600
- if @tags.has_key?("elastic_lb")
601
- log "Removing instance: #{@id} from '#{@tags['elastic_lb']}' load balancer"
602
- remove_from_lb(tags["elastic_lb"])
603
- end
604
- log "Stopping instance: #{@region.region}://#{@id}"
605
- @region.ec2.stop_instances({:instance_ids => [@id]})
606
- while self.state(use_cached_state = false) != "stopped"
607
- sleep 10
608
- log "Stopping instance: #{@region.region}://#{@id} - state: #{self.state}"
609
- end if wait
610
- if self.state(use_cached_state = false) == "stopped"
611
- log "Instance stopped: #{@region.region}://#{@id}"
612
- end
613
- end
614
-
615
- # Connects using ssh to an ec2 instance
616
- def connect
617
- if self.state(use_cached_state = false) != "running"
618
- log "Cannot connect, instance: #{@region.region}://#{@id} due to its state: #{self.state}"
619
- return
620
- end
621
- ip = self.public_ip != "" ? self.public_ip : self.private_ip
622
- log "Connecting: ssh #{@tags[:user]}@#{ip}"
623
- exec "ssh #{@tags[:user]}@#{ip}"
624
- end
625
- def eject_from_environment
626
- if @tags.has_key?(:elastic_lb)
627
- log "Removing instance: #{@id} from '#{@tags[:elastic_lb]}' load balancer"
628
- self.remove_from_lb(tags[:elastic_lb])
629
- end
630
- if @tags.has_key?(:security_groups_foreign)
631
- self.revoke_sg_ingress(@tags[:security_groups_foreign].split(","))
632
- end
633
- end
634
-
635
- def inject_into_environment
636
- if @tags.has_key?(:elastic_ip)
637
- @region.ec2.associate_address({:instance_id => @id, :public_ip => @tags[:elastic_ip]})
638
- log "Associated ip: #{@tags[:elastic_ip]} with instance: #{@id}"
639
- elsif @tags.has_key?(:elastic_ip_allocation_id)
640
- @region.ec2.associate_address({:instance_id => @id, :allocation_id => @tags[:elastic_ip_allocation_id]})
641
- log "Associated allocation id: #{@tags[:elastic_ip_allocation_id]} with instance: #{@id}"
642
- end
643
- if @tags.has_key?(:mount_points)
644
- mounts = @tags[:mount_points].split(";")
645
- mounts.each do |mnt|
646
- (volume_id,device) = mnt.split(",")
647
- log "Mounting volume: #{volume_id} on #{device}"
648
- self.mount(volume_id, device)
649
- end
650
- end
651
- if @tags.has_key?(:security_group_ids)
652
- self.set_security_groups(@tags[:security_group_ids].split(","))
653
- end
654
- if @tags.has_key?(:security_groups_foreign)
655
- self.authorize_sg_ingress(@tags[:security_groups_foreign].split(","))
656
- end
657
-
658
- # if any of the above fails, we probably do not want it in the lb
659
- if @tags.has_key?(:elastic_lb)
660
- self.add_to_lb(@tags[:elastic_lb])
661
- log "Adding instance: #{@id} to '#{@tags[:elastic_lb]}' load balancer"
662
- end
663
- end
664
- def wait(options = {:timeout => 300, :desired_status => "running"})
665
- t0 = Time.now.to_i
666
- begin
667
- sleep 10
668
- log "Waiting on instance: #{@region.region}://#{@id} - current state: #{self.state}"
669
- return if Time.now.to_i - t0 > options[:timeout]
670
- end while self.state(use_cached_state = false) != options[:desired_status]
671
- end
672
-
673
- def set_security_groups(groups)
674
- # only works on instances in a vpc
675
- @region.ec2.modify_instance_attribute({:instance_id => @id, :groups => groups})
676
- end
677
-
678
- # Does a security group allow ingress on a port for the public IP of this instance
679
- # @param group_port [String] - security_group:port like "sg_xxxxxx:8080"
680
- # @return [Boolean] true if the security group allows ingress for the public IP of this instance on a certain port
681
- def has_sg_rule?(group_port)
682
- options = get_simple_sg_options(group_port)
683
- options_cidr_ip = options[:ip_permissions][0][:ip_ranges][0][:cidr_ip]
684
- group_id = options[:group_id]
685
- raise "missing security group_id" if group_id.nil?
686
- sg = @region.ec2.describe_security_groups(:group_ids => [group_id]).data.security_groups[0]
687
- sg.ip_permissions.each do |p|
688
- if p.ip_protocol == "tcp" &&
689
- p.from_port == options[:ip_permissions][0][:from_port] &&
690
- p.to_port == options[:ip_permissions][0][:to_port]
691
- p[:ip_ranges].each do |ipr|
692
- return true if ipr.cidr_ip == options_cidr_ip
693
- end
694
- end
695
- end
696
- false
697
- end
698
-
699
- # authorize security group ingress for public ip of this instance on port
700
- # @param groups [Array] - each element is String: "security_group_id:port". For example: ["sg-0xxxxx:80", "sg-0xxxxx:8443", "sg-0yyyyy:3000"]
701
- def authorize_sg_ingress(groups)
702
- raise "no public ip" unless @public_ip.to_s.match /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
703
- groups.each do |gp|
704
- options = get_simple_sg_options(gp)
705
- if has_sg_rule?(gp)
706
- log "security group rule #{gp} for #{self.public_ip} already exists"
707
- else
708
- @region.ec2.authorize_security_group_ingress options
709
- log "added #{self.public_ip} to security group for :port #{gp}"
710
- end
711
- end
712
- end
713
-
714
- # revoke security group ingress for public ip of this instance on port
715
- # @param groups [Array] - each element is String: "security_group_id:port". For example: ["sg-0xxxxx:80", "sg-0xxxxx:8443", "sg-0yyyyy:3000"]
716
- def revoke_sg_ingress(groups)
717
- # revoke the public ip of this instance for ingress on port for security group
718
- # groups is array of strings: security_group_id:port
719
- raise "no public ip" unless @public_ip.to_s.match /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
720
- groups.each do |gp|
721
- options = get_simple_sg_options(gp)
722
- if has_sg_rule?(gp)
723
- @region.ec2.revoke_security_group_ingress options
724
- log "removed #{self.public_ip} from security group for :port #{gp}"
725
- else
726
- log "not removing #{self.public_ip} rule #{gp} because it does not exist"
727
- end
728
- end
729
- end
730
- def mount(volume_id, device)
731
- @region.ec2.attach_volume({:instance_id => @id,
732
- :volume_id => volume_id,
733
- :device => device})
734
- end
735
-
736
- # construct hash for authorize/revoke ingress and has_sg_rule?
737
- # @param group_id_port [String] - security_group:port like "sg_xxxxxx:8080"
738
- # @return [Hash] - Hash for ec2 call for security group management
739
- def get_simple_sg_options(group_id_port)
740
- security_group_id, port = group_id_port.split(':')
741
- port = port.to_s.to_i
742
- raise "no security group id" unless security_group_id.to_s.length > 0
743
- raise "no, or invalid port" unless port.to_s.to_i > 0
744
- {:group_id => security_group_id,
745
- :ip_permissions => [ :ip_protocol => "tcp",
746
- :from_port => port,
747
- :to_port => port,
748
- :ip_ranges => [:cidr_ip => "#{self.public_ip}/32"]
749
- ]
750
- }
751
- end
752
-
753
-
754
- end
755
- end
1
+ require 'aws-sdk'
2
+ require 'yaml'
3
+ require 'json'
4
+
5
+ class AwsBase
6
+ def log(msg)
7
+ @logger.write("#{Time.now.strftime("%b %D, %Y %H:%S:%M:")} #{msg}\n") if @logger
8
+ puts "#{Time.now.strftime("%b %D, %Y %H:%S:%M:")} #{msg}\n"
9
+ end
10
+ end
11
+
12
+ # AwsRegion is a simplified wrapper on top of a few of the Aws core objects
13
+ # The main goal is to expose a extremely simple interface for some our most
14
+ # frequently used Aws facilities.
15
+ class AwsRegion < AwsBase
16
+ attr_accessor :ec2, :region, :rds, :account_id, :elb, :cw, :s3, :sns
17
+ REGIONS = {'or' => "us-west-2", 'ca' => "us-west-1", 'va' => 'us-east-1'}
18
+
19
+ # @param region [String] must be one of the keys of the {AwsRegion::REGIONS REGIONS} static hash
20
+ # @param account_id [String] Aws account id
21
+ # @param access_key_id [String] Aws access key id
22
+ # @param secret_access_key [String] Aws secret access key
23
+ def initialize(region, account_id, access_key_id, secret_access_key, logger = nil)
24
+ @logger = logger
25
+ @region = REGIONS[region]
26
+ @account_id = account_id
27
+ Aws.config = {:access_key_id => access_key_id,
28
+ :secret_access_key => secret_access_key}
29
+ @ec2 = Aws::EC2::Client.new(region: @region)
30
+ @rds = Aws::RDS::Client.new(region: @region)
31
+ @elb = Aws::ElasticLoadBalancing::Client.new(region: @region)
32
+ @cw = Aws::CloudWatch::Client.new(region: @region)
33
+ @s3 = Aws::S3::Client.new(region: @region)
34
+ @sns = Aws::SNS::Client.new(region: @region)
35
+ end
36
+
37
+ # Simple EC2 Instance finder. Can find using instance_id, or using
38
+ # :environment and :purpose instance tags which must both match.
39
+ #
40
+ # @param options [Hash] containing search criteria. Values can be:
41
+ # * :instance_id - identifies an exact instance
42
+ # * :environment - instance tag
43
+ # * :purpose - instance tag
44
+ # @return [Array<AwsInstance>] instances found to match criteria
45
+ def find_instances(options={})
46
+ instances = []
47
+ @ec2.describe_instances[:reservations].each do |i|
48
+ i.instances.each do |y|
49
+ instance = AwsInstance.new(self, {:instance => y})
50
+ if instance.state != 'terminated'
51
+ if options.has_key?(:environment) and options.has_key?(:purpose)
52
+ instances << instance if instance.tags[:environment] == options[:environment] and instance.tags[:purpose] == options[:purpose]
53
+ elsif options.has_key?(:instance_id)
54
+ instances << instance if instance.id == options[:instance_id]
55
+ end
56
+ end
57
+ end
58
+ end
59
+ return instances
60
+ end
61
+
62
+ # Simple DB Instance finder. Can find using instance_id, or using
63
+ # :environment and :purpose instance tags which must both match.
64
+ #
65
+ # @param options [Hash] containing search criteria. Values can be:
66
+ # * :instance_id - identifies an exact instance
67
+ # * :environment - instance tag
68
+ # * :purpose - instance tag
69
+ # @return [Array<AwsDbInstance>] instances found to match criteria
70
+ def find_db_instances(options={})
71
+ instances = []
72
+ @rds.describe_db_instances[:db_instances].each do |i|
73
+ instance = AwsDbInstance.new(self, {:instance => i})
74
+ if options.has_key?(:instance_id)
75
+ instance.id == options[:instance_id]
76
+ instances << instance
77
+ elsif instance.tags[:environment] == options[:environment] and
78
+ instance.tags[:purpose] == options[:purpose]
79
+ instances << instance
80
+ end
81
+ end
82
+ instances
83
+ end
84
+
85
+
86
+ # Search region for a bucket by name
87
+ #
88
+ # @param options [Hash] containing search criteria. Values can be:
89
+ # * :bucket - Bucket name
90
+ # @return [Array<AwsBucket>] instances found to match criteria
91
+ def find_buckets(options={})
92
+ buckets = []
93
+ _buckets = @s3.list_buckets()
94
+ _buckets[:buckets].each do |b|
95
+ buckets << AwsBucket.new(self, {id: b[:name]}) if b[:name] == options[:bucket]
96
+ end
97
+ buckets
98
+ end
99
+
100
+ # Construct new EC2 instance
101
+ #
102
+ # @param options [Hash] containing initialization parameters. See {AwsInstance#initialize}
103
+ # @return [AwsInstance]
104
+ def create_instance(options={})
105
+ AwsInstance.new(self, options)
106
+ end
107
+
108
+ # Construct new DB instance
109
+ #
110
+ # @param options [Hash] containing initialization parameters. See {AwsDbInstance#initialize}
111
+ # @return [AwsDbInstance]
112
+ def create_db_instance(options={})
113
+ AwsDbInstance.new(self, options)
114
+ end
115
+
116
+ # Construct new CloudWatch instance
117
+ #
118
+ # @param options [Hash] containing initialization parameters. See {AwsCw#initialize}
119
+ # @return [AwsCw]
120
+ def create_cw_instance(options={})
121
+ AwsCw.new(self, options)
122
+ end
123
+
124
+ # Construct new AwsBucket instance
125
+ #
126
+ # @param options [Hash] containing initialization parameters. See {AwsBucket#initialize}
127
+ # @return [AwsBucket]
128
+ def create_bucket(options={})
129
+ AwsBucket.new(self, options)
130
+ end
131
+
132
+ def create_sns_instance
133
+ AwsSns.new(self)
134
+ end
135
+
136
+ class AwsSns
137
+ attr_accessor :region
138
+ def initialize(region)
139
+ @region = region
140
+ end
141
+ def publish(topic_arn, subject) #, message)
142
+ @region.sns.publish(topic_arn: topic_arn, message: "unused for texts", subject: subject)
143
+ end
144
+ end
145
+
146
+ # Methods for dealing with CloudWatch
147
+ class AwsCw < AwsBase
148
+ attr_accessor :region
149
+
150
+ # @param region [String] - Value from REGION static hash
151
+ def initialize(region, options={})
152
+ @region = region
153
+ end
154
+
155
+ # Put a cw metric
156
+ # @param arg_csv [String] - CSV row: "namespace,name,value,dims"
157
+ # * Note that dims is formatted as an arbitrary semicolon separated list of name:value dimensions. For example:
158
+ # * "activeservers,count,10,env:prod;purp:test"
159
+ # @return [Aws::PageableResponse]
160
+ def put_metric(arg_csv)
161
+ (namespace, name, value, dims) = arg_csv.split(",")
162
+ dimensions = []
163
+ dims.split(";").each do |d|
164
+ (n, v) = d.split(":")
165
+ dimensions << {:name => n, :value => v}
166
+ end
167
+ args = {:namespace => namespace}
168
+ metric ={:metric_name => name, :value => value.to_f, :timestamp => Time.now, :dimensions => dimensions}
169
+ args[:metric_data] = [metric]
170
+ @region.cw.put_metric_data(args)
171
+ end
172
+ end
173
+
174
+ # Methods for dealing with S3 buckets
175
+ class AwsBucket < AwsBase
176
+ attr_accessor :region
177
+
178
+ # Constructs a bucket instance from an existing bucket, or creates a new one with the name
179
+ # @param region [String] - Value from REGION static hash
180
+ # @param options [Hash] - Possible values:
181
+ # * :id - id of existing bucket
182
+ # * :bucket - Name of bucket to find or create
183
+ def initialize(region, options={})
184
+ @region = region
185
+ if options.has_key?(:id)
186
+ @id = options[:id]
187
+ elsif options.has_key?(:bucket)
188
+ bucket = options[:bucket]
189
+ if @region.find_buckets({bucket: bucket}).length <= 0
190
+ @region.s3.create_bucket({:bucket => bucket,
191
+ :create_bucket_configuration => {:location_constraint => @region.region}})
192
+ if @region.find_buckets({bucket: bucket}).length <= 0
193
+ raise "Error creating bucket: #{bucket} in region: #{@region.region}"
194
+ end
195
+ end
196
+ @id = bucket
197
+ end
198
+ end
199
+
200
+ # Delete this bucket instance
201
+ # @return [AwsPageableResponse]]
202
+ def delete
203
+ @region.s3.delete_bucket({bucket: @id})
204
+ end
205
+
206
+ # Put a local file to this bucket
207
+ # @param filename [String] - local file name
208
+ # @param file_identity [String] - S3 file path
209
+ # @return [AwsPageableResponse]
210
+ def put_file(filename, file_identity)
211
+ File.open(filename, 'r') do |reading_file|
212
+ resp = @region.s3.put_object(
213
+ acl: "bucket-owner-full-control",
214
+ body: reading_file,
215
+ bucket: @id,
216
+ key: file_identity
217
+ )
218
+ end
219
+ end
220
+
221
+ # puts a local file to an s3 object in bucket on path
222
+ # example: put_local_file(:bucket=>"bucket", :local_file_path=>"/tmp/bar/foo.txt", :aws_path=>"b")
223
+ # would make an s3 object named foo.txt in bucket/b
224
+ # @param local_file_path [String] - Location of file to put
225
+ # @param aws_path [String] - S3 path to put the file
226
+ # @param options [Hash] - Can contain any valid S3 bucket options see [docs](http://docs.aws.amazon.com/sdkforruby/api/frames.html)
227
+ def put(local_file_path, aws_path, options={})
228
+ aws_path = aws_path[0..-2] if aws_path[-1..-1] == '/'
229
+ s3_path = "#{aws_path}/#{File.basename(local_file_path)}"
230
+ log "s3 writing #{local_file_path} to bucket #{@id} path: #{aws_path} s3 path: #{s3_path}"
231
+ f = File.open local_file_path, 'rb'
232
+ options[:bucket] = @id
233
+ options[:key] = s3_path
234
+ options[:body] = f
235
+ options[:storage_class] = 'REDUCED_REDUNDANCY'
236
+ result = @region.s3.put_object(params=options)
237
+ f.close
238
+ result
239
+ end
240
+
241
+ # prefix is something like: hchd-A-A-Items
242
+ # This will return in an array of strings the names of all objects in s3 in
243
+ # the :aws_path under :bucket starting with passed-in prefix
244
+ # example: :aws_path=>'development', :prefix=>'broadhead'
245
+ # would return array of names of objects in said bucket
246
+ # matching (in regex terms) development/broadhead.*
247
+ # @param options [Hash] - Can contain:
248
+ # * :aws_path - first part of S3 path to search
249
+ # * :prefix - Actually suffix of path to search.
250
+ # @return [Array<Hash>] - 0 or more objects
251
+ def find(options={})
252
+ aws_path = options[:aws_path]
253
+ prefix = options[:prefix]
254
+ aws_path = '' if aws_path.nil?
255
+ aws_path = aws_path[0..-2] if aws_path[-1..-1] == '/'
256
+ log "s3 searching bucket:#{@id} for #{aws_path}/#{prefix}"
257
+ objects = @region.s3.list_objects(:bucket => @id,
258
+ :prefix => "#{aws_path}/#{prefix}")
259
+ f = objects.contents.collect(&:key)
260
+ log "s3 searched got: #{f.inspect}"
261
+ f
262
+ end
263
+
264
+ # writes contents of S3 object to local file
265
+ # example: get( :s3_path_to_object=>development/myfile.txt',
266
+ # :dest_file_path=>'/tmp/foo.txt')
267
+ # would write to local /tmp/foo.txt a file retrieved from s3
268
+ # at development/myfile.txt
269
+ # @param options [Hash] - Can contain:
270
+ # * :s3_path_to_object - S3 object path
271
+ # * :dest_file_path - local file were file will be written
272
+ # @return [Boolean]
273
+ def get(options={})
274
+ begin
275
+ s3_path_to_object = options[:s3_path_to_object]
276
+ dest_file_path = options[:dest_file_path]
277
+ File.delete dest_file_path if File.exists?(dest_file_path)
278
+ log "s3 get bucket:#{@id} path:#{s3_path_to_object} dest:#{dest_file_path}"
279
+ response = @region.s3.get_object(:bucket => @id,
280
+ :key => s3_path_to_object)
281
+ response.body.rewind
282
+ File.open(dest_file_path, 'wb') do |file|
283
+ response.body.each { |chunk| file.write chunk }
284
+ end
285
+ rescue Exception => e
286
+ return false
287
+ end
288
+ true
289
+ end
290
+
291
+ # deletes from s3 an object at :s3_path_to_object
292
+ # @param options [Hash] - Can be:
293
+ # * :s3_path_to_object
294
+ # @return [Boolean]
295
+ def delete_object(options={})
296
+ begin
297
+ s3_path_to_object = options[:s3_path_to_object]
298
+ log "s3 delete #{s3_path_to_object}"
299
+ @region.s3.delete_object(:bucket => @id,
300
+ :key => s3_path_to_object)
301
+ rescue Exception => e
302
+ return false
303
+ end
304
+ true
305
+ end
306
+
307
+ # delete all objects in a bucket
308
+ # @return [Boolean]
309
+ def delete_all_objects
310
+ begin
311
+ response = @region.s3.list_objects({:bucket => @id})
312
+ response[:contents].each do |obj|
313
+ @region.s3.delete_object(:bucket => @id,
314
+ :key => obj[:key])
315
+ end
316
+ rescue Exception => e
317
+ return false
318
+ end
319
+ true
320
+ end
321
+ end
322
+
323
+ # Class to handle RDS Db instances
324
+ class AwsDbInstance < AwsBase
325
+ attr_accessor :id, :tags, :region, :endpoint
326
+
327
+ # Creates an AwsDbInstance for an existing instance or creates a new database
328
+ # @param region [String] - - Value from REGION static hash
329
+ # @param options [Hash] - Can contain:
330
+ # * :instance - If specified, create an instance of this class using this RDS instance.
331
+ # * :opts - [Hash] - Includes parameters for constructing the database. The format is:
332
+ # * :db_instance_identifier - RDS instance identifier
333
+ # * :db_subnet_group_name - DB Subgroup name
334
+ # * :publicly_accessible - [true|false]
335
+ # * :db_instance_class - RDS db instance class
336
+ # * :availability_zone - RDS/Aws availability zone
337
+ # * :multi_az - [true|false]
338
+ # * :engine - RDS engine (Only tested with Mysql at this point)
339
+ # * :tags - Tags to be applied to RDS instance. The follow are required. Arbitrary tags may also be added.
340
+ # * :environment - Environment designation - can be anything. Used to locate instance with other aws-tools
341
+ # * :purpose - Purpose designation - can be anything. Used to locate instance with other aws-tools
342
+ # * :name - Name will appear in the Aws web page if you set this
343
+ # * :snapshot_name - Name of the snapshot that will be used to construct the new instance. This name will be matched with the RDS db_instance_identifier. The latest snapshot will be used.
344
+ # * :vpc_security_group_ids: - Comma separated list of security groups that will be applied to this instance
345
+ def initialize(region, options = {})
346
+ @region = region
347
+ opts = options[:opts]
348
+ if !options.has_key?(:instance)
349
+ @id = opts[:db_instance_identifier]
350
+ snapshot_name = options[:snapshot_name]
351
+ if 0 < @region.find_db_instances({:instance_id => @id}).length
352
+ log "Error, instance: #{@id} already exists"
353
+ return
354
+ end
355
+ last = self.get_latest_db_snapshot({:snapshot_name => snapshot_name})
356
+ log "Restoring: #{last.db_instance_identifier}, snapshot: #{last.db_instance_identifier} from : #{last.snapshot_create_time}"
357
+ opts[:db_snapshot_identifier] = last.db_snapshot_identifier
358
+ response = @region.rds.restore_db_instance_from_db_snapshot(opts)
359
+ @_instance = response[:db_instance]
360
+ @region.rds.add_tags_to_resource({:resource_name => "arn:aws:rds:#{@region.region}:#{@region.account_id}:db:#{@id}",
361
+ :tags => [{:key => "environment", :value => options[:environment]},
362
+ {:key => "purpose", :value => options[:purpose]}]})
363
+
364
+ self.wait
365
+
366
+ opts = {:db_instance_identifier => @id,
367
+ :vpc_security_group_ids => options[:vpc_security_group_ids]}
368
+ @region.rds.modify_db_instance(opts)
369
+ else
370
+ @_instance = options[:instance]
371
+ @id = @_instance[:db_instance_identifier]
372
+ end
373
+ @tags = {}
374
+ _tags = @region.rds.list_tags_for_resource({:resource_name => "arn:aws:rds:#{@region.region}:#{@region.account_id}:db:#{@id}"})
375
+ _tags[:tag_list].each do |t|
376
+ @tags[t[:key].to_sym] = t[:value]
377
+ end
378
+ @endpoint = @_instance.endpoint[:address]
379
+ end
380
+
381
+ # Delete a database and be sure to capture a final stapshot
382
+ # @return [Boolean] - A return value of true only means that the command was issued. The caller should follow up later with a call to determine status in order to know when the delete has been completed
383
+ def delete(options={})
384
+ log "Deleting database: #{@id}"
385
+ opts = {:db_instance_identifier => @id,
386
+ :skip_final_snapshot => false,
387
+ :final_db_snapshot_identifier => "#{@id}-#{Time.now.strftime("%Y-%m-%d-%H-%M-%S")}"}
388
+ begin
389
+ i = @region.rds.delete_db_instance(opts)
390
+ rescue Exception => e
391
+ return false
392
+ end
393
+ true
394
+ end
395
+
396
+ # Purge db snapshots, keep just one - the latest
397
+ # @return [Boolean]
398
+ def purge_db_snapshots
399
+ latest = 0
400
+ @region.rds.describe_db_snapshots[:db_snapshots].each do |i|
401
+ if i.snapshot_type == "manual" and i.db_instance_identifier == @id
402
+ if i.snapshot_create_time.to_i > latest
403
+ latest = i.snapshot_create_time.to_i
404
+ end
405
+ end
406
+ end
407
+ @region.rds.describe_db_snapshots[:db_snapshots].each do |i|
408
+ if i.snapshot_type == "manual" and i.db_instance_identifier == @id
409
+ if i.snapshot_create_time.to_i != latest
410
+ log "Removing snapshot: #{i.db_snapshot_identifier}/#{i.snapshot_create_time.to_s}"
411
+ begin
412
+ @region.rds.delete_db_snapshot({:db_snapshot_identifier => i.db_snapshot_identifier})
413
+ rescue
414
+ log "Error removing snapshot: #{i.db_snapshot_identifier}/#{i.snapshot_create_time.to_s}"
415
+ return false
416
+ end
417
+ else
418
+ log "Keeping snapshot: #{i.db_snapshot_identifier}/#{i.snapshot_create_time.to_s}"
419
+ end
420
+ end
421
+ end
422
+ true
423
+ end
424
+
425
+ # Wait for the database to get to a state - we are usually waiting for it to be "available"
426
+ # @param options [Hash] - Can be:
427
+ # * :desired_status - Default: "available" - The RDS Status that is sought
428
+ # * :timeout - Default: 600 seconds. - The time to wait for the status before returning failure
429
+ # @return [Boolean]
430
+ def wait(options = {:desired_status => "available",
431
+ :timeout => 600})
432
+ inst = @region.find_db_instances({:instance_id => @id})[0]
433
+ if !inst
434
+ log "Error, instance: #{@id} not found"
435
+ return false
436
+ end
437
+ t0 = Time.now.to_i
438
+ while inst.status != options[:desired_status]
439
+ inst = @region.find_db_instances({:instance_id => @id})[0]
440
+ log "Database: #{@id} at #{@endpoint}. Current status: #{inst.status}"
441
+ if Time.now.to_i - t0 > options[:timeout]
442
+ log "Timed out waiting for database: #{@id} at #{@endpoint} to move into status: #{options[:desired_status]}. Current status: #{inst.status}"
443
+ return false
444
+ end
445
+ sleep 20
446
+ end
447
+ return true
448
+ end
449
+
450
+ # Get the status of a database
451
+ # @return [String] - Current status of this database
452
+ def status
453
+ @_instance.db_instance_status
454
+ end
455
+
456
+ # Get the name of the latest snapshot
457
+ # @return [Hash] - Hash describing RDS Snapshot. See [RDS Tech Docs](http://docs.aws.amazon.com/sdkforruby/api/Aws/RDS/V20130909.html)
458
+ def get_latest_db_snapshot(options={})
459
+ snapshot_name = options.has_key?(:snapshot_name) ? options[:snapshot_name] : @id
460
+
461
+ last = nil
462
+ last_t = 0
463
+ @region.rds.describe_db_snapshots[:db_snapshots].each do |i|
464
+ if i.db_instance_identifier == snapshot_name and (last.nil? or i.snapshot_create_time > last_t)
465
+ last = i
466
+ last_t = i.snapshot_create_time
467
+ end
468
+ end
469
+ last
470
+ end
471
+
472
+ end
473
+
474
+ # Class to handle EC2 Instances
475
+ class AwsInstance < AwsBase
476
+ attr_accessor :id, :tags, :region, :private_ip, :public_ip, :_instance
477
+
478
+ def initialize(region, options = {})
479
+ @tags = {}
480
+ @region = region
481
+ if options.has_key?(:instance)
482
+ @_instance = options[:instance]
483
+ @id = @_instance[:instance_id]
484
+ @public_ip = @_instance[:public_ip_address]
485
+ @private_ip = @_instance[:private_ip_address]
486
+ else
487
+ resp = @region.ec2.run_instances(options[:template])
488
+ raise "Error creating instance using options" if resp.nil? or resp[:instances].length <= 0
489
+ @_instance = resp[:instances][0]
490
+ @id = @_instance[:instance_id]
491
+ @tags = options[:tags]
492
+ self.add_tags(@tags)
493
+ self.wait
494
+ instance = @region.ec2.describe_instances(:instance_ids => [@id])[0][0].instances[0]
495
+ @public_ip = instance[:public_ip_address]
496
+ @private_ip = instance[:private_ip_address]
497
+ raise "could not get ip address" if @public_ip.nil? && @private_ip.nil?
498
+ self.inject_into_environment
499
+ end
500
+ @_instance.tags.each do |t|
501
+ @tags[t[:key].to_sym] = t[:value]
502
+ end
503
+ end
504
+
505
+
506
+ # Determine the state of an ec2 instance
507
+ # @param use_cached_state [Boolean] - When true will use a cached version of the state rather than querying EC2 directly
508
+ def state(use_cached_state=true)
509
+ if !use_cached_state
510
+ response = @region.ec2.describe_instances({instance_ids: [@id]})
511
+ response[:reservations].each do |res|
512
+ res[:instances].each do |inst|
513
+ if inst[:instance_id] == @id
514
+ return inst[:state][:name].strip()
515
+ end
516
+ end
517
+ end
518
+ return ""
519
+ else
520
+ @_instance.state[:name].strip()
521
+ end
522
+ end
523
+
524
+ # Start an EC2 instance
525
+ # @param wait [Boolean] - When true, will wait for instance to move into "running" state before returning
526
+ def start(wait=false)
527
+ if self.state(use_cached_state = false) != "stopped"
528
+ log "Instance cannot be started - #{@region.region}://#{@id} is in the state: #{self.state}"
529
+ return
530
+ end
531
+ log "Starting instance: #{@region.region}://#{@id}"
532
+ @region.ec2.start_instances({:instance_ids => [@id]})
533
+ if wait
534
+ begin
535
+ sleep 10
536
+ log "Starting instance: #{@region.region}://#{@id} - state: #{self.state}"
537
+ end while self.state(use_cached_state = false) != "running"
538
+ end
539
+ if @tags.has_key?("elastic_ip")
540
+ @region.ec2.associate_address({:instance_id => @id, :public_ip => @tags['elastic_ip']})
541
+ log "Associated ip: #{@tags['elastic_ip']} with instance: #{@id}"
542
+ elsif @tags.has_key?("elastic_ip_allocation_id")
543
+ @region.ec2.associate_address({:instance_id => @id, :allocation_id => @tags['elastic_ip_allocation_id']})
544
+ log "Associated allocation id: #{@tags['elastic_ip_allocation_id']} with instance: #{@id}"
545
+ end
546
+ if @tags.has_key?("elastic_lb")
547
+ self.add_to_lb(@tags["elastic_lb"])
548
+ log "Adding instance: #{@id} to '#{@tags['elastic_lb']}' load balancer"
549
+ end
550
+ end
551
+
552
+ # Add tags to an instance
553
+ # @param h_tags [Hash] - Hash of tags to add to instance
554
+ def add_tags(h_tags)
555
+ tags = []
556
+ h_tags.each do |k, v|
557
+ tags << {:key => k.to_s, :value => v}
558
+ end
559
+ resp = @region.ec2.create_tags({:resources => [@id],
560
+ :tags => tags})
561
+ end
562
+
563
+ # Add an instance to an elastic lb
564
+ # @param lb_name [String] - Name of elastic load balancer
565
+ def add_to_lb(lb_name)
566
+ @region.elb.register_instances_with_load_balancer({:load_balancer_name => lb_name,
567
+ :instances => [{:instance_id => @id}]})
568
+ end
569
+
570
+ # Remove instance from elastic lb
571
+ # @param instance [AwsInstance] Instance to remove from lb
572
+ # @param lb_name [String] Lb name from which the instance is to be removed
573
+ # @return [Aws::PageableResponse]
574
+ def remove_from_lb(lb_name)
575
+ lb = @region.elb.describe_load_balancers({:load_balancer_names => [lb_name]})
576
+ if lb and lb[:load_balancer_descriptions].length > 0
577
+ lb[:load_balancer_descriptions][0][:instances].each do |lb_i|
578
+ if lb_i[:instance_id] == @id
579
+ @elb.deregister_instances_from_load_balancer({:load_balancer_name => lb_name,
580
+ :instances => [{:instance_id => @id}]})
581
+ sleep 30
582
+ end
583
+ end
584
+ end
585
+ end
586
+
587
+ # Terminates ec2 instance
588
+ def terminate()
589
+ eject_from_environment
590
+ @region.ec2.terminate_instances({:instance_ids => [@id]})
591
+ end
592
+
593
+ # Stops an ec2 instance
594
+ # @param wait [Boolean] - When true, will wait for the instance to be completely stopped before returning
595
+ def stop(wait=false)
596
+ if self.state(use_cached_state = false) != "running"
597
+ log "Instance cannot be stopped - #{@region.region}://#{@id} is in the state: #{self.state}"
598
+ return
599
+ end
600
+ self.eject_from_environment
601
+ if @tags.has_key?("elastic_lb")
602
+ log "Removing instance: #{@id} from '#{@tags['elastic_lb']}' load balancer"
603
+ remove_from_lb(tags["elastic_lb"])
604
+ end
605
+ log "Stopping instance: #{@region.region}://#{@id}"
606
+ @region.ec2.stop_instances({:instance_ids => [@id]})
607
+ while self.state(use_cached_state = false) != "stopped"
608
+ sleep 10
609
+ log "Stopping instance: #{@region.region}://#{@id} - state: #{self.state}"
610
+ end if wait
611
+ if self.state(use_cached_state = false) == "stopped"
612
+ log "Instance stopped: #{@region.region}://#{@id}"
613
+ end
614
+ end
615
+
616
+ # Connects using ssh to an ec2 instance
617
+ def connect
618
+ if self.state(use_cached_state = false) != "running"
619
+ log "Cannot connect, instance: #{@region.region}://#{@id} due to its state: #{self.state}"
620
+ return
621
+ end
622
+ ip = (self.public_ip && self.public_ip.strip() != "") ? self.public_ip : self.private_ip
623
+ log "Connecting: ssh -i ~/.ssh/aws/#{@tags[:key]} #{@tags[:user]}@#{ip}"
624
+ exec "ssh -i ~/.ssh/aws/#{@tags[:key]} #{@tags[:user]}@#{ip}"
625
+ end
626
+ def eject_from_environment
627
+ if @tags.has_key?(:elastic_lb)
628
+ log "Removing instance: #{@id} from '#{@tags[:elastic_lb]}' load balancer"
629
+ self.remove_from_lb(tags[:elastic_lb])
630
+ end
631
+ if @tags.has_key?(:security_groups_foreign)
632
+ self.revoke_sg_ingress(@tags[:security_groups_foreign].split(","))
633
+ end
634
+ end
635
+
636
+ def inject_into_environment
637
+ if @tags.has_key?(:elastic_ip)
638
+ @region.ec2.associate_address({:instance_id => @id, :public_ip => @tags[:elastic_ip]})
639
+ log "Associated ip: #{@tags[:elastic_ip]} with instance: #{@id}"
640
+ elsif @tags.has_key?(:elastic_ip_allocation_id)
641
+ @region.ec2.associate_address({:instance_id => @id, :allocation_id => @tags[:elastic_ip_allocation_id]})
642
+ log "Associated allocation id: #{@tags[:elastic_ip_allocation_id]} with instance: #{@id}"
643
+ end
644
+ if @tags.has_key?(:mount_points)
645
+ mounts = @tags[:mount_points].split(";")
646
+ mounts.each do |mnt|
647
+ (volume_id,device) = mnt.split(",")
648
+ log "Mounting volume: #{volume_id} on #{device}"
649
+ self.mount(volume_id, device)
650
+ end
651
+ end
652
+ if @tags.has_key?(:security_group_ids)
653
+ self.set_security_groups(@tags[:security_group_ids].split(","))
654
+ end
655
+ if @tags.has_key?(:security_groups_foreign)
656
+ self.authorize_sg_ingress(@tags[:security_groups_foreign].split(","))
657
+ end
658
+
659
+ # if any of the above fails, we probably do not want it in the lb
660
+ if @tags.has_key?(:elastic_lb)
661
+ self.add_to_lb(@tags[:elastic_lb])
662
+ log "Adding instance: #{@id} to '#{@tags[:elastic_lb]}' load balancer"
663
+ end
664
+ end
665
+ def wait(options = {:timeout => 300, :desired_status => "running"})
666
+ t0 = Time.now.to_i
667
+ begin
668
+ sleep 10
669
+ log "Waiting on instance: #{@region.region}://#{@id} - current state: #{self.state}"
670
+ return if Time.now.to_i - t0 > options[:timeout]
671
+ end while self.state(use_cached_state = false) != options[:desired_status]
672
+ end
673
+
674
+ def set_security_groups(groups)
675
+ # only works on instances in a vpc
676
+ @region.ec2.modify_instance_attribute({:instance_id => @id, :groups => groups})
677
+ end
678
+
679
+ # Does a security group allow ingress on a port for the public IP of this instance
680
+ # @param group_port [String] - security_group:port like "sg_xxxxxx:8080"
681
+ # @return [Boolean] true if the security group allows ingress for the public IP of this instance on a certain port
682
+ def has_sg_rule?(group_port)
683
+ options = get_simple_sg_options(group_port)
684
+ options_cidr_ip = options[:ip_permissions][0][:ip_ranges][0][:cidr_ip]
685
+ group_id = options[:group_id]
686
+ raise "missing security group_id" if group_id.nil?
687
+ sg = @region.ec2.describe_security_groups(:group_ids => [group_id]).data.security_groups[0]
688
+ sg.ip_permissions.each do |p|
689
+ if p.ip_protocol == "tcp" &&
690
+ p.from_port == options[:ip_permissions][0][:from_port] &&
691
+ p.to_port == options[:ip_permissions][0][:to_port]
692
+ p[:ip_ranges].each do |ipr|
693
+ return true if ipr.cidr_ip == options_cidr_ip
694
+ end
695
+ end
696
+ end
697
+ false
698
+ end
699
+
700
+ # authorize security group ingress for public ip of this instance on port
701
+ # @param groups [Array] - each element is String: "security_group_id:port". For example: ["sg-0xxxxx:80", "sg-0xxxxx:8443", "sg-0yyyyy:3000"]
702
+ def authorize_sg_ingress(groups)
703
+ raise "no public ip" unless @public_ip.to_s.match /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
704
+ groups.each do |gp|
705
+ options = get_simple_sg_options(gp)
706
+ if has_sg_rule?(gp)
707
+ log "security group rule #{gp} for #{self.public_ip} already exists"
708
+ else
709
+ @region.ec2.authorize_security_group_ingress options
710
+ log "added #{self.public_ip} to security group for :port #{gp}"
711
+ end
712
+ end
713
+ end
714
+
715
+ # revoke security group ingress for public ip of this instance on port
716
+ # @param groups [Array] - each element is String: "security_group_id:port". For example: ["sg-0xxxxx:80", "sg-0xxxxx:8443", "sg-0yyyyy:3000"]
717
+ def revoke_sg_ingress(groups)
718
+ # revoke the public ip of this instance for ingress on port for security group
719
+ # groups is array of strings: security_group_id:port
720
+ raise "no public ip" unless @public_ip.to_s.match /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
721
+ groups.each do |gp|
722
+ options = get_simple_sg_options(gp)
723
+ if has_sg_rule?(gp)
724
+ @region.ec2.revoke_security_group_ingress options
725
+ log "removed #{self.public_ip} from security group for :port #{gp}"
726
+ else
727
+ log "not removing #{self.public_ip} rule #{gp} because it does not exist"
728
+ end
729
+ end
730
+ end
731
+ def mount(volume_id, device)
732
+ @region.ec2.attach_volume({:instance_id => @id,
733
+ :volume_id => volume_id,
734
+ :device => device})
735
+ end
736
+
737
+ # construct hash for authorize/revoke ingress and has_sg_rule?
738
+ # @param group_id_port [String] - security_group:port like "sg_xxxxxx:8080"
739
+ # @return [Hash] - Hash for ec2 call for security group management
740
+ def get_simple_sg_options(group_id_port)
741
+ security_group_id, port = group_id_port.split(':')
742
+ port = port.to_s.to_i
743
+ raise "no security group id" unless security_group_id.to_s.length > 0
744
+ raise "no, or invalid port" unless port.to_s.to_i > 0
745
+ {:group_id => security_group_id,
746
+ :ip_permissions => [ :ip_protocol => "tcp",
747
+ :from_port => port,
748
+ :to_port => port,
749
+ :ip_ranges => [:cidr_ip => "#{self.public_ip}/32"]
750
+ ]
751
+ }
752
+ end
753
+
754
+
755
+ end
756
+ end