class Stemcell::Launcher

Constants

INITIAL_RETRY_SEC
LAST_BOOTSTRAP_LINE
LAUNCH_PARAMETERS
MAX_RUNNING_STATE_WAIT_TIME
REQUIRED_LAUNCH_PARAMETERS
REQUIRED_OPTIONS
RUNNING_STATE_WAIT_SLEEP_TIME
TEMPLATE_PATH

Public Class Methods

new(opts={}) click to toggle source
# File lib/stemcell/launcher.rb, line 66
def initialize(opts={})
  @log = Logger.new(STDOUT)
  @log.level = Logger::INFO unless ENV['DEBUG']
  @log.debug "creating new stemcell object"
  @log.debug "opts are #{opts.inspect}"

  REQUIRED_OPTIONS.each do |opt|
    raise ArgumentError, "missing required option 'region'" unless opts[opt]
  end

  @region = opts['region']
  @vpc_id = opts['vpc_id']
  @ec2_endpoint = opts['ec2_endpoint']
  @aws_access_key = opts['aws_access_key']
  @aws_secret_key = opts['aws_secret_key']
  @aws_session_token = opts['aws_session_token']
  @max_attempts = opts['max_attempts'] || 3
  configure_aws_creds_and_region
end

Public Instance Methods

kill(instances, opts={}) click to toggle source
# File lib/stemcell/launcher.rb, line 230
def kill(instances, opts={})
  return if !instances || instances.empty?

  errors = run_batch_operation(instances) do |instance|
    begin
      @log.warn "Terminating instance #{instance.id}"
      instance.terminate
      nil # nil == success
    rescue AWS::EC2::Errors::InvalidInstanceID::NotFound => e
      opts[:ignore_not_found] ? nil : e
    end
  end
  check_errors(:kill, instances.map(&:id), errors)
end
launch(opts={}) click to toggle source
# File lib/stemcell/launcher.rb, line 86
def launch(opts={})
  verify_required_options(opts, REQUIRED_LAUNCH_PARAMETERS)

  # attempt to accept keys as file paths
  opts['git_key'] = try_file(opts['git_key'])
  opts['chef_data_bag_secret'] = try_file(opts['chef_data_bag_secret'])

  # generate tags and merge in any that were specefied as inputs
  tags = {
    'Name' => "#{opts['chef_role']}-#{opts['chef_environment']}",
    'Group' => "#{opts['chef_role']}-#{opts['chef_environment']}",
    'created_by' => opts.fetch('user', ENV['USER']),
    'stemcell' => VERSION,
  }
  # Short name if we're in production
  tags['Name'] = opts['chef_role'] if opts['chef_environment'] == 'production'
  tags.merge!(opts['tags']) if opts['tags']

  # generate launch options
  launch_options = {
    :image_id => opts['image_id'],
    :instance_type => opts['instance_type'],
    :key_name => opts['key_name'],
    :count => opts['count'],
  }

  if opts['security_group_ids'] && !opts['security_group_ids'].empty?
    launch_options[:security_group_ids] = opts['security_group_ids']
  end

  if opts['security_groups'] && !opts['security_groups'].empty?
    if @vpc_id
      # convert sg names to sg ids as VPC only accepts ids
      security_group_ids = get_vpc_security_group_ids(@vpc_id, opts['security_groups'])
      launch_options[:security_group_ids] ||= []
      launch_options[:security_group_ids].concat(security_group_ids)
    else
      launch_options[:security_groups] = opts['security_groups']
    end
  end

  # specify availability zone (optional)
  if opts['availability_zone']
    launch_options[:availability_zone] = opts['availability_zone']
  end

  if opts['subnet']
    launch_options[:subnet] = opts['subnet']
  end

  if opts['private_ip_address']
    launch_options[:private_ip_address] = opts['private_ip_address']
  end

  if opts['dedicated_tenancy']
    launch_options[:dedicated_tenancy] = opts['dedicated_tenancy']
  end

  if opts['associate_public_ip_address']
    launch_options[:associate_public_ip_address] = opts['associate_public_ip_address']
  end

  # specify IAM role (optional)
  if opts['iam_role']
    launch_options[:iam_instance_profile] = opts['iam_role']
  end

  # specify placement group (optional)
  if opts['placement_group']
    launch_options[:placement] = {
      :group_name => opts['placement_group'],
    }
  end

  # specify an EBS-optimized instance (optional)
  launch_options[:ebs_optimized] = true if opts['ebs_optimized']

  # specify placement group (optional)
  if opts['instance_initiated_shutdown_behavior']
    launch_options[:instance_initiated_shutdown_behavior] =
      opts['instance_initiated_shutdown_behavior']
  end

  # specify raw block device mappings (optional)
  if opts['block_device_mappings']
    launch_options[:block_device_mappings] = opts['block_device_mappings']
  end

  # specify ephemeral block device mappings (optional)
  if opts['ephemeral_devices']
    launch_options[:block_device_mappings] ||= []
    opts['ephemeral_devices'].each_with_index do |device,i|
      launch_options[:block_device_mappings].push ({
        :device_name => device,
        :virtual_name => "ephemeral#{i}"
      })
    end
  end

  # generate user data script to bootstrap instance, include in launch
  # options UNLESS we have manually set the user-data (ie. for ec2admin)
  launch_options[:user_data] = opts.fetch('user_data', render_template(opts))

  # launch instances
  instances = do_launch(launch_options)

  # everything from here on out must succeed, or we kill the instances we just launched
  begin
    # set tags on all instances launched
    set_tags(instances, tags)
    @log.info "sent ec2 api tag requests successfully"

    # link to classiclink
    unless @vpc_id
      set_classic_link(instances, opts['classic_link'])
      @log.info "successfully applied classic link settings (if any)"
    end

    # turn on termination protection
    # we do this now to make sure all other settings worked
    if opts['termination_protection']
      enable_termination_protection(instances)
      @log.info "successfully enabled termination protection"
    end

    # wait for aws to report instance stats
    if opts.fetch('wait', true)
      wait(instances)
      print_run_info(instances)
      @log.info "launched instances successfully"
    end
  rescue => e
    @log.info "launch failed, killing all launched instances"
    begin
      kill(instances, :ignore_not_found => true)
    rescue => kill_error
      @log.warn "encountered an error during cleanup: #{kill_error.message}"
    end
    raise e
  end

  return instances
