ec2launcher 1.4.3 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 1.5.0
2
+
3
+ * Initial support for Provisioned IOPS for EBS volumes.
4
+
1
5
  ## 1.4.3
2
6
 
3
7
  * Fixed typo when deleting snapshots.
data/lib/ec2launcher.rb CHANGED
@@ -175,7 +175,6 @@ module EC2Launcher
175
175
  missing_security_groups = []
176
176
  security_groups.each do |sg_name|
177
177
  missing_security_groups << sg_name unless sg_map.has_key?(sg_name)
178
- puts sg_name
179
178
  security_group_ids << sg_map[sg_name].security_group_id
180
179
  end
181
180
 
@@ -280,6 +279,7 @@ module EC2Launcher
280
279
  gems = []
281
280
  gems += @environment.gems unless @environment.gems.nil?
282
281
  gems += @application.gems unless @application.gems.nil?
282
+ gems << "ec2launcher"
283
283
 
284
284
  ##############################
285
285
  # Packages - preinstall
@@ -340,9 +340,11 @@ module EC2Launcher
340
340
  if block_device_mappings[key] =~ /^ephemeral/
341
341
  @log.info " Block device : #{key}, #{block_device_mappings[key]}"
342
342
  else
343
- @log.info " Block device : #{key}, #{block_device_mappings[key][:volume_size]}GB, " +
344
- "#{block_device_mappings[key][:snapshot_id]}, " +
345
- "(#{block_device_mappings[key][:delete_on_termination] ? 'auto-delete' : 'no delete'})"
343
+ block_device_text = " Block device : #{key}, #{block_device_mappings[key][:volume_size]}GB, "
344
+ block_device_text += "#{block_device_mappings[key][:snapshot_id]}" if block_device_mappings[key][:snapshot_id]
345
+ block_device_text += ", (#{block_device_mappings[key][:delete_on_termination] ? 'auto-delete' : 'no delete'}), "
346
+ block_device_text += "(#{block_device_mappings[key][:iops].nil? ? 'standard' : block_device_mappings[key][:iops].to_s} IOPS)"
347
+ @log.info block_device_text
346
348
  end
347
349
  end
348
350
  end
@@ -352,10 +354,35 @@ module EC2Launcher
352
354
  exit 3
353
355
  end
354
356
 
357
+ # Launch options
358
+ launch_options = {
359
+ :ami => ami.ami_id,
360
+ :availability_zone => availability_zone,
361
+ :aws_keyfile => aws_keyfile,
362
+ :block_device_mappings => block_device_mappings,
363
+ :chef_validation_pem_url => chef_validation_pem_url,
364
+ :email_notifications => email_notifications,
365
+ :environment => @environment.name,
366
+ :gems => gems,
367
+ :iam_profile => iam_profile,
368
+ :instance_type => instance_type,
369
+ :key => key_name,
370
+ :packages => packages,
371
+ :provisioned_iops => @application.has_provisioned_iops?(),
372
+ :roles => roles,
373
+ :security_group_ids => security_group_ids,
374
+ :subnet => subnet
375
+ }
376
+
355
377
  # Quit if we're only displaying the defaults
356
378
  if @options.show_defaults || @options.show_user_data
357
379
  if @options.show_user_data
358
- user_data = build_launch_command(fqdn_names[0], short_hostnames[0], roles, chef_validation_pem_url, aws_keyfile, gems, packages, email_notifications)
380
+ user_data = build_launch_command(
381
+ launch_options.merge({
382
+ :fqdn => fqdn_names[0],
383
+ :short_name => short_hostnames[0]
384
+ })
385
+ )
359
386
  @log.info ""
360
387
  @log.info "---user-data---"
361
388
  @log.info user_data
@@ -371,9 +398,14 @@ module EC2Launcher
371
398
  instances = []
372
399
  fqdn_names.each_index do |i|
373
400
  block_device_tags = block_device_builder.generate_device_tags(fqdn_names[i], short_hostnames[i], @environment.name, @application.block_devices)
374
- user_data = build_launch_command(fqdn_names[i], short_hostnames[i], roles, chef_validation_pem_url, aws_keyfile, gems, packages, email_notifications)
375
-
376
- instance = launch_instance(fqdn_names[i], short_hostnames[i], ami.ami_id, availability_zone, key_name, security_group_ids, iam_profile, instance_type, user_data, block_device_mappings, block_device_tags, subnet)
401
+ launch_options.merge!({
402
+ :fqdn => fqdn_names[i],
403
+ :short_name => short_hostnames[i],
404
+ :block_device_tags => block_device_tags,
405
+ })
406
+ user_data = build_launch_command(launch_options)
407
+
408
+ instance = launch_instance(launch_options, user_data)
377
409
  instances << instance
378
410
 
379
411
  public_dns_name = get_instance_dns(instance, true)
@@ -513,37 +545,58 @@ module EC2Launcher
513
545
 
514
546
  # Launches an EC2 instance.
515
547
  #
516
- # @param [String] FQDN for the new host.
517
- # @param [String] ami_id id for the AMI to use.
518
- # @param [String] availability_zone EC2 availability zone to use
519
- # @param [String] key_name EC2 SSH key to use.
520
- # @param [Array<String>] security_group_ids list of security groups ids
521
- # @param [String, nil] iam_profile The name or ARN of an IAM instance profile. May be nil.
522
- # @param [String] instance_type EC2 instance type.
523
- # @param [String] user_data command data to store pass to the instance in the EC2 user-data field.
524
- # @param [Hash<String,Hash<String, String>, nil] block_device_mappings mapping of device names to block device details.
525
- # See http://docs.amazonwebservices.com/AWSRubySDK/latest/AWS/EC2/InstanceCollection.html#create-instance_method.
526
- # @param [Hash<String,Hash<String, String>>, nil] block_device_tags mapping of device names to hash objects with tags for the new EBS block devices.
548
+ # launch_options = {
549
+ # :ami
550
+ # :availability_zone
551
+ # :aws_keyfile
552
+ # :block_device_mappings
553
+ # :block_device_tags
554
+ # :chef_validation_pem_url
555
+ # :email_notifications
556
+ # :fqdn
557
+ # :gems
558
+ # :iam_profile
559
+ # :instance_type
560
+ # :key
561
+ # :packages
562
+ # :roles
563
+ # :security_group_ids
564
+ # :short_name
565
+ # :subnet
566
+ # }
527
567
  #
528
568
  # @return [AWS::EC2::Instance] newly created EC2 instance or nil if the launch failed.
529
- def launch_instance(hostname, short_hostname, ami_id, availability_zone, key_name, security_group_ids, iam_profile, instance_type, user_data, block_device_mappings = nil, block_device_tags = nil, vpc_subnet = nil)
530
- @log.warn "Launching instance... #{hostname}"
569
+ def launch_instance(launch_options, user_data)
570
+ @log.warn "Launching instance... #{launch_options[:fqdn]}"
531
571
  new_instance = nil
532
572
  run_with_backoff(30, 1, "launching instance") do
533
573
  launch_mapping = {
534
- :image_id => ami_id,
535
- :availability_zone => availability_zone,
536
- :key_name => key_name,
537
- :security_group_ids => security_group_ids,
574
+ :image_id => launch_options[:ami],
575
+ :availability_zone => launch_options[:availability_zone],
576
+ :key_name => launch_options[:key],
577
+ :security_group_ids => launch_options[:security_group_ids],
538
578
  :user_data => user_data,
539
- :instance_type => instance_type
579
+ :instance_type => launch_options[:instance_type]
540
580
  }
