class Chef::Provisioning::FogDriver::Providers::AWS

Public Class Methods

aws_account_info_for(aws_profile, default_iam_endpoint = nil) click to toggle source
# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 265
def self.aws_account_info_for(aws_profile, default_iam_endpoint = nil)
  iam_endpoint = aws_profile[:iam_endpoint] || default_iam_endpoint

  @@aws_account_info ||= {}
  @@aws_account_info[aws_profile[:aws_access_key_id]] ||= begin
    options = {
      # Endpoint configuration
      aws_access_key_id: aws_profile[:aws_access_key_id],
      aws_secret_access_key: aws_profile[:aws_secret_access_key],
      aws_session_token: aws_profile[:aws_security_token]
    }
    if iam_endpoint
      options[:host] = URI(iam_endpoint).host
      options[:scheme] = URI(iam_endpoint).scheme
      options[:port] = URI(iam_endpoint).port
      options[:path] = URI(iam_endpoint).path
    end
    options.delete_if { |_key, value| value.nil? }

    iam = Fog::AWS::IAM.new(options)
    arn = begin
            # TODO: it would be nice if Fog let you do this normally ...
            iam.send(:request,
                     "Action" => "GetUser",
                     :parser => Fog::Parsers::AWS::IAM::GetUser.new).body["User"]["Arn"]
          rescue Fog::AWS::IAM::Error
            # TODO: Someone tell me there is a better way to find out your current
            # user ID than this!  This is what happens when you use an IAM user
            # with default privileges.
            if $ERROR_INFO.message =~ /AccessDenied.+(arn:aws:iam::\d+:\S+)/
              arn = Regexp.last_match(1)
            else
              raise
            end
          end
    arn_split = arn.split(":", 6)
    {
      aws_account_id: arn_split[4],
      aws_username: arn_split[5],
      aws_user_arn: arn
    }
  end
end
compute_options_for(provider, id, config) click to toggle source
# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 326
def self.compute_options_for(provider, id, config)
  new_compute_options = {}
  new_compute_options[:provider] = provider
  new_config = { driver_options: { compute_options: new_compute_options } }
  new_defaults = {
    driver_options: { compute_options: {} },
    machine_options: { bootstrap_options: {} }
  }
  result = Cheffish::MergedConfig.new(new_config, config, new_defaults)

  if id && id != ""
    # AWS canonical URLs are of the form fog:AWS:
    if id =~ /^(\d{12}|IAM)(:(.+))?$/
      if Regexp.last_match(2)
        id = Regexp.last_match(1)
        new_compute_options[:region] = Regexp.last_match(3)
      else
        Chef::Log.warn("Old-style AWS URL #{id} from an early beta of chef-provisioning (before chef-metal 0.11-final) found. If you have servers in multiple regions on this account, you may see odd behavior like servers being recreated. To fix, edit any nodes with attribute chef_provisioning.location.driver_url to include the region like so: fog:AWS:#{id}:<region> (e.g. us-east-1)")
      end
    else
      # Assume it is a profile name, and set that.
      aws_profile, region = id.split(":", 2)
      new_config[:driver_options][:aws_profile] = aws_profile
      new_compute_options[:region] = region
      id = nil
    end
  end
  if id == "IAM"
    id = "IAM:#{result[:driver_options][:compute_options][:region]}"
    new_config[:driver_options][:aws_account_info] = { aws_username: "IAM" }
    new_compute_options[:use_iam_profile] = true
  else
    aws_profile = get_aws_profile(result[:driver_options], id)
    new_compute_options[:aws_access_key_id] = aws_profile[:aws_access_key_id]
    new_compute_options[:aws_secret_access_key] = aws_profile[:aws_secret_access_key]
    new_compute_options[:aws_session_token] = aws_profile[:aws_security_token]
    new_defaults[:driver_options][:compute_options][:region] = aws_profile[:region]
    new_defaults[:driver_options][:compute_options][:endpoint] = aws_profile[:ec2_endpoint]

    account_info = aws_account_info_for(result[:driver_options][:compute_options])
    new_config[:driver_options][:aws_account_info] = account_info
    id = "#{account_info[:aws_account_id]}:#{result[:driver_options][:compute_options][:region]}"
  end

  # Make sure we're using a reasonable default AMI, for now this is Ubuntu 14.04 LTS
  result[:machine_options][:bootstrap_options][:image_id] ||=
    default_ami_for_region(result[:driver_options][:compute_options][:region])

  [result, id]