end
render_template(opts={}) click to toggle source

this is made public for ec2admin usage

# File lib/stemcell/launcher.rb, line 246
def render_template(opts={})
  template_file_path = File.expand_path(TEMPLATE_PATH, __FILE__)
  template_file = File.read(template_file_path)
  erb_template = ERB.new(template_file)
  last_bootstrap_line = LAST_BOOTSTRAP_LINE
  generated_template = erb_template.result(binding)
  @log.debug "genereated template is #{generated_template}"
  return generated_template
end

Private Instance Methods

check_errors(operation, instance_ids, errors) click to toggle source
# File lib/stemcell/launcher.rb, line 425
def check_errors(operation, instance_ids, errors)
  return if errors.all?(&:nil?)
  raise IncompleteOperation.new(
    operation,
    instance_ids,
    instance_ids.zip(errors).reject { |i, e| e.nil? }
  )
end
configure_aws_creds_and_region() click to toggle source
# File lib/stemcell/launcher.rb, line 455
def configure_aws_creds_and_region
  # configure AWS with creds/region
  aws_configs = {:region => @region}
  aws_configs.merge!({
    :ec2_endpoint      => @ec2_endpoint
  }) if @ec2_endpoint
  aws_configs.merge!({
    :access_key_id     => @aws_access_key,
    :secret_access_key => @aws_secret_key
  }) if @aws_access_key && @aws_secret_key
  aws_configs.merge!({
    :session_token     => @aws_session_token,
  }) if @aws_session_token
  AWS.config(aws_configs)
end
do_launch(opts={}) click to toggle source
# File lib/stemcell/launcher.rb, line 291
def do_launch(opts={})
  @log.debug "about to launch instance(s) with options #{opts}"
  @log.info "launching instances"
  instances = ec2.instances.create(opts)
  instances = [instances] unless Array === instances
  instances.each do |instance|
    @log.info "launched instance #{instance.instance_id}"
  end
  return instances
end
ec2() click to toggle source
# File lib/stemcell/launcher.rb, line 434
def ec2
  return @ec2 if @ec2

  if @vpc_id
    @ec2 = AWS::EC2::VPC.new(@vpc_id)
  else
    @ec2 = AWS::EC2.new
  end

  @ec2