541
- unless block_device_mappings.nil? || block_device_mappings.keys.empty?
542
- launch_mapping[:block_device_mappings] = block_device_mappings
581
+ unless launch_options[:block_device_mappings].nil? || launch_options[:block_device_mappings].keys.empty?
582
+ if launch_options[:provisioned_iops]
583
+ # Only include ephemeral devices if we're using provisioned IOPS for the EBS volumes
584
+ launch_mapping[:block_device_mappings] = {}
585
+ launch_options[:block_device_mappings].keys.sort.each do |block_device_name|
586
+ if block_device_name =~ /^ephemeral/
587
+ launch_mapping[:block_device_mappings][block_device_name] = launch_options[:block_device_mappings][block_device_name]
588
+ end
589
+ end
590
+ else
591
+ launch_mapping[:block_device_mappings] = launch_options[:block_device_mappings]
592
+ end
593
+
594
+ # Remove the block_device_mappings entry if it's empty. Otherwise the AWS API will throw an error.
595
+ launch_mapping.delete(:block_device_mappings) if launch_mapping[:block_device_mappings].keys.empty?
543
596
  end
544
597
 
545
- launch_mapping[:iam_instance_profile] = iam_profile if iam_profile
546
- launch_mapping[:subnet] = vpc_subnet if vpc_subnet
598
+ launch_mapping[:iam_instance_profile] = launch_options[:iam_profile] if launch_options[:iam_profile]
599
+ launch_mapping[:subnet] = launch_options[:vpc_subnet] if launch_options[:vpc_subnet]
547
600
 
548
601
  new_instance = @ec2.instances.create(launch_mapping)
549
602
  end
@@ -568,21 +621,24 @@ module EC2Launcher
568
621
  ##############################
569
622
  # Tag instance
570
623
  @log.info "Tagging instance..."
571
- run_with_backoff(30, 1, "tag #{new_instance.id}, tag: name, value: #{hostname}") { new_instance.add_tag("Name", :value => hostname) }
572
- run_with_backoff(30, 1, "tag #{new_instance.id}, tag: short_name, value: #{short_hostname}") { new_instance.add_tag("short_name", :value => short_hostname) }
624
+ run_with_backoff(30, 1, "tag #{new_instance.id}, tag: name, value: #{launch_options[:fqdn]}") { new_instance.add_tag("Name", :value => launch_options[:fqdn]) }
625
+ run_with_backoff(30, 1, "tag #{new_instance.id}, tag: short_name, value: #{launch_options[:short_name]}") { new_instance.add_tag("short_name", :value => launch_options[:short_name]) }
573
626
  run_with_backoff(30, 1, "tag #{new_instance.id}, tag: environment, value: #{@environment.name}") { new_instance.add_tag("environment", :value => @environment.name) }
574
627
  run_with_backoff(30, 1, "tag #{new_instance.id}, tag: application, value: #{@application.name}") { new_instance.add_tag("application", :value => @application.name) }
628
+ if @options.clone_host
629
+ run_with_backoff(30, 1, "tag #{new_instance.id}, tag: cloned_from, value: #{@options.clone_host}") { new_instance.add_tag("cloned_from", :value => @options.clone_host) }
630
+ end
575
631
 
576
632
  ##############################
577
633
  # Tag volumes
578
- unless block_device_tags.empty?
634
+ unless launch_options[:provisioned_iops] || launch_options[:block_device_tags].empty?
579
635
  @log.info "Tagging volumes..."
580
636
  AWS.start_memoizing
581
- block_device_tags.keys.each do |device|
637
+ launch_options[:block_device_tags].keys.each do |device|
582
638
  v = new_instance.block_device_mappings[device].volume
583
- block_device_tags[device].keys.each do |tag_name|
584
- run_with_backoff(30, 1, "tag #{v.id}, tag: #{tag_name}, value: #{block_device_tags[device][tag_name]}") do
585
- v.add_tag(tag_name, :value => block_device_tags[device][tag_name])
639
+ launch_options[:block_device_tags][device].keys.each do |tag_name|
640
+ run_with_backoff(30, 1, "tag #{v.id}, tag: #{tag_name}, value: #{launch_options[:block_device_tags][device][tag_name]}") do
641
+ v.add_tag(tag_name, :value => launch_options[:block_device_tags][device][tag_name])
586
642
  end
587
643
  end
588
644
  end
@@ -592,8 +648,8 @@ module EC2Launcher
592
648
  ##############################
593
649
  # Add to Route53
594
650
  if @route53
595
- @log.info "Adding A record to Route53: #{hostname} => #{new_instance.private_ip_address}"
596
- @route53.create_record(hostname, new_instance.private_ip_address, 'A')
651
+ @log.info "Adding A record to Route53: #{launch_options[:fqdn]} => #{new_instance.private_ip_address}"
652
+ @route53.create_record(launch_options[:fqdn], new_instance.private_ip_address, 'A')
597
653
  end
598
654
 
599
655
  new_instance
@@ -628,26 +684,42 @@ module EC2Launcher
628
684
 
629
685
  # Builds the launch scripts that should run on the new instance.
630
686
  #
631
- # @param [String] fqdn Fully qualified hostname
632
- # @param [String] short_name Short hostname without the domain
633
- # @param [String] chef_validation_pem_url URL For the Chef validation pem file
634
- # @param [String] aws_keyfile Name of the AWS key to use
635
- # @param [Array<String>] gems List of gems to pre-install
636
- # @param [Array<String>] packages List of packages to pre-install
637
- # @param [EC2Launcher::DSL::EmailNotifications] email_notifications Email notification settings for launch updates
687
+ # launch_options = {
688
+ # :ami
689
+ # :availability_zone
690
+ # :aws_keyfile
691
+ # :block_device_mappings
692
+ # :block_device_tags
693
+ # :chef_validation_pem_url
694
+ # :email_notifications
695
+ # :fqdn
696
+ # :gems
697
+ # :iam_profile
698
+ # :instance_type
699
+ # :key
700
+ # :packages
701
+ # :roles
702
+ # :security_group_ids
703
+ # :short_name
704
+ # :subnet
705
+ # }
638
706
  #
639
707
  # @return [String] Launch commands to pass into new instance as userdata
640
- def build_launch_command(fqdn, short_hostname, roles, chef_validation_pem_url, aws_keyfile, gems, packages, email_notifications)
708
+ def build_launch_command(launch_options)
641
709
  # Build JSON for setup scripts
710
+
711
+ # Require ec2launcher gem if cloning and using provisioned IOPS
642
712
  setup_json = {
643
- 'hostname' => fqdn,
644
- 'short_hostname' => short_hostname,
645
- 'roles' => roles,
713
+ 'hostname' => launch_options[:fqdn],
714
+ 'short_hostname' => launch_options[:short_name],
715
+ 'block_device_mappings' => launch_options[:block_device_mappings],
716
+ 'roles' => launch_options[:roles],
646
717
  'chef_server_url' => @environment.chef_server_url,
647
- 'chef_validation_pem_url' => chef_validation_pem_url,
648
- 'aws_keyfile' => aws_keyfile,
649
- 'gems' => gems,
650
- 'packages' => packages
718
+ 'chef_validation_pem_url' => launch_options[:chef_validation_pem_url],
719
+ 'aws_keyfile' => launch_options[:aws_keyfile],
720
+ 'gems' => launch_options[:gems],
721
+ 'packages' => launch_options[:packages],
722
+ 'provisioned_iops' => false
651
723
  }
