ec2launcher 1.4.3 → 1.5.0

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.
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