end
enable_termination_protection(instances) click to toggle source
# File lib/stemcell/launcher.rb, line 376
def enable_termination_protection(instances)
  @log.info "enabling termination protection on instance(s)"
  errors = run_batch_operation(instances) do |instance|
    begin
      resp = ec2.client.modify_instance_attribute({
          :instance_id => instance.id,
          :disable_api_termination => {
            :value => true
          }
        })
      resp.error  # returns nil (success) unless there was an error
    rescue StandardError => e
      e
    end
  end
  check_errors(:enable_termination_protection, instances.map(&:id), errors)
end
get_vpc_security_group_ids(vpc_id, group_names) click to toggle source

Resolve security group names to their ids in the given VPC

# File lib/stemcell/launcher.rb, line 316
def get_vpc_security_group_ids(vpc_id, group_names)
  group_map = {}
  @log.info "resolving security groups #{group_names} in #{vpc_id}"
  vpc = AWS::EC2::VPC.new(vpc_id)
  vpc.security_groups.each do |sg|
    next if sg.vpc_id != vpc_id
    group_map[sg.name] = sg.group_id
  end
  group_ids = []
  group_names.each do |sg_name|
    raise "Couldn't find security group #{sg_name} in #{vpc_id}" unless group_map.has_key?(sg_name)
    group_ids << group_map[sg_name]
  end
  group_ids
end
print_run_info(instances) click to toggle source
run_batch_operation(instances) { |instance| ... } click to toggle source

Return a Hash of instance => error. Empty hash indicates “no error” for code block:

- if block returns nil, success
- if block returns non-nil value (e.g., exception), retry 3 times w/ backoff
- if block raises exception, fail
# File lib/stemcell/launcher.rb, line 406
def run_batch_operation(instances)
  instances.map do |instance|
    begin
      attempt = 0
      result = nil
      while attempt < @max_attempts
        # sleep idempotently except for the first attempt
        sleep(INITIAL_RETRY_SEC * 2 ** attempt) if attempt != 0
        result = yield(instance)
        break if result.nil? # nil indicates success
        attempt += 1
      end
      result # result for this instance is nil or returned exception
    rescue => e
      e # result for this instance is caught exception
    end
  end
end
set_tags(instances=[], tags) click to toggle source
# File lib/stemcell/launcher.rb, line 302
def set_tags(instances=[], tags)
  @log.info "setting tags on instance(s)"
  errors = run_batch_operation(instances) do |instance|
    begin
      instance.tags.set(tags)
      nil # nil == success
    rescue AWS::EC2::Errors::InvalidInstanceID::NotFound => e
      e
    end
  end
  check_errors(:set_tags, instances.map(&:id), errors)
end
try_file(opt="") click to toggle source

attempt to accept keys as file paths

# File lib/stemcell/launcher.rb, line 395
def try_file(opt="")
    File.read(File.expand_path(opt)) rescue opt
end
verify_required_options(params, required_options) click to toggle source
# File lib/stemcell/launcher.rb, line 281
def verify_required_options(params, required_options)
  @log.debug "params is #{params}"
  @log.debug "required_options are #{required_options}"
  required_options.each do |required|
    unless params.include?(required)
      raise ArgumentError, "you need to provide option #{required}"
    end
  end
end
wait(instances) click to toggle source
# File lib/stemcell/launcher.rb, line 269
def wait(instances)
  @log.info "Waiting up to #{MAX_RUNNING_STATE_WAIT_TIME} seconds for #{instances.count} " \
            "instance(s): (#{instances.inspect})"

  times_out_at = Time.now + MAX_RUNNING_STATE_WAIT_TIME
  until instances.all?{ |i| i.status == :running }
    wait_time_expire_or_sleep(times_out_at)
  end

  @log.info "all instances in running state"
end
wait_time_expire_or_sleep(times_out_at) click to toggle source
# File lib/stemcell/launcher.rb, line 446
def wait_time_expire_or_sleep(times_out_at)
  now = Time.now
  if now >= times_out_at
    raise TimeoutError, "exceded timeout of #{MAX_RUNNING_STATE_WAIT_TIME} seconds"
  else
    sleep [RUNNING_STATE_WAIT_SLEEP_TIME, times_out_at - now].min
  end
end