end
find_aws_profile_for_account_id(aws_credentials, aws_account_id, default_iam_endpoint = nil) click to toggle source
# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 241
def self.find_aws_profile_for_account_id(aws_credentials, aws_account_id, default_iam_endpoint = nil)
  aws_profile = nil
  aws_credentials.each do |profile_name, profile|
    begin
      aws_account_info = aws_account_info_for(profile, default_iam_endpoint)
    rescue StandardError
      Chef::Log.warn("Could not connect to AWS profile #{aws_credentials[:name]}: #{$ERROR_INFO}")
      Chef::Log.debug($ERROR_INFO.backtrace.join("\n"))
      next
    end
    next unless aws_account_info[:aws_account_id] == aws_account_id
    aws_profile = profile
    aws_profile[:name] = profile_name
    aws_profile = aws_profile.merge(aws_account_info)
    break
  end
  if aws_profile
    Chef::Log.info("Discovered AWS profile #{aws_profile[:name]} pointing at account #{aws_account_id}.  Using ...")
  else
    raise "No AWS profile leads to account #{aws_account_id}.  Do you need to add profiles to the AWS config?"
  end
  aws_profile
end
get_aws_credentials(driver_options) click to toggle source
# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 309
def self.get_aws_credentials(driver_options)
  # Grab the list of possible credentials
  if driver_options[:aws_credentials]
    aws_credentials = driver_options[:aws_credentials]
  else
    aws_credentials = Credentials.new
    if driver_options[:aws_config_file]
      aws_credentials.load_ini(driver_options[:aws_config_file])
    elsif driver_options[:aws_csv_file]
      aws_credentials.load_csv(driver_options[:aws_csv_file])
    else
      aws_credentials.load_default
    end
  end
  aws_credentials
end
get_aws_profile(driver_options, aws_account_id) click to toggle source
# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 177
def self.get_aws_profile(driver_options, aws_account_id)
  aws_credentials = get_aws_credentials(driver_options)
  compute_options = driver_options[:compute_options] || {}

  # Order of operations:
  # compute_options[:aws_access_key_id] / compute_options[:aws_secret_access_key] / compute_options[:aws_security_token] / compute_options[:region]
  # compute_options[:aws_profile]
  # ENV['AWS_ACCESS_KEY_ID'] / ENV['AWS_SECRET_ACCESS_KEY'] / ENV['AWS_SECURITY_TOKEN'] / ENV['AWS_DEFAULT_REGION']
  # ENV['AWS_PROFILE']
  # ENV['DEFAULT_PROFILE']
  # 'default'
  if compute_options[:aws_access_key_id]
    Chef::Log.debug("Using AWS driver access key options")
    aws_profile = {
      aws_access_key_id: compute_options[:aws_access_key_id],
      aws_secret_access_key: compute_options[:aws_secret_access_key],
      aws_security_token: compute_options[:aws_session_token],
      region: compute_options[:region]
    }
  elsif driver_options[:aws_profile]
    Chef::Log.debug("Using AWS profile #{driver_options[:aws_profile]}")
    aws_profile = aws_credentials[driver_options[:aws_profile]]
  elsif ENV["AWS_ACCESS_KEY_ID"] || ENV["AWS_ACCESS_KEY"]
    Chef::Log.debug("Using AWS environment variable access keys")
    aws_profile = {
      aws_access_key_id: ENV["AWS_ACCESS_KEY_ID"] || ENV["AWS_ACCESS_KEY"],
      aws_secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"] || ENV["AWS_SECRET_KEY"],
      aws_security_token: ENV["AWS_SECURITY_TOKEN"],
      region: ENV["AWS_DEFAULT_REGION"] || ENV["AWS_REGION"] || ENV["EC2_REGION"]
    }
  elsif ENV["AWS_PROFILE"]
    Chef::Log.debug("Using AWS profile #{ENV['AWS_PROFILE']} from AWS_PROFILE environment variable")
    aws_profile = aws_credentials[ENV["AWS_PROFILE"]]
    unless aws_profile
      raise "Environment variable AWS_PROFILE is set to #{ENV['AWS_PROFILE'].inspect} but your AWS config file does not contain that profile!"
    end
  else
    Chef::Log.debug("Using AWS default profile")
    aws_profile = aws_credentials.default
  end

  default_ec2_endpoint = compute_options[:ec2_endpoint] || ENV["EC2_URL"]
  default_iam_endpoint = compute_options[:iam_endpoint] || ENV["AWS_IAM_URL"]

  # Merge in account info for profile
  if aws_profile
    aws_profile = aws_profile.merge(aws_account_info_for(aws_profile, default_iam_endpoint))
  end

  # If no profile is found (or the profile is not the right account), search
  # for a profile that matches the given account ID
  if aws_account_id && (!aws_profile || aws_profile[:aws_account_id] != aws_account_id)
    aws_profile = find_aws_profile_for_account_id(aws_credentials, aws_account_id, default_iam_endpoint)
  end

  raise "No AWS profile specified! Are you missing something in the Chef or AWS config?" unless aws_profile

  aws_profile[:ec2_endpoint] ||= default_ec2_endpoint
  aws_profile[:iam_endpoint] ||= default_iam_endpoint

  aws_profile.delete_if { |_key, value| value.nil? }
  aws_profile