652
724
  setup_json["gem_path"] = @instance_paths.gem_path
653
725
  setup_json["ruby_path"] = @instance_paths.ruby_path
@@ -656,9 +728,16 @@ module EC2Launcher
656
728
 
657
729
  unless @application.block_devices.nil? || @application.block_devices.empty?
658
730
  setup_json['block_devices'] = @application.block_devices
731
+
732
+ @application.block_devices.each do |bd|
733
+ if bd.provisioned_iops?
734
+ setup_json['provisioned_iops'] = true
735
+ break
736
+ end
737
+ end
659
738
  end
660
- unless email_notifications.nil?
661
- setup_json['email_notifications'] = email_notifications
739
+ unless launch_options[:email_notifications].nil?
740
+ setup_json['email_notifications'] = launch_options[:email_notifications]
662
741
  end
663
742
 
664
743
  ##############################
@@ -711,7 +790,9 @@ EOF
711
790
  user_data += "\nchmod +x /tmp/setup.rb"
712
791
  # user_data += "\nrm -f /tmp/setup.rb.gz.base64"
713
792
 
714
- user_data += "\n#{setup_json['ruby_path']} /tmp/setup.rb -e #{@environment.name} -a #{@application.name} -h #{fqdn} /tmp/setup.json"
793
+ user_data += "\ngem install ec2launcher --no-ri --no-rdoc"
794
+
795
+ user_data += "\n#{setup_json['ruby_path']} /tmp/setup.rb -e #{@environment.name} -a #{@application.name} -h #{launch_options[:fqdn]} /tmp/setup.json"
715
796
  user_data += " -c #{@options.clone_host}" unless @options.clone_host.nil?
716
797
  user_data += " 2>&1 > /var/log/cloud-startup.log"
717
798
  end
@@ -35,5 +35,24 @@ module EC2Launcher
35
35
  true
36
36
  end
37
37
 
38
+ # Runs a block that returns true or false. If the block returns
39
+ # false, retries the request after sleeping. Repeated failures
40
+ # trigger an exponential backoff in sleep time.
41
+ #
42
+ # @return [Boolean] True if the request suceeded, False otherwise.
43
+ #
44
+ def test_with_backoff(max_time, sleep_time, message, &block)
45
+ if sleep_time < max_time
46
+ result = block.call
47
+ unless result
48
+ puts "Retrying #{message} in #{sleep_time} seconds"
49
+ sleep sleep_time
50
+ result = test_with_backoff(max_time, sleep_time * 2, message, &block)
51
+ end
52
+ result
53
+ else
54
+ false
55
+ end
56
+ end
38
57
  end
39
58
  end
@@ -118,8 +118,10 @@ module EC2Launcher
118
118
 
119
119
  block_device_mappings["/dev/#{device_name}"] = {
120
120
  :volume_size => volume_size,
121
- :delete_on_termination => true
121
+ :delete_on_termination => block_device.iops.nil?
122
122
  }
123
+
124
+ block_device_mappings["/dev/#{device_name}"][:iops] = block_device.iops if block_device.iops
123
125
  end
124
126
  end
125
127
  end
@@ -141,6 +141,19 @@ module EC2Launcher
141
141
  end
142
142
  end
143
143
 
144
+ def has_provisioned_iops?()
145
+ return false unless @block_devices
146
+
147
+ provisioned_iops = false
148
+ @block_devices.each do |bd|
149
+ if bd.provisioned_iops?
150
+ provisioned_iops = true
151
+ break
152
+ end
153
+ end
154
+ provisioned_iops
155
+ end
156
+
144
157
  # IAM profile role name to use for new instances.
145
158
  #
146
159
  # Expects one param in the form of either:
@@ -2,6 +2,7 @@
2
2
  # Copyright (c) 2012 Sean Laurent
3
3
  #
4
4
  require 'ec2launcher/dsl/helper'
5
+ require 'json'
5
6
 
6
7
  module EC2Launcher
7
8
  module DSL
@@ -16,26 +17,56 @@ module EC2Launcher
16
17
  dsl_accessor :owner
17
18
  dsl_accessor :raid_level
18
19
  dsl_accessor :size
20
+ dsl_accessor :iops
19
21
 
20
- def initialize()
21
- @count = 1
22
- @group = "root"
23
- @user = "root"
22
+ def initialize(option_hash = nil)
23
+ if option_hash
24
+ @name = option_hash["name"]
25
+ @count = option_hash["count"]
26
+ @size = option_hash["size"]
27
+ @iops = option_hash["iops"]
28
+ @raid_level = option_hash["raid_level"]
29
+ @mount = option_hash["mount_point"]
30
+ @owner = option_hash["owner"]
31
+ @group = option_hash["group"]
32
+ end
33
+
34
+ # Default values
35
+ @count ||= 1
36
+ @group ||= "root"
37
+ @user ||= "root"
24
38
  end
25
39
 
26
40
  def is_raid?()
27
41
  @raid_level.nil?
28
42
  end
29
43
 
30
- def to_json(*a)
44
+ def provisioned_iops?()
45
+ ! @iops.nil? || @iops == 0
46
+ end
47
+
48
+ def as_json(*)
31
49
  {
32
- "name" => @name,
33
- "count" => @count,
34
- "raid_level" => @raid_level,
35
- "mount_point" => @mount,
36
- "owner" => @owner,
37
- "group" => @group
38
- }.to_json(*a)
50
+ JSON.create_id => self.class.name,
51
+ "data" => {
52
+ "name" => @name,
53
+ "count" => @count,
54
+ "size" => @size,
55
+ "iops" => @iops,
56
+ "raid_level" => @raid_level,
57
+ "mount_point" => @mount,
58
+ "owner" => @owner,
59
+ "group" => @group
60
+ }
61
+ }
62
+ end
63
+
64
+ def to_json(*a)
65
+ as_json.to_json(*a)
66
+ end
67
+
68
+ def self.json_create(o)
69
+ new(o['data'])
39
70
  end
40
71
  end
41
72
  end
@@ -78,9 +78,19 @@ module EC2Launcher
78
78
  aws_route53 = AWS::Route53.new if @environment.route53_zone_id
79
79
  route53 = EC2Launcher::Route53.new(aws_route53, @environment.route53_zone_id, @log)
80
80
 
81
- # Remove EBS snapshots
81
+ ##############################
82
+ # EBS Volumes
83
+ ##############################
84
+ # Find EBS volumes
85
+ attachments = nil
82
86
  AWS.memoize do
83
- remove_snapshots(ec2, instance) if snapshot_removal
87
+ attachments = instance.block_device_mappings.values
88
+
89
+ # Remove snapshots
90
+ remove_snapshots(ec2, attachments) if snapshot_removal
91
+
92
+ # Remove volumes, if necessary
93
+ remove_volumes(ec2, attachments)
84
94
  end
85
95
 
86
96
  private_ip_address = instance.private_ip_address
@@ -104,20 +114,69 @@ module EC2Launcher
104
114
  end
105
115
  end
106
116
 
107
- def remove_snapshots(ec2, instance)
108
- # Find EBS volumes for instance
109
- volumes = instance.block_device_mappings.values
110
-
117
+ def remove_snapshots(ec2, attachments)
111
118
  # Iterate over over volumes to find snapshots
112
119
  @log.info("Searching for snapshots...")
113
120
  snapshots = []
