jnewland-capsize 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,568 @@
1
+ module Capsize
2
+ module CapsizeEC2
3
+ include Capsize
4
+
5
+
6
+ # HELPER METHODS
7
+ #########################################
8
+
9
+
10
+ def hostname_from_instance_id(instance_id = nil)
11
+ raise Exception, "Instance ID required" if instance_id.nil? || instance_id.empty?
12
+
13
+ amazon = connect()
14
+
15
+ response = amazon.describe_instances(:instance_id => instance_id)
16
+ return dns_name = response.reservationSet.item[0].instancesSet.item[0].dnsName
17
+ end
18
+
19
+ def hostnames_from_instance_ids(ids = [])
20
+ ids.collect { |id| hostname_from_instance_id(id) }
21
+ end
22
+
23
+ def hostnames_from_group(group_name = nil)
24
+ hostnames = []
25
+ return hostnames if group_name.nil?
26
+ instances = describe_instances
27
+ return hostnames if instances.nil?
28
+ return hostnames if instances.reservationSet.nil?
29
+ instances.reservationSet.item.each do |reservation|
30
+ hostname = nil
31
+ in_group = false
32
+ running = false
33
+ unless reservation.groupSet.nil?
34
+ reservation.groupSet.item.each do |group|
35
+ in_group = group.groupId == group_name
36
+ end
37
+ end
38
+
39
+ unless reservation.instancesSet.nil?
40
+ reservation.instancesSet.item.each do |instance|
41
+ hostname = instance.dnsName
42
+ running = (!instance.instanceState.nil? && (instance.instanceState.name == "running"))
43
+ end
44
+ end
45
+ hostnames << hostname if in_group and running
46
+ end
47
+ return hostnames
48
+ end
49
+
50
+ def role_from_security_group(role, security_group, *args)
51
+ options = args.last.is_a?(Hash) ? args.pop : {}
52
+ options = {:user => 'root', :ssh_options => { :keys => [capsize_ec2.get_key_file] }}.merge(options)
53
+ role(role, options) do
54
+ hostnames_from_group(security_group)
55
+ end
56
+ end
57
+
58
+ # build the key file path from key_dir and key_file
59
+ def get_key_file(options = {})
60
+ options = {:key_dir => nil, :key_name => nil}.merge(options)
61
+ key_dir = options[:key_dir] || get(:key_dir) || get(:capsize_secure_config_dir)
62
+ key_name = options[:key_name] || get(:key_name)
63
+ return key_file = [key_dir, "id_rsa-" + key_name].join('/')
64
+ end
65
+
66
+
67
+ # CONSOLE METHODS
68
+ #########################################
69
+
70
+
71
+ def get_console_output(options = {})
72
+ amazon = connect()
73
+ options = {:instance_id => ""}.merge(options)
74
+ amazon.get_console_output(:instance_id => options[:instance_id])
75
+ end
76
+
77
+
78
+ # KEYPAIR METHODS
79
+ #########################################
80
+
81
+
82
+ #describe your keypairs
83
+ def describe_keypairs(options = {})
84
+ amazon = connect()
85
+ options = {:key_name => []}.merge(options)
86
+ amazon.describe_keypairs(:key_name => options[:key_name])
87
+ end
88
+
89
+ #sets up a keypair named options[:key_name] and writes out the private key to options[:key_dir]
90
+ def create_keypair(options = {})
91
+ amazon = connect()
92
+
93
+ # default key_name is the same as our appname, unless specifically overriden in capsize.yml
94
+ # default key_dir is set in the :capsize_config_dir variable
95
+ options = {:key_name => nil, :key_dir => nil}.merge(options)
96
+
97
+ options[:key_name] = options[:key_name] || get(:key_name)
98
+ options[:key_dir] = options[:key_dir] || get(:key_dir) || get(:capsize_secure_config_dir)
99
+
100
+ #verify key_name and key_dir are set
101
+ raise Exception, "Keypair name required" if options[:key_name].nil? || options[:key_name].empty?
102
+ raise Exception, "Keypair directory required" if options[:key_dir].nil? || options[:key_dir].empty?
103
+
104
+ key_file = get_key_file(:key_name => options[:key_name], :key_dir => options[:key_dir])
105
+
106
+ # Verify keypair doesn't already exist on EC2 servers...
107
+ unless amazon.describe_keypairs(:key_name => options[:key_name]).keySet.nil?
108
+ raise Exception, "Sorry, a keypair with the name \"#{options[:key_name]}\" already exists on EC2."
109
+ end
110
+
111
+ # and doesn't exist locally either...
112
+ file_exists_message = <<-MESSAGE
113
+ \n
114
+ Warning! A keypair with the name \"#{key_file}\"
115
+ already exists on your local filesytem. You must remove it before trying to overwrite
116
+ again. Warning! Removing keypairs associated with active instances will prevent you
117
+ from accessing them via SSH or Capistrano!!\n\n
118
+ MESSAGE
119
+ raise Exception, file_exists_message if File.exists?(key_file)
120
+
121
+ #All is good, so we create the new keypair
122
+ private_key = amazon.create_keypair(:key_name => options[:key_name])
123
+
124
+ # write private key to file
125
+ File.open(key_file, 'w') do |file|
126
+ file.write(private_key.keyMaterial)
127
+ end
128
+
129
+ # Cross platform CHMOD, make the file owner +rw, group and other -all
130
+ File.chmod 0600, key_file
131
+ return [key_name, key_file]
132
+ end
133
+
134
+
135
+ # TODO : Is there a way to extract the 'puts' calls from here and make this have less 'view' code?
136
+ # Deletes a keypair from EC2 and from the local filesystem
137
+ def delete_keypair(options = {})
138
+ amazon = connect()
139
+
140
+ options = {:key_name => nil, :key_dir => nil}.merge(options)
141
+
142
+ options[:key_name] = options[:key_name] || get(:key_name)
143
+ options[:key_dir] = options[:key_dir] || get(:key_dir) || get(:capsize_secure_config_dir)
144
+
145
+ raise Exception, "Keypair name required" if options[:key_name].nil? || options[:key_name].empty?
146
+ raise Exception, "Keypair directory required" if options[:key_dir].nil? || options[:key_dir].empty?
147
+ raise Exception, "Keypair \"#{options[:key_name]}\" does not exist on EC2." if amazon.describe_keypairs(:key_name => options[:key_name]).keySet.nil?
148
+
149
+ # delete the keypair from the amazon EC2 servers
150
+ amazon.delete_keypair(:key_name => options[:key_name])
151
+ puts "Keypair \"#{options[:key_name]}\" deleted from EC2!"
152
+
153
+ begin
154
+ # determine the local key file name and delete it
155
+ key_file = get_key_file(:key_name => options[:key_name])
156
+ File.delete(key_file)
157
+ rescue
158
+ puts "Keypair \"#{key_file}\" not found on the local filesystem."
159
+ else
160
+ puts "Keypair \"#{key_file}\" deleted from local file system!"
161
+ end
162
+ end
163
+
164
+
165
+ # IMAGE METHODS
166
+ #########################################
167
+
168
+
169
+ #describe the amazon machine images available for launch
170
+ # Even though the amazon-ec2 library allows us to pass in an array of image_id's,
171
+ # owner_id's, or executable_by's we restrict Capsize usage to passing in a String
172
+ # with a single value.
173
+ def describe_images(options = {})
174
+ amazon = connect()
175
+
176
+ options = {:image_id => nil, :owner_id => nil, :executable_by => nil}.merge(options)
177
+
178
+ options[:image_id] = options[:image_id] || get(:image_id) || ""
179
+ options[:owner_id] = options[:owner_id] || get(:owner_id) || ""
180
+ options[:executable_by] = options[:executable_by] || get(:executable_by) || ""
181
+
182
+ amazon.describe_images(:image_id => options[:image_id], :owner_id => options[:owner_id], :executable_by => options[:executable_by])
183
+
184
+ end
185
+
186
+
187
+ # INSTANCE METHODS
188
+ #########################################
189
+
190
+
191
+ #returns information about instances owned by the user
192
+ def describe_instances(options = {})
193
+ amazon = connect()
194
+ options = {:instance_id => []}.merge(options)
195
+ amazon.describe_instances(:instance_id => options[:instance_id])
196
+ end
197
+
198
+
199
+ # Run EC2 instance(s)
200
+ # TODO : Deal with starting multiple instances! Now only single instances are properly handled.
201
+ def run_instance(options = {})
202
+ amazon = connect()
203
+
204
+ options = { :image_id => get(:image_id),
205
+ :min_count => get(:min_count),
206
+ :max_count => get(:max_count),
207
+ :key_name => nil,
208
+ :group_name => nil,
209
+ :user_data => get(:user_data),
210
+ :addressing_type => get(:addressing_type),
211
+ :instance_type => get(:instance_type)
212
+ }.merge(options)
213
+
214
+ # What security group should we run as?
215
+ options[:group_id] = (options[:group_name] || get(:group_name) || "").split(',')
216
+
217
+ # We want to run the new instance using our public/private keypair if
218
+ # one is defined for this application or of the user has explicitly passed
219
+ # in a key_name as a parameter. Only allow use of application name keyname if
220
+ # the <application> name is defined on EC2 as a key_name, AND we have the local
221
+ # private key stored in the config dir.
222
+
223
+ # override application key_name if the user provided one in config or on the command line
224
+ options[:key_name] = options[:key_name] || get(:key_name)
225
+
226
+ # key_dir defaults to same as :capsize_config_dir variable
227
+ options[:key_dir] = options[:key_dir] || get(:key_dir) || get(:capsize_secure_config_dir)
228
+
229
+ # determine the local key file name and delete it
230
+ key_file = get_key_file(:key_name => options[:key_name], :key_dir => options[:key_dir])
231
+
232
+ # don't let them go further if there is no private key present.
233
+ raise Exception, "Private key is not present in #{key_file}.\nPlease generate one with 'cap ec2:keypairs:create' or specify a different KEY_NAME." unless File.exists?(key_file)
234
+
235
+ # Verify image_id, min_count, and max_count are present as these are required
236
+ raise Exception, "image_id (ami-) required" if options[:image_id].nil? || options[:image_id].empty?
237
+ raise Exception, "min_count is required" if options[:min_count].nil?
238
+ raise Exception, "max_count is required" if options[:max_count].nil?
239
+
240
+ # Start instance(s)!
241
+ response = amazon.run_instances(options)
242
+
243
+ instance_id = response.instancesSet.item[0].instanceId
244
+ puts "Instance #{instance_id} startup in progress"
245
+
246
+ #set scope outside of block
247
+ instance = nil
248
+
249
+ #loop checking for confirmation that instance is running
250
+ tries = 0
251
+ begin
252
+ instance = amazon.describe_instances(:instance_id => instance_id)
253
+ raise "Server Not Running" unless instance.reservationSet.item[0].instancesSet.item[0].instanceState.name == "running"
254
+ puts ""
255
+ puts "Instance #{instance_id} entered state 'running'"
256
+ rescue
257
+ $stdout.print '.'
258
+ sleep(10)
259
+ tries += 1
260
+ retry unless tries == 35
261
+ raise "Instance #{instance_id} never moved to state 'running'!"
262
+ end
263
+
264
+ #loop waiting to get the public key
265
+ tries = 0
266
+ begin
267
+ require 'timeout'
268
+ begin
269
+ Timeout::timeout(5) do
270
+ system("ssh -o StrictHostKeyChecking=no -o PasswordAuthentication=no -i #{get_key_file} root@#{hostname_from_instance_id(instance_id)} echo success") or raise "SSH Auth Failure"
271
+ end
272
+ rescue Timeout::Error
273
+ raise "SSH timed out..."
274
+ end
275
+ puts ""
276
+ puts "SSH is up! Grabbing the public key..."
277
+ if system "scp -o StrictHostKeyChecking=no -i #{get_key_file} root@#{hostname_from_instance_id(instance_id)}:/mnt/openssh_id.pub #{get_key_file}.pub"
278
+ puts "Public key saved at #{get_key_file}.pub"
279
+ else
280
+ puts "Error grabbing public key"
281
+ end
282
+ rescue Exception => e
283
+ $stdout.print '.'
284
+ sleep(10)
285
+ tries += 1
286
+ retry unless tries == 35
287
+ puts "We couldn't ever SSH in!"
288
+ end
289
+
290
+ #scripts
291
+ if File.exists?(fetch(:capsize_config_dir)+"/scripts")
292
+ begin
293
+ instance = amazon.describe_instances(:instance_id => instance_id)
294
+ instance.reservationSet.item.first.groupSet.item.map { |g| g.groupId }.sort.each do |group|
295
+ script_path = fetch(:capsize_config_dir)+"/scripts/#{group}"
296
+ if File.exists?(script_path)
297
+ begin
298
+ puts "Found script for security group #{group}, running"
299
+ system("scp -o StrictHostKeyChecking=no -i #{get_key_file} #{script_path} root@#{hostname_from_instance_id(instance_id)}:/tmp/") or raise "SCP ERROR"
300
+ system("ssh -o StrictHostKeyChecking=no -i #{get_key_file} root@#{hostname_from_instance_id(instance_id)} chmod o+x /tmp/#{group}") or raise "Error changing script permissions"
301
+ system("ssh -o StrictHostKeyChecking=no -i #{get_key_file} root@#{hostname_from_instance_id(instance_id)} /tmp/#{group}") or raise "Error running script"
302
+ rescue Exception => e
303
+ puts e
304
+ end
305
+ end
306
+ end
307
+ rescue Exception => e
308
+ puts e
309
+ end
310
+ end
311
+
312
+ return instance
313
+ end
314
+
315
+
316
+ #reboot a running instance
317
+ def reboot_instance(options = {})
318
+ amazon = connect()
319
+ options = {:instance_id => []}.merge(options)
320
+ raise Exception, ":instance_id required" if options[:instance_id].nil?
321
+ amazon.reboot_instances(:instance_id => options[:instance_id])
322
+ end
323
+
324
+
325
+ #terminates a running instance
326
+ def terminate_instance(options = {})
327
+ amazon = connect()
328
+ options = {:instance_id => []}.merge(options)
329
+ raise Exception, ":instance_id required" if options[:instance_id].nil?
330
+ amazon.terminate_instances(:instance_id => options[:instance_id])
331
+ end
332
+
333
+
334
+ # SECURITY GROUP METHODS
335
+ #########################################
336
+
337
+
338
+ def create_security_group(options = {})
339
+ amazon = connect()
340
+
341
+ # default group_name is the same as our appname, unless specifically overriden in capsize.yml
342
+ # default group_description is set in the :group_description variable
343
+ options = {:group_name => nil, :group_description => nil}.merge(options)
344
+
345
+ options[:group_name] = options[:group_name] || get(:group_name)
346
+ options[:group_description] = options[:group_description] || get(:group_description)
347
+
348
+ raise Exception, "Group name required" if options[:group_name].nil? || options[:group_name].empty?
349
+ raise Exception, "Group description required" if options[:group_description].nil? || options[:group_description].empty?
350
+
351
+ amazon.create_security_group(:group_name => options[:group_name], :group_description => options[:group_description])
352
+
353
+ end
354
+
355
+
356
+ #describe your security groups
357
+ def describe_security_groups(options = {})
358
+ amazon = connect()
359
+ options = {:group_name => nil}.merge(options)
360
+ options[:group_name] = options[:group_name] || get(:group_name) || ""
361
+ amazon.describe_security_groups(:group_name => options[:group_name])
362
+ end
363
+
364
+
365
+ def delete_security_group(options = {})
366
+ amazon = connect()
367
+
368
+ # default group_name is the same as our appname, unless specifically overriden in capsize.yml
369
+ options = {:group_name => nil}.merge(options)
370
+
371
+ options[:group_name] = options[:group_name] || get(:group_name)
372
+
373
+ raise Exception, "Group name required" if options[:group_name].nil? || options[:group_name].empty?
374
+
375
+ amazon.delete_security_group(:group_name => options[:group_name])
376
+
377
+ end
378
+
379
+
380
+ # Define firewall access rules for a specific security group. Instances will inherit
381
+ # the security group permissions based on the group they are assigned to.
382
+ def authorize_ingress(options = {})
383
+ amazon = connect()
384
+
385
+ options = { :group_name => nil,
386
+ :ip_protocol => get(:ip_protocol),
387
+ :from_port => get(:from_port),
388
+ :to_port => get(:to_port),
389
+ :cidr_ip => get(:cidr_ip),
390
+ :source_security_group_name => get(:source_security_group_name),
391
+ :source_security_group_owner_id => get(:source_security_group_owner_id) }.merge(options)
392
+
393
+ options[:group_name] = options[:group_name] || get(:group_name)
394
+
395
+ # Verify only that :group_name is passed. This is the only REQUIRED parameter.
396
+ # The others are optional and depend on what it is you are trying to
397
+ # do (CIDR based permissions vs. user/group pair permissions). We let the EC2
398
+ # service itself do the validations on the extra params and count on it to raise an exception
399
+ # if it doesn't like the options passed. We'll see an EC2::Exception class returned if so.
400
+ raise Exception, "You must specify a :group_name" if options[:group_name].nil? || options[:group_name].empty?
401
+
402
+ # set the :to_port to the same value as :from_port if :to_port was not explicitly defined.
403
+ unless options[:from_port].nil? || options[:from_port].empty?
404
+ set :to_port, options[:from_port] if options[:to_port].nil? || options[:to_port].empty?
405
+ options[:to_port] = to_port if options[:to_port].nil? || options[:to_port].empty?
406
+ end
407
+
408
+ #if source_security_group_name and source_security_group_owner_id are specified, unset the incompatible options
409
+ if !options[:source_security_group_name].nil? && !options[:source_security_group_owner_id].nil?
410
+ options.delete(:ip_protocol)
411
+ options.delete(:from_port)
412
+ options.delete(:to_port)
413
+ options.delete(:cidr_ip)
414
+ end
415
+
416
+ amazon.authorize_security_group_ingress(options)
417
+
418
+ end
419
+
420
+
421
+ # Revoke firewall access rules for a specific security group. Instances will inherit
422
+ # the security group permissions based on the group they are assigned to.
423
+ def revoke_ingress(options = {})
424
+ amazon = connect()
425
+
426
+ options = { :group_name => nil,
427
+ :ip_protocol => get(:ip_protocol),
428
+ :from_port => get(:from_port),
429
+ :to_port => get(:to_port),
430
+ :cidr_ip => get(:cidr_ip),
431
+ :source_security_group_name => get(:source_security_group_name),
432
+ :source_security_group_owner_id => get(:source_security_group_owner_id) }.merge(options)
433
+
434
+ options[:group_name] = options[:group_name] || get(:group_name)
435
+
436
+ # Verify only that :group_name is passed. This is the only REQUIRED parameter.
437
+ # The others are optional and depend on what it is you are trying to
438
+ # do (CIDR based permissions vs. user/group pair permissions). We let the EC2
439
+ # service itself do the validations on the extra params and count on it to raise an exception
440
+ # if it doesn't like the options passed. We'll see an EC2::Exception class returned if so.
441
+ raise Exception, "You must specify a :group_name" if options[:group_name].nil? || options[:group_name].empty?
442
+
443
+ # set the :to_port to the same value as :from_port if :to_port was not explicitly defined.
444
+ unless options[:from_port].nil? || options[:from_port].empty?
445
+ set :to_port, options[:from_port] if options[:to_port].nil? || options[:to_port].empty?
446
+ options[:to_port] = to_port if options[:to_port].nil? || options[:to_port].empty?
447
+ end
448
+
449
+ #if source_security_group_name and source_security_group_owner_id are specified, unset the incompatible options
450
+ if !options[:source_security_group_name].nil? && !options[:source_security_group_owner_id].nil?
451
+ options.delete(:ip_protocol)
452
+ options.delete(:from_port)
453
+ options.delete(:to_port)
454
+ options.delete(:cidr_ip)
455
+ end
456
+
457
+ amazon.revoke_security_group_ingress(options)
458
+
459
+ end
460
+
461
+ # CAPSIZE HELPER METHODS
462
+ #########################################
463
+ # call these from tasks.rb with 'capsize.method_name'
464
+ # returns an EC2::Base object
465
+ def connect()
466
+
467
+ # get the :use_ssl value from the config pool and set it if its available
468
+ # this will allow users to globally override whether or not their connection
469
+ # is made via SSL in their config files or deploy.rb. Of course default to using SSL.
470
+ case get(:use_ssl)
471
+ when true, nil
472
+ set :use_ssl, true
473
+ when false
474
+ set :use_ssl, false
475
+ else
476
+ raise Exception, "You have an invalid value in your config for :use_ssl. Must be 'true' or 'false'."
477
+ end
478
+
479
+ # Optimized so we don't read the config files six times just to connect.
480
+ # Read once, set it, and re-use what we get back...
481
+ set :aws_access_key_id, get(:aws_access_key_id)
482
+ set :aws_secret_access_key, get(:aws_secret_access_key)
483
+
484
+ raise Exception, "You must have an :aws_access_key_id defined in your config." if fetch(:aws_access_key_id).nil? || fetch(:aws_access_key_id).empty?
485
+ raise Exception, "You must have an :aws_secret_access_key defined in your config." if fetch(:aws_secret_access_key).nil? || fetch(:aws_secret_access_key).empty?
486
+
487
+ begin
488
+ return amazon = EC2::Base.new(:access_key_id => get(:aws_access_key_id), :secret_access_key => get(:aws_secret_access_key), :use_ssl => use_ssl)
489
+ rescue Exception => e
490
+ puts "Your EC2::Base authentication setup failed with the following message : " + e
491
+ raise e
492
+ end
493
+ end
494
+
495
+ # TODO : Finish this...
496
+ # accept a Response object and provide screen output of the key data from
497
+ # this response that needs to be permanently added to the users deploy.rb
498
+ # and/or Capsize config files.
499
+ def print_config_instructions(response = nil)
500
+
501
+ raise Exception, "run_instances Response object expected" if response.nil?
502
+
503
+ dns_name = response.reservationSet.item[0].instancesSet.item[0].dnsName
504
+
505
+ puts "\n\nConfiguration Instructions:\n"
506
+
507
+ config_help <<-HELP
508
+ In order to control this new server instance from Capsize and Capistrano in the
509
+ future you will need to store some critical instance information in your
510
+ deploy.rb configuration file. Please add something like the following to
511
+ the appropriate places in your config/deploy.rb file. Of course you may need to
512
+ modify this information to suite your circumstances, this is only an example.
513
+ \n\n
514
+ config/deploy.rb
515
+ --
516
+ HELP
517
+
518
+ puts config_help
519
+
520
+ puts "role :app, #{dns_name}"
521
+ puts "role :web, #{dns_name}"
522
+ puts "role :db, #{dns_name}, :primary => true"
523
+
524
+ end
525
+
526
+ # Keeping DRY. This is called from run instances and describe instances.
527
+ def print_instance_description(result = nil)
528
+ puts "" if result.nil?
529
+ unless result.reservationSet.nil?
530
+ result.reservationSet.item.each do |reservation|
531
+ puts "reservationSet:reservationId = " + reservation.reservationId
532
+ puts "reservationSet:ownerId = " + reservation.ownerId
533
+
534
+ unless reservation.groupSet.nil?
535
+ reservation.groupSet.item.each do |group|
536
+ puts " groupSet:groupId = " + group.groupId unless group.groupId.nil?
537
+ end
538
+ end
539
+
540
+ unless reservation.instancesSet.nil?
541
+ reservation.instancesSet.item.each do |instance|
542
+ puts " instancesSet:instanceId = " + instance.instanceId unless instance.instanceId.nil?
543
+ puts " instancesSet:instanceType = " + instance.instanceType unless instance.instanceType.nil?
544
+ puts " instancesSet:imageId = " + instance.imageId unless instance.imageId.nil?
545
+ puts " instancesSet:privateDnsName = " + instance.privateDnsName unless instance.privateDnsName.nil?
546
+ puts " instancesSet:dnsName = " + instance.dnsName unless instance.dnsName.nil?
547
+ puts " instancesSet:reason = " + instance.reason unless instance.reason.nil?
548
+ puts " instancesSet:launchTime = " + instance.launchTime unless instance.launchTime.nil?
549
+ puts " instancesSet:amiLaunchIndex = " + instance.amiLaunchIndex
550
+
551
+ unless instance.instanceState.nil?
552
+ puts " instanceState:code = " + instance.instanceState.code
553
+ puts " instanceState:name = " + instance.instanceState.name
554
+ end
555
+
556
+ end
557
+
558
+ end
559
+
560
+ puts ""
561
+ end
562
+ else
563
+ puts "You don't own any running or pending instances"
564
+ end
565
+ end
566
+ end
567
+ end
568
+ Capistrano.plugin :capsize_ec2, Capsize::CapsizeEC2