end

Private Class Methods

default_ami_for_region(region) click to toggle source
# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 430
def self.default_ami_for_region(region)
  Chef::Log.debug("Choosing default AMI for region '#{region}'")

  case region
  when "ap-northeast-1"
    "ami-c786dcc6"
  when "ap-southeast-1"
    "ami-eefca7bc"
  when "ap-southeast-2"
    "ami-996706a3"
  when "eu-west-1"
    "ami-4ab46b3d"
  when "sa-east-1"
    "ami-6770d87a"
  when "us-east-1"
    "ami-d2ff23ba"
  when "us-west-1"
    "ami-73717d36"
  when "us-west-2"
    "ami-f1ce8bc1"
  end
end

Public Instance Methods

allocate_image(action_handler, image_spec, image_options, machine_spec) click to toggle source
# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 68
def allocate_image(action_handler, image_spec, image_options, machine_spec)
  if image_spec.location
    image = compute.images.get(image_spec.location["image_id"])
    if image
      raise "The image already exists, why are you asking me to create it?  I can't do that, Dave."
    end
  end
  action_handler.perform_action "Create image #{image_spec.name} from machine #{machine_spec.name} with options #{image_options.inspect}" do
    opt = image_options.dup
    response = compute.create_image(machine_spec.location["server_id"],
                                    image_spec.name,
                                    opt.delete(:description) || "The image formerly and currently named '#{image_spec.name}'",
                                    opt.delete(:no_reboot) || false,
                                    opt)
    image_spec.location = {
      "driver_url" => driver_url,
      "driver_version" => FogDriver::VERSION,
      "image_id" => response.body["imageId"],
      "creator" => creator,
      "allocated_at" => Time.now.to_i
    }

    image_spec.machine_options ||= {}
    image_spec.machine_options.merge!(
      bootstrap_options: {
        image_id: image_spec.location["image_id"]
      }
    )
  end
end
attach_ip(server, ip) click to toggle source

Attach given IP to machine

# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 170
def attach_ip(server, ip)
  Chef::Log.info "Attaching floating IP <#{ip}>"
  compute.associate_address(instance_id: server.id,
                            allocation_id: option_for(machine_options, :allocation_id),
                            public_ip: ip)
