aws-tools 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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