114
- volumes.each do |vol|
115
- volume_snaps = ec2.snapshots.filter("volume-id", vol.volume.id)
121
+ attachments.each do |attachment|
122
+ volume_snaps = ec2.snapshots.filter("volume-id", attachment.volume.id)
116
123
  volume_snaps.each {|volume_snapshot| snapshots << volume_snapshot }
117
124
  end
118
125
 
119
126
  @log.info("Deleting #{snapshots.size} snapshots...")
120
- snapshots.each {|snap| snap.delete }
127
+ snapshots.each do |snap|
128
+ run_with_backoff(30, 1, "Deleting snapshot #{snap.id}") do
129
+ snap.delete
130
+ end
131
+ end
132
+ end
133
+
134
+ def remove_volume(ec2, instance, device, volume)
135
+ @log.info(" Detaching #{volume.id}...")
136
+ run_with_backoff(30, 1, "detaching #{volume.id}") do
137
+ volume.detach_from(instance, device)
138
+ end
139
+
140
+ # Wait for volume to fully detach
141
+ detached = test_with_backoff(120, 1, "waiting for #{volume.id} to detach") do
142
+ volume.status == :available
143
+ end
144
+
145
+ # Volume failed to detach - do a force detatch instead
146
+ unless detached
147
+ @log.info(" Failed to detach #{volume.id}")
148
+ run_with_backoff(60, 1, "force detaching #{volume.id}") do
149
+ unless volume.status == :available
150
+ volume.detach_from(instance, device, {:force => true})
151
+ end
152
+ end
153
+ # Wait for volume to fully detach
154
+ detached = test_with_backoff(120, 1, "waiting for #{volume.id} to force detach") do
155
+ volume.status == :available
156
+ end
157
+ end
158
+
159
+ @log.info(" Deleting volume #{volume.id}")
160
+ run_with_backoff(30, 1, "delete volume #{volume.id}") do
161
+ volume.delete
162
+ end
163
+ end
164
+
165
+ def remove_volumes(ec2, attachments)
166
+ @log.info("Cleaning up volumes...")
167
+
168
+ AWS.memoize do
169
+ removal_threads = []
170
+ attachments.each do |attachment|
171
+ if attachment.exists? && ! attachment.delete_on_termination
172
+ removal_threads << Thread.new {
173
+ remove_volume(ec2, attachment.instance, attachment.device, attachment.volume)
174
+ }
175
+ end
176
+ end
177
+
178
+ removal_threads.each {|t| t.join }
179
+ end
121
180
  end
122
181
  end
123
182
  end
@@ -2,5 +2,5 @@
2
2
  # Copyright (c) 2012 Sean Laurent
3
3
  #
4
4
  module EC2Launcher
5
- VERSION = "1.4.3"
5
+ VERSION = "1.5.0"
6
6
  end
@@ -7,6 +7,8 @@ require 'ostruct'
7
7
 
8
8
  require 'json'
9
9
 
10
+ require 'ec2launcher'
11
+
10
12
  SETUP_SCRIPT = "setup_instance.rb"
11
13
  SETUP_SCRIPT_URL = "https://raw.github.com/StudyBlue/ec2launcher/master/startup-scripts//#{SETUP_SCRIPT}"
12
14
 
@@ -10,6 +10,8 @@ require 'json'
10
10
 
11
11
  require 'aws-sdk'
12
12
 
13
+ require 'ec2launcher'
14
+
13
15
  AWS_KEYS = "/etc/aws/startup_runner_keys"
14
16
 
15
17
  class InitOptions
@@ -67,351 +69,435 @@ class InitOptions
67
69
  end
68
70
  end
69
71
 