end
bootstrap_options_for(action_handler, machine_spec, machine_options) click to toggle source
# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 121
def bootstrap_options_for(action_handler, machine_spec, machine_options)
  bootstrap_options = symbolize_keys(machine_options[:bootstrap_options] || {})

  unless bootstrap_options[:key_name]
    bootstrap_options[:key_name] = overwrite_default_key_willy_nilly(action_handler, machine_spec)
  end

  if machine_options[:is_windows]
    Chef::Log.debug("Attaching WinRM data for user data.")
    # Enable WinRM basic auth, HTTP and open the firewall
    bootstrap_options[:user_data] = user_data if bootstrap_options[:user_data].nil?
  end
  bootstrap_options.delete(:tags) # we handle these separately for performance reasons
  bootstrap_options
end
convergence_strategy_for(machine_spec, machine_options) click to toggle source
# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 163
def convergence_strategy_for(machine_spec, machine_options)
  machine_options = Cheffish::MergedConfig.new(machine_options,
                                               convergence_options: { ohai_hints: { "ec2" => "" } })
  super(machine_spec, machine_options)
end
create_many_servers(num_servers, bootstrap_options, parallelizer) { |server| ... } click to toggle source
# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 377
def create_many_servers(num_servers, bootstrap_options, parallelizer)
  # Create all the servers in one request if we have a version of Fog that can do that
  if compute.servers.respond_to?(:create_many)
    servers = compute.servers.create_many(num_servers, num_servers, bootstrap_options)
    if block_given?
      parallelizer.parallelize(servers) do |server|
        yield server
      end.to_a
    end
    servers
  else
    super
  end
end
create_servers(action_handler, specs_and_options, parallelizer) { |machine_spec, server| ... } click to toggle source
# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 137
def create_servers(action_handler, specs_and_options, parallelizer)
  super(action_handler, specs_and_options, parallelizer) do |machine_spec, server|
    yield machine_spec, server if block_given?

    machine_options = specs_and_options[machine_spec]
    bootstrap_options = symbolize_keys(machine_options[:bootstrap_options] || {})
    tags = default_tags(machine_spec, bootstrap_options[:tags] || {})

    # Right now, not doing that in case manual tagging is going on
    server_tags = server.tags || {}
    extra_tags = tags.keys.reject { |tag_name| server_tags.key?(tag_name) }.to_a
    different_tags = server_tags.select { |tag_name, tag_value| tags.key?(tag_name) && tags[tag_name] != tag_value }.to_a
    if !extra_tags.empty? || !different_tags.empty?
      tags_description = ["Update tags for #{machine_spec.name} on #{driver_url}"]
      tags_description += extra_tags.map { |tag| "  Add #{tag} = #{tags[tag].inspect}" }
      tags_description += different_tags.map { |tag_name, tag_value| "  Update #{tag_name} from #{tag_value.inspect} to #{tags[tag_name].inspect}" }
      action_handler.perform_action tags_description do
        # TODO: should we narrow this down to just extra/different tags or
        # is it OK to just pass 'em all?  Certainly easier to do the
        # latter, and I can't think of a consequence for doing so offhand.
        compute.create_tags(server.identity, tags)
      end
    end
  end
end
create_winrm_transport(machine_spec, machine_options, server) click to toggle source

Create a WinRM transport for an AWS instance @param [Hash] machine_spec Machine-spec hash @param [Hash] machine_options Machine options (from the recipe) @param [Fog::Compute::Server] server A Fog mapping to the AWS instance @return [ChefMetal::Transport::WinRM] A WinRM Transport object to talk to the server

# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 34
def create_winrm_transport(machine_spec, machine_options, server)
  remote_host = if machine_spec.location["use_private_ip_for_ssh"]
                  server.private_ip_address
                elsif !server.public_ip_address
                  Chef::Log.warn("Server #{machine_spec.name} has no public IP address.  Using private IP '#{server.private_ip_address}'.  Set driver option 'use_private_ip_for_ssh' => true if this will always be the case ...")
                  server.private_ip_address
                elsif server.public_ip_address
                  server.public_ip_address
                else
                  raise "Server #{server.id} has no private or public IP address!"
                end

  port = machine_spec.location["winrm_port"] || 5985
  endpoint = "http://#{remote_host}:#{port}/wsman"
  type = :plaintext
  pem_bytes = private_key_for(machine_spec, machine_options, server)
  encrypted_admin_password = wait_for_admin_password(machine_spec)
  decoded = Base64.decode64(encrypted_admin_password)
  private_key = OpenSSL::PKey::RSA.new(pem_bytes)
  decrypted_password = private_key.private_decrypt decoded

  # Use basic HTTP auth - this is required for the WinRM setup we
  # are using
  # TODO: Improve that.
  options = {
    user: machine_spec.location["winrm.username"] || "Administrator",
    pass: decrypted_password,
    disable_sspi: true,
    basic_auth_only: true
  }

  Chef::Provisioning::Transport::WinRM.new(endpoint, type, options, {})
end
creator() click to toggle source
# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 21
def creator
  driver_options[:aws_account_info][:aws_username]
end
default_ssh_username() click to toggle source
# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 25
def default_ssh_username
  "ubuntu"
end
destroy_image(_action_handler, image_spec, image_options) click to toggle source
# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 112
def destroy_image(_action_handler, image_spec, image_options)
  return unless image_spec.location
  image = compute.images.get(image_spec.location["image_id"])
  return unless image
  delete_snapshots = image_options[:delete_snapshots]
  delete_snapshots = true if delete_snapshots.nil?
  image.deregister(delete_snapshots)
end
ready_image(action_handler, image_spec, _image_options) click to toggle source
# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 99
def ready_image(action_handler, image_spec, _image_options)
  unless image_spec.location
    raise "Cannot ready an image that does not exist"
  end
  image = compute.images.get(image_spec.location["image_id"])
  unless image.ready?
    action_handler.report_progress "Waiting for image to be ready ..."
    # TODO: timeout
    image.wait_for { ready? }
    action_handler.report_progress "Image is ready!"
  end
end
servers_for(machine_specs) click to toggle source
# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 392
def servers_for(machine_specs)
  # Grab all the servers in one request
  instance_ids = machine_specs.map { |machine_spec| (machine_spec.location || {})["server_id"] }.reject(&:nil?)
  servers = compute.servers.all("instance-id" => instance_ids)
  result = {}
  machine_specs.each do |machine_spec|
    if machine_spec.location
      result[machine_spec] = servers.select { |s| s.id == machine_spec.location["server_id"] }.first
    else
      result[machine_spec] = nil
    end
  end
  result
end

Private Instance Methods

user_data() click to toggle source
# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 409
          def user_data
            # TODO: Make this use HTTPS at some point.
            <<EOD
<powershell>
winrm quickconfig -q
winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="300"}'
winrm set winrm/config '@{MaxTimeoutms="1800000"}'
winrm set winrm/config/service '@{AllowUnencrypted="true"}'
winrm set winrm/config/service/auth '@{Basic="true"}'

netsh advfirewall firewall add rule name="WinRM 5985" protocol=TCP dir=in localport=5985 action=allow
netsh advfirewall firewall add rule name="WinRM 5986" protocol=TCP dir=in localport=5986 action=allow

net stop winrm
sc config winrm start=auto
net start winrm
</powershell>

EOD
          end
wait_for_admin_password(machine_spec) click to toggle source

Wait for the Windows Admin password to become available @param [Hash] machine_spec Machine spec data @return [String] encrypted admin password

# File lib/chef/provisioning/fog_driver/providers/aws.rb, line 456
def wait_for_admin_password(machine_spec)
  time_elapsed = 0
  sleep_time = 10
  max_wait_time = 900 # 15 minutes
  encrypted_admin_password = nil
  instance_id = machine_spec.location["server_id"]

  Chef::Log.info "waiting for #{machine_spec.name}'s admin password to be available..."
  while time_elapsed < max_wait_time && encrypted_admin_password.nil?
    response = compute.get_password_data(instance_id)
    encrypted_admin_password = response.body["passwordData"]
    next unless encrypted_admin_password.nil?
    Chef::Log.info "#{time_elapsed}/#{max_wait_time}s elapsed -- sleeping #{sleep_time} seconds for #{machine_spec.name}'s admin password."
    sleep(sleep_time)
    time_elapsed += sleep_time
  end

  Chef::Log.info "#{machine_spec.name}'s admin password is available!'"

  encrypted_admin_password
end