70
- ##############################
71
- # Wrapper that retries failed calls to AWS
72
- # with an exponential back-off rate.
73
- def retry_aws_with_backoff(&block)
74
- timeout = 1
75
- result = nil
76
- while timeout < 33 && result.nil?
77
- begin
78
- result = yield
79
- rescue AWS::Errors::ServerError
80
- puts "Error contacting Amazon. Sleeping #{timeout} seconds."
81
- sleep timeout
82
- timeout *= 2
83
- result = nil
84
- end
85
- end
86
- result
87
- end
88
-
89
-
90
- # Runs a command and displays the output line by line
91
- def run_command(cmd)
92
- IO.popen(cmd) do |f|
93
- while ! f.eof
94
- puts f.gets
72
+ class InstanceSetup
73
+ include EC2Launcher::AWSInitializer
74
+ include EC2Launcher::BackoffRunner
75
+
76
+ def initialize(args)
77
+ option_parser = InitOptions.new
78
+ @options = option_parser.parse(args)
79
+
80
+ @setup_json_filename = args[0]
81
+
82
+ # Load the AWS access keys
83
+ properties = {}
84
+ File.open(AWS_KEYS, 'r') do |file|
85
+ file.read.each_line do |line|
86
+ line.strip!
87
+ if (line[0] != ?# and line[0] != ?=)
88
+ i = line.index('=')
89
+ if (i)
90
+ properties[line[0..i - 1].strip] = line[i + 1..-1].strip
91
+ else
92
+ properties[line] = ''
93
+ end
94
+ end
95
+ end
95
96
  end
97
+ @AWS_ACCESS_KEY = properties["AWS_ACCESS_KEY"].gsub('"', '')
98
+ @AWS_SECRET_ACCESS_KEY = properties["AWS_SECRET_ACCESS_KEY"].gsub('"', '')
99
+
100
+ ##############################
101
+ # Find current instance data
102
+ @EC2_INSTANCE_TYPE = `wget -T 5 -q -O - http://169.254.169.254/latest/meta-data/instance-type`
103
+ @AZ = `wget -T 5 -q -O - http://169.254.169.254/latest/meta-data/placement/availability-zone`
104
+ @INSTANCE_ID = `wget -T 5 -q -O - http://169.254.169.254/latest/meta-data/instance-id`
96
105
  end
97
- $?
98
- end
99
106
 
100
- option_parser = InitOptions.new
101
- options = option_parser.parse(ARGV)
107
+ def setup()
108
+ initialize_aws(@AWS_ACCESS_KEY, @AWS_SECRET_ACCESS_KEY)
109
+
110
+ # Read the setup JSON file
111
+ instance_data = JSON.parse(File.read(@setup_json_filename))
112
+
113
+ ##############################
114
+ # EBS VOLUMES
115
+ ##############################
116
+ # Create and setup EBS volumes =
117
+ setup_ebs_volumes(instance_data) unless instance_data["block_devices"].nil?
118
+
119
+ ##############################
120
+ # EPHEMERAL VOLUMES
121
+ ##############################
122
+ system_arch = `uname -p`.strip
123
+ default_fs_type = system_arch == "x86_64" ? "xfs" : "ext4"
124
+
125
+ # Process ephemeral devices first
126
+ ephemeral_drive_count = case EC2_INSTANCE_TYPE
127
+ when "m1.small" then 1
128
+ when "m1.medium" then 1
129
+ when "m2.xlarge" then 1
130
+ when "m2.2xlarge" then 1
131
+ when "c1.medium" then 1
132
+ when "m1.large" then 2
133
+ when "m2.4xlarge" then 2
134
+ when "cc1.4xlarge" then 2
135
+ when "cg1.4xlarge" then 2
136
+ when "m1.xlarge" then 4
137
+ when "c1.xlarge" then 4
138
+ when "cc2.8xlarge" then 4
139
+ else 0
140
+ end
102
141
 
103
- setup_json_filename = ARGV[0]
142
+ # Partition the ephemeral drives
143
+ partition_list = []
144
+ build_block_devices(ephemeral_drive_count, "xvdf") do |device_name, index|
145
+ partition_list << "/dev/#{device_name}"
146
+ end
147
+ partition_devices(partition_list)
104
148
 
105
- # Load the AWS access keys
106
- properties = {}
107
- File.open(AWS_KEYS, 'r') do |file|
108
- file.read.each_line do |line|
109
- line.strip!
110
- if (line[0] != ?# and line[0] != ?=)
111
- i = line.index('=')
112
- if (i)
113
- properties[line[0..i - 1].strip] = line[i + 1..-1].strip
114
- else
115
- properties[line] = ''
149
+ # Format and mount the ephemeral drives
150
+ build_block_devices(ephemeral_drive_count, "xvdf") do |device_name, index|
151
+ format_filesystem(system_arch, "/dev/#{device_name}1")
152
+
153
+ mount_point = case index
154
+ when 0 then "/mnt"
155
+ else "/mnt/extra#{index - 1}"
116
156
  end
157
+ mount_device("/dev/#{device_name}1", mount_point, "root", "root", default_fs_type)
117
158
  end
118
- end
119
- end
120
- AWS_ACCESS_KEY = properties["AWS_ACCESS_KEY"].gsub('"', '')
121
- AWS_SECRET_ACCESS_KEY = properties["AWS_SECRET_ACCESS_KEY"].gsub('"', '')
122
-
123
- ##############################
124
- # Find current instance data
125
- EC2_INSTANCE_TYPE = `wget -T 5 -q -O - http://169.254.169.254/latest/meta-data/instance-type`
126
-
127
- # Read the setup JSON file
128
- instance_data = JSON.parse(File.read(setup_json_filename))
129
-
130
- ##############################
131
- # Block devices
132
- ##############################
133
-
134
- # Creates filesystem on a device
135
- # XFS on 64-bit
136
- # ext4 on 32-bit
137
- def format_filesystem(system_arch, device)
138
- fs_type = system_arch == "x86_64" ? "XFS" : "ext4"
139
- puts "Formatting #{fs_type} filesystem on #{device} ..."
140
-
141
- command = case system_arch
142
- when "x86_64" then "/sbin/mkfs.xfs -f #{device}"
143
- else "/sbin/mkfs.ext4 -F #{device}"
144
- end
145
- IO.popen(command) do |f|
146
- while ! f.eof
147
- puts f.gets
148
- end
149
- end
150
- end
151
159
 
152
- # Creates and formats a RAID array, given a
153
- # list of partitioned devices
154
- def initialize_raid_array(system_arch, device_list, raid_device = '/dev/md0', raid_type = 0)
155
- partitions = device_list.collect {|device| "#{device}1" }
160
+ ##############################
161
+ # CHEF SETUP
162
+ ##############################
156
163
 
157
- puts "Creating RAID-#{raid_type.to_s} array #{raid_device} ..."
158
- command = "/sbin/mdadm --create #{raid_device} --level #{raid_type.to_s} --raid-devices #{partitions.length} #{partitions.join(' ')}"
159
- puts command
160
- puts `#{command}`
164
+ # Path to executables
165
+ chef_path = instance_data["chef_path"]
166
+ knife_path = instance_data["knife_path"]
161
167
 
162
- format_filesystem(system_arch, raid_device)
163
- end
168
+ ##############################
169
+ # Create knife configuration
170
+ knife_config = <<EOF
171
+ log_level :info
172
+ log_location STDOUT
173
+ node_name '#{options.hostname}'
174
+ client_key '/etc/chef/client.pem'
175
+ validation_client_name 'chef-validator'
176
+ validation_key '/etc/chef/validation.pem'
177
+ chef_server_url '#{instance_data["chef_server_url"]}'
178
+ cache_type 'BasicFile'
179
+ cache_options( :path => '/etc/chef/checksums' )
180
+ EOF
181
+ home_folder = `echo $HOME`.strip
182
+ `mkdir -p #{home_folder}/.chef && chown 700 #{home_folder}/.chef`
183
+ File.open("#{home_folder}/.chef/knife.rb", "w") {|f| f.puts knife_config }
184
+ `chmod 600 #{home_folder}/.chef/knife.rb`
185
+
186
+ ##############################
187
+ # Add roles
188
+ instance_data["roles"].each do |role|
189
+ cmd = "#{knife_path} node run_list add #{options.hostname} \"role[#{role}]\""
190
+ puts cmd
191
+ puts `#{cmd}`
192
+ end
164
193
 
165
- # Creates a mount point, mounts the device and adds it to fstab
166
- def mount_device(device, mount_point, owner, group, fs_type)
167
- puts `echo #{device} #{mount_point} #{fs_type} noatime 0 0|tee -a /etc/fstab`
168
- puts `mkdir -p #{mount_point}`
169
- puts `mount #{mount_point}`
170
- puts `chown #{owner}:#{owner} #{mount_point}`
171
- end
194
+ result = run_chef_client(chef_path)
195
+ unless result == 0
196
+ puts "***** ERROR running chef-client. Relaunching chef-client in 30 seconds."
197
+ sleep(30)
198
+ result = run_chef_client(chef_path)
199
+ end
200
+ unless result == 0
201
+ puts "***** ERROR running chef-client. Relaunching chef-client in 30 seconds."
202
+ sleep(30)
203
+ result = run_chef_client(chef_path)
204
+ end
172
205
 
173
- # Partitions a list of mounted EBS volumes
174
- def partition_devices(device_list)
175
- puts "Partioning devices ..."
176
- device_list.each do |device|
177
- puts " * #{device}"
178
- `echo 0|sfdisk #{device}`
179
- end
206
+ ##############################
207
+ # EMAIL NOTIFICATION
208
+ ##############################
209
+ if instance_data["email_notifications"]
210
+ # Email notification through SES
211
+ puts "Email notification through SES..."
212
+ AWS.config({
213
+ :access_key_id => instance_data["email_notifications"]["ses_access_key"],
214
+ :secret_access_key => instance_data["email_notifications"]["ses_secret_key"]
215
+ })
216
+ ses = AWS::SimpleEmailService.new
217
+ ses.send_email(
218
+ :from => instance_data["email_notifications"]["from"],
219
+ :to => instance_data["email_notifications"]["to"],
220
+ :subject => "Server setup complete: #{options.hostname}",
221
+ :body_text => "Server setup is complete for Host: #{options.hostname}, Environment: #{options.environ}, Application: #{options.application}",
222
+ :body_html => "<div>Server setup is complete for:</div><div><strong>Host:</strong> #{options.hostname}</div><div><strong>Environment:</strong> #{options.environ}</div><div><strong>Application:</strong> #{options.application}</div>"
223
+ )
224
+ else
225
+ puts "Skipping email notification."
226
+ end
180
227
 
181
- puts "Sleeping 10 seconds to reload partition tables ..."
182
- sleep 10
183
- end
228
+ end
184
229
 
185
- ##############################
186
- # Assembles a set of existing partitions into a RAID array.
187
- def assemble_raid_array(partition_list, raid_device = '/dev/md0', raid_type = 0)
188
- puts "Assembling cloned RAID-#{raid_type.to_s} array #{raid_device} ..."
189
- command = "/sbin/mdadm --assemble #{raid_device} #{partition_list.join(' ')}"
190
- puts command
191
- puts `#{command}`
192
- end
230
+ ##############################
231
+ # Launch Chef
232
+ def run_chef_client(chef_path)
233
+ result = 0
234
+ last_line = nil
235
+ Open3.popen3(chef_path) do |stdin, stdout, stderr, wait_thr|
236
+ stdout.each do |line|
237
+ last_line = line
238
+ puts line
239
+ end
240
+ result = wait_thr.value if wait_thr
241
+ end
242
+ if last_line =~ /[ ]ERROR[:][ ]/
243
+ result = -1
244
+ end
193
245
 
194
- # Initializes a raid array with existing EBS volumes that are already attached to the instace.
195
- # Partitions & formats new volumes.
196
- # Returns the RAID device name.
197
- def setup_attached_raid_array(system_arch, devices, raid_device = '/dev/md0', raid_type = 0, clone = false)
198
- partitions = devices.collect {|device| "#{device}1" }
199
-
200
- unless clone
201
- partition_devices(devices)
202
- initialize_raid_array(system_arch, devices, raid_device, raid_type)
203
- else
204
- assemble_raid_array(partitions, raid_device, raid_type)
205
- end
206
- `echo DEVICE #{partitions.join(' ')} |tee -a /etc/mdadm.conf`
207
-
208
- # RAID device name can be a symlink on occasion, so we
209
- # want to de-reference the symlink to keep everything clear.
210
- raid_info = "/dev/md0"
211
- raid_scan_info = `/sbin/mdadm --detail --scan 2>&1`
212
- puts "RAID Scan Info: #{raid_scan_info}"
213
- if raid_scan_info =~ /cannot open/
214
- # This happens occasionally on CentOS 6:
215
- # $ /sbin/mdadm --detail --scan
216
- # mdadm: cannot open /dev/md/0_0: No such file or directory
217
- # mdadm: cannot open /dev/md/1_0: No such file or directory
218
- #
219
- # This is tied to how the raid array was created, especially if the array was created with an older version of mdadm.
220
- # See https://bugzilla.redhat.com/show_bug.cgi?id=606481 for a lengthy discussion. We should really be naming RAID
221
- # arrays correctly and using the HOMEHOST setting to re-assemble it.
222
- #
223
- # As a stop-gap, try to use the specified raid_device name passed into this method.
224
- raid_info = raid_device
225
-
226
- # We need to manually retrieve the UUID of the array
227
- array_uuid = `mdadm --detail #{raid_device}|grep UUID|awk '// { print $3; }'`.strip
228
-
229
- # We have to manually update mdadm.conf as well
230
- #`echo ARRAY #{raid_device} level=raid#{raid_type.to_s} num-devices=#{devices.count.to_s} meta-data=0.90 UUID=#{array_uuid} |tee -a /etc/mdadm.conf`
231
- `echo ARRAY #{raid_device} level=raid#{raid_type.to_s} num-devices=#{devices.count.to_s} UUID=#{array_uuid} |tee -a /etc/mdadm.conf`
232
- else
233
- raid_info = raid_scan_info.split("\n")[-1].split()[1]
246
+ result
234
247
  end
235
- raid_device_real_path = Pathname.new(raid_info).realpath.to_s
236
- puts "Using raid device: #{raid_info}. Real path: #{raid_device_real_path}"
237
-
238
- raid_device_real_path
239
- end
240
248
 
241
- def build_block_devices(count, device = "xvdj", &block)
242
- device_name = device
243
- 0.upto(count - 1).each do |index|
244
- yield device_name, index
245
- device_name.next!
249
+ # Runs a command and displays the output line by line
250
+ def run_command(cmd)
251
+ IO.popen(cmd) do |f|
252
+ while ! f.eof
253
+ puts f.gets
254
+ end
255
+ end
256
+ $?
246
257
  end
247
- end
248
258
 
249
- system_arch = `uname -p`.strip
250
- default_fs_type = system_arch == "x86_64" ? "xfs" : "ext4"
251
-
252
- # Process ephemeral devices first
253
- ephemeral_drive_count = case EC2_INSTANCE_TYPE
254
- when "m1.small" then 1
255
- when "m1.medium" then 1
256
- when "m2.xlarge" then 1
257
- when "m2.2xlarge" then 1
258
- when "c1.medium" then 1
259
- when "m1.large" then 2
260
- when "m2.4xlarge" then 2
261
- when "cc1.4xlarge" then 2
262
- when "cg1.4xlarge" then 2
263
- when "m1.xlarge" then 4
264
- when "c1.xlarge" then 4
265
- when "cc2.8xlarge" then 4
266
- else 0
267
- end
259
+ def attach_volume(instance, device_name, volume)
260
+ ec2 = AWS::EC2.new
268
261
 
269
- # Partition the ephemeral drives
270
- partition_list = []
271
- build_block_devices(ephemeral_drive_count, "xvdf") do |device_name, index|
272
- partition_list << "/dev/#{device_name}"
273
- end
274
- partition_devices(partition_list)
262
+ volume_available = test_with_backoff(120, 1, "check EBS volume available #{device_name} (#{volume.id})") do
263
+ volume.status == :available
264
+ end
275
265
 
276
- # Format and mount the ephemeral drives
277
- build_block_devices(ephemeral_drive_count, "xvdf") do |device_name, index|
278
- format_filesystem(system_arch, "/dev/#{device_name}1")
266
+ # TODO: Handle when volume is still not available
279
267
 
280
- mount_point = case index
281
- when 0 then "/mnt"
282
- else "/mnt/extra#{index - 1}"
283
- end
284
- mount_device("/dev/#{device_name}1", mount_point, "root", "root", default_fs_type)
285
- end
268
+ # Attach volume
269
+ attachment = nil
270
+ run_with_backoff(60, 1, "attaching volume #{volume.id} to #{device_name}") do
271
+ attachment = volume.attach_to(instance, device_name)
272
+ end
273
+ volume_attached = test_with_backoff(60, 1, "check EBS volume attached #{device_name} (#{volume.id})") do
274
+ attachment.status == :attached
275
+ end
276
+
277
+ # TODO: Handle when volume fails to attach
286
278
 
287
- # Process EBS volumes
288
- unless instance_data["block_devices"].nil?
289
- # Install mdadm if we have any RAID devices
290
- raid_required = false
291
- instance_data["block_devices"].each do |block_device_json|
292
- unless block_device_json["raid_level"].nil?
293
- raid_required = true
294
- break
279
+ attachment
280
+ end
281
+
282
+ def setup_ebs_volumes(instance_data)
283
+ # Install mdadm if we have any RAID devices
284
+ raid_required = false
285
+ instance_data["block_devices"].each do |block_device|
286
+ unless block_device.raid_level.nil?
287
+ raid_required = true
288
+ break
289
+ end
290
+ end
291
+ if raid_required
292
+ result = run_command("yum install mdadm -y")
293
+ unless result == 0
294
+ run_command("yum clean all")
295
+ run_command("yum install mdadm -y")
296
+ end
297
+ end
298
+
299
+ # Create and attach the EBS volumes, if necessary
300
+ if instance_data["provisioned_iops"]
301
+ puts "Setup requires EBS volumes with provisioned IOPS."
302
+
303
+ ec2 = AWS::EC2.new
304
+ instance = ec2.instances[@INSTANCE_ID]
305
+
306
+ volumes = {}
307
+ block_creation_threads = []
308
+ instance_data["block_device_mappings"].keys.sort.each do |device_name|
309
+ block_data = instance_data["block_device_mappings"][device_name]
310
+ next if block_data =~ /^ephemeral/
311
+
312
+ block_info = {}
313
+ block_info[:availability_zone] = @AZ
314
+ block_info[:size] = block_data["volume_size"]
315
+ block_info[:snapshot_id] = block_data["snapshot_id"] if block_data["snapshot_id"]
316
+ if block_data["iops"]
317
+ block_info[:iops] = block_data["iops"]
318
+ block_info[:volume_type] = "io1"
319
+ end
320
+
321
+ # Create volume
322
+ block_device_text = "Creating EBS volume: #{device_name}, #{block_info[:volume_size]}GB, "
323
+ block_device_text += "#{block_info[:snapshot_id]}" if block_info[:snapshot_id]
324
+ block_device_text += "#{block_info[:iops].nil? ? 'standard' : block_info[:iops].to_s} IOPS"
325
+ puts block_device_text
326
+ volume = nil
327
+ run_with_backoff(60, 1, "creating ebs volume") do
328
+ volume = ec2.volumes.create(block_info)
329
+ end
330
+
331
+ volumes[device_name] = volume
332
+
333
+ block_creation_threads << Thread.new {
334
+ attach_volume(instance, device_name, volume)
335
+ }
336
+ end
337
+
338
+ block_creation_threads.each do |t|
339
+ t.join
340
+ end
341
+
342
+ AWS.memoize do
343
+ block_device_builder = EC2Launcher::BlockDeviceBuilder.new(ec2, 60)
344
+ block_device_tags = block_device_builder.generate_device_tags(instance_data["hostname"], instance_data["short_hostname"], instance_data["environment"], instance_data["block_devices"])
345
+ unless block_device_tags.empty?
346
+ puts "Tagging volumes"
347
+ AWS.memoize do
348
+ block_device_tags.keys.each do |device_name|
349
+ volume = volumes[device_name]
350
+ block_device_tags[device_name].keys.each do |tag_name|
351
+ run_with_backoff(30, 1, "tag #{volume.id}, tag: #{tag_name}, value: #{block_device_tags[device_name][tag_name]}") do
352
+ volume.add_tag(tag_name, :value => block_device_tags[device_name][tag_name])
353
+ end
354
+ end
355
+ end
356
+ end
357
+ end
358
+ end
359
+ end # provisioned iops
360
+
361
+ raid_array_count = 0
362
+ next_device_name = "xvdj"
363
+ instance_data["block_devices"].each do |block_device_json|
364
+ if block_device_json["raid_level"].nil?
365
+ # If we're not cloning an existing snapshot, then we need to partition and format the drive.
366
+ if options.clone_host.nil?
367
+ partition_devices([ "/dev/#{next_device_name}" ])
368
+ format_filesystem(system_arch, "/dev/#{next_device_name}1")
369
+ end
370
+ mount_device("/dev/#{next_device_name}1", block_device_json["mount_point"], block_device_json["owner"], block_device_json["group"], default_fs_type)
371
+ next_device_name.next!
372
+ else
373
+ raid_devices = []
374
+ build_block_devices(block_device_json["count"], next_device_name) do |device_name, index|
375
+ raid_devices << "/dev/#{device_name}"
376
+ next_device_name = device_name
377
+ end
378
+ puts "Setting up attached raid array... system_arch = #{system_arch}, raid_devices = #{raid_devices}, device = /dev/md#{(127 - raid_array_count).to_s}"
379
+ raid_device_name = setup_attached_raid_array(system_arch, raid_devices, "/dev/md#{(127 - raid_array_count).to_s}", block_device_json["raid_level"].to_i, ! options.clone_host.nil?)
380
+ mount_device(raid_device_name, block_device_json["mount_point"], block_device_json["owner"], block_device_json["group"], default_fs_type)
381
+ raid_array_count += 1
382
+ end
295
383
  end
296
384
  end
297
- if raid_required
298
- result = run_command("yum install mdadm -y")
299
- unless result == 0
300
- run_command("yum clean all")
301
- run_command("yum install mdadm -y")
385
+
386
+ # Creates filesystem on a device
387
+ # XFS on 64-bit
388
+ # ext4 on 32-bit
389
+ def format_filesystem(system_arch, device)
390
+ fs_type = system_arch == "x86_64" ? "XFS" : "ext4"
391
+ puts "Formatting #{fs_type} filesystem on #{device} ..."
392
+
393
+ command = case system_arch
394
+ when "x86_64" then "/sbin/mkfs.xfs -f #{device}"
395
+ else "/sbin/mkfs.ext4 -F #{device}"
396
+ end
397
+ IO.popen(command) do |f|
398
+ while ! f.eof
399
+ puts f.gets
400
+ end
302
401
  end
303
402
  end
304
403
 
305
- raid_array_count = 0
306
- next_device_name = "xvdj"
307
- instance_data["block_devices"].each do |block_device_json|
308
- if block_device_json["raid_level"].nil?
309
- # If we're not cloning an existing snapshot, then we need to partition and format the drive.
310
- if options.clone_host.nil?
311
- partition_devices([ "/dev/#{next_device_name}" ])
312
- format_filesystem(system_arch, "/dev/#{next_device_name}1")
313
- end
314
- mount_device("/dev/#{next_device_name}1", block_device_json["mount_point"], block_device_json["owner"], block_device_json["group"], default_fs_type)
315
- next_device_name.next!
316
- else
317
- raid_devices = []
318
- build_block_devices(block_device_json["count"], next_device_name) do |device_name, index|
319
- raid_devices << "/dev/#{device_name}"
320
- next_device_name = device_name
321
- end
322
- puts "Setting up attached raid array... system_arch = #{system_arch}, raid_devices = #{raid_devices}, device = /dev/md#{(127 - raid_array_count).to_s}"
323
- raid_device_name = setup_attached_raid_array(system_arch, raid_devices, "/dev/md#{(127 - raid_array_count).to_s}", block_device_json["raid_level"].to_i, ! options.clone_host.nil?)
324
- mount_device(raid_device_name, block_device_json["mount_point"], block_device_json["owner"], block_device_json["group"], default_fs_type)
325
- raid_array_count += 1
326
- end
327
- end
328
- end
404
+ # Creates and formats a RAID array, given a
405
+ # list of partitioned devices
406
+ def initialize_raid_array(system_arch, device_list, raid_device = '/dev/md0', raid_type = 0)
407
+ partitions = device_list.collect {|device| "#{device}1" }
329
408
 
330
- ##############################
331
- # CHEF SETUP
332
- ##############################
409
+ puts "Creating RAID-#{raid_type.to_s} array #{raid_device} ..."
410
+ command = "/sbin/mdadm --create #{raid_device} --level #{raid_type.to_s} --raid-devices #{partitions.length} #{partitions.join(' ')}"
411
+ puts command
412
+ puts `#{command}`
333
413
 
334
- # Path to executables
335
- chef_path = instance_data["chef_path"]
336
- knife_path = instance_data["knife_path"]
414
+ format_filesystem(system_arch, raid_device)
415
+ end
337
416
 
338
- ##############################
339
- # Create knife configuration
340
- knife_config = <<EOF
341
- log_level :info
342
- log_location STDOUT
343
- node_name '#{options.hostname}'
344
- client_key '/etc/chef/client.pem'
345
- validation_client_name 'chef-validator'
346
- validation_key '/etc/chef/validation.pem'
347
- chef_server_url '#{instance_data["chef_server_url"]}'
348
- cache_type 'BasicFile'
349
- cache_options( :path => '/etc/chef/checksums' )
350
- EOF
351
- home_folder = `echo $HOME`.strip
352
- `mkdir -p #{home_folder}/.chef && chown 700 #{home_folder}/.chef`
353
- File.open("#{home_folder}/.chef/knife.rb", "w") do |f|
354
- f.puts knife_config
355
- end
356
- `chmod 600 #{home_folder}/.chef/knife.rb`
357
-
358
- ##############################
359
- # Add roles
360
- instance_data["roles"].each do |role|
361
- cmd = "#{knife_path} node run_list add #{options.hostname} \"role[#{role}]\""
362
- puts cmd
363
- puts `#{cmd}`
364
- end
417
+ # Creates a mount point, mounts the device and adds it to fstab
418
+ def mount_device(device, mount_point, owner, group, fs_type)
419
+ puts `echo #{device} #{mount_point} #{fs_type} noatime 0 0|tee -a /etc/fstab`
420
+ puts `mkdir -p #{mount_point}`
421
+ puts `mount #{mount_point}`
422
+ puts `chown #{owner}:#{owner} #{mount_point}`
423
+ end
365
424
 
366
- ##############################
367
- # Launch Chef
368
- def run_chef_client(chef_path)
369
- result = 0
370
- last_line = nil
371
- Open3.popen3(chef_path) do |stdin, stdout, stderr, wait_thr|
372
- stdout.each do |line|
373
- last_line = line
374
- puts line
425
+ # Partitions a list of mounted EBS volumes
426
+ def partition_devices(device_list)
427
+ puts "Partioning devices ..."
428
+ device_list.each do |device|
429
+ puts " * #{device}"
430
+ `echo 0|sfdisk #{device}`
375
431
  end
376
- result = wait_thr.value if wait_thr
432
+
433
+ puts "Sleeping 10 seconds to reload partition tables ..."
434
+ sleep 10
377
435
  end
378
- if last_line =~ /[ ]ERROR[:][ ]/
379
- result = -1
436
+
437
+ ##############################
438
+ # Assembles a set of existing partitions into a RAID array.
439
+ def assemble_raid_array(partition_list, raid_device = '/dev/md0', raid_type = 0)
440
+ puts "Assembling cloned RAID-#{raid_type.to_s} array #{raid_device} ..."
441
+ command = "/sbin/mdadm --assemble #{raid_device} #{partition_list.join(' ')}"
442
+ puts command
443
+ puts `#{command}`
380
444
  end
381
445
 
382
- result
383
- end
446
+ # Initializes a raid array with existing EBS volumes that are already attached to the instace.
447
+ # Partitions & formats new volumes.
448
+ # Returns the RAID device name.
449
+ def setup_attached_raid_array(system_arch, devices, raid_device = '/dev/md0', raid_type = 0, clone = false)
450
+ partitions = devices.collect {|device| "#{device}1" }
451
+
452
+ unless clone
453
+ partition_devices(devices)
454
+ initialize_raid_array(system_arch, devices, raid_device, raid_type)
455
+ else
456
+ assemble_raid_array(partitions, raid_device, raid_type)
457
+ end
458
+ `echo DEVICE #{partitions.join(' ')} |tee -a /etc/mdadm.conf`
459
+
460
+ # RAID device name can be a symlink on occasion, so we
461
+ # want to de-reference the symlink to keep everything clear.
462
+ raid_info = "/dev/md0"
463
+ raid_scan_info = `/sbin/mdadm --detail --scan 2>&1`
464
+ puts "RAID Scan Info: #{raid_scan_info}"
465
+ if raid_scan_info =~ /cannot open/
466
+ # This happens occasionally on CentOS 6:
467
+ # $ /sbin/mdadm --detail --scan
468
+ # mdadm: cannot open /dev/md/0_0: No such file or directory
469
+ # mdadm: cannot open /dev/md/1_0: No such file or directory
470
+ #
471
+ # This is tied to how the raid array was created, especially if the array was created with an older version of mdadm.
472
+ # See https://bugzilla.redhat.com/show_bug.cgi?id=606481 for a lengthy discussion. We should really be naming RAID
473
+ # arrays correctly and using the HOMEHOST setting to re-assemble it.
474
+ #
475
+ # As a stop-gap, try to use the specified raid_device name passed into this method.
476
+ raid_info = raid_device
477
+
478
+ # We need to manually retrieve the UUID of the array
479
+ array_uuid = `mdadm --detail #{raid_device}|grep UUID|awk '// { print $3; }'`.strip
480
+
481
+ # We have to manually update mdadm.conf as well
482
+ #`echo ARRAY #{raid_device} level=raid#{raid_type.to_s} num-devices=#{devices.count.to_s} meta-data=0.90 UUID=#{array_uuid} |tee -a /etc/mdadm.conf`
483
+ `echo ARRAY #{raid_device} level=raid#{raid_type.to_s} num-devices=#{devices.count.to_s} UUID=#{array_uuid} |tee -a /etc/mdadm.conf`
484
+ else
485
+ raid_info = raid_scan_info.split("\n")[-1].split()[1]
486
+ end
487
+ raid_device_real_path = Pathname.new(raid_info).realpath.to_s
488
+ puts "Using raid device: #{raid_info}. Real path: #{raid_device_real_path}"
489
+
490
+ raid_device_real_path
491
+ end
384
492
 
385
- result = run_chef_client(chef_path)
386
- unless result == 0
387
- puts "***** ERROR running chef-client. Relaunching chef-client in 30 seconds."
388
- sleep(30)
389
- result = run_chef_client(chef_path)
390
- end
391
- unless result == 0
392
- puts "***** ERROR running chef-client. Relaunching chef-client in 30 seconds."
393
- sleep(30)
394
- result = run_chef_client(chef_path)
493
+ def build_block_devices(count, device = "xvdj", &block)
494
+ device_name = device
495
+ 0.upto(count - 1).each do |index|
496
+ yield device_name, index
497
+ device_name.next!
498
+ end
499
+ end
395
500
  end
396
501
 
397
- ##############################
398
- # EMAIL NOTIFICATION
399
- ##############################
400
- if instance_data["email_notifications"]
401
- # Email notification through SES
402
- puts "Email notification through SES..."
403
- AWS.config({
404
- :access_key_id => instance_data["email_notifications"]["ses_access_key"],
405
- :secret_access_key => instance_data["email_notifications"]["ses_secret_key"]
406
- })
407
- ses = AWS::SimpleEmailService.new
408
- ses.send_email(
409
- :from => instance_data["email_notifications"]["from"],
410
- :to => instance_data["email_notifications"]["to"],
411
- :subject => "Server setup complete: #{options.hostname}",
412
- :body_text => "Server setup is complete for Host: #{options.hostname}, Environment: #{options.environ}, Application: #{options.application}",
413
- :body_html => "<div>Server setup is complete for:</div><div><strong>Host:</strong> #{options.hostname}</div><div><strong>Environment:</strong> #{options.environ}</div><div><strong>Application:</strong> #{options.application}</div>"
414
- )
415
- else
416
- puts "Skipping email notification."
417
- end
502
+ instance_setup = InstanceSetup.new(ARGV)
503
+ instance_setup.setup()
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ec2launcher
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.3
4
+ version: 1.5.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-28 00:00:00.000000000 Z
12
+ date: 2012-12-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: aws-sdk