class Chef::Provisioning::AWSDriver::Driver

Provisions machines using the AWS SDK

Constants

PORT_DEFAULTS
PROTOCOL_DEFAULTS

Attributes

aws_config[R]
aws_config_2[R]

Public Class Methods

canonicalize_url(driver_url, config) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 165
def self.canonicalize_url(driver_url, config)
  [driver_url, config]
end
from_url(driver_url, config) click to toggle source

URL scheme: aws:profilename:region TODO: migration path from fog:AWS - parse that URL canonical URL calls realpath on <path>

# File lib/chef/provisioning/aws_driver/driver.rb, line 101
def self.from_url(driver_url, config)
  Driver.new(driver_url, config)
end
new(driver_url, config) click to toggle source
Calls superclass method
# File lib/chef/provisioning/aws_driver/driver.rb, line 105
def initialize(driver_url, config)
  super

  _, profile_name, region = driver_url.split(":")
  profile_name = nil if profile_name && profile_name.empty?
  region = nil if region && region.empty?

  credentials = profile_name ? aws_credentials[profile_name] : aws_credentials.default
  @aws_config = Aws.config.update(
    access_key_id:     credentials[:aws_access_key_id],
    secret_access_key: credentials[:aws_secret_access_key],
    region: region || credentials[:region],
    http_proxy: credentials[:proxy_uri] || nil,
    session_token: credentials[:aws_session_token] || nil,
    logger: Chef::Log.logger
  )

  # TODO: document how users could add something to the Aws.config themselves if they want to
  # Right now we are supporting both V1 and V2, so we create 2 config sets
  credentials2 = Credentials2.new(profile_name: profile_name)
  Chef::Config.chef_provisioning ||= {}
  @aws_config_2 = {
    credentials: credentials2.get_credentials,
    region: region || ENV["AWS_DEFAULT_REGION"] || credentials[:region],
    # TODO: when we get rid of V1 replace the credentials class with something that knows how
    # to read ~/.aws/config
    http_proxy: credentials[:proxy_uri] || nil,
    logger: Chef::Log.logger,
    retry_limit: Chef::Config.chef_provisioning[:aws_retry_limit] || 5
  }

  driver = self
  Chef::Resource::Machine.send(:define_method, :aws_object) do
    resource = Chef::Resource::AwsInstance.new(name, nil)
    resource.driver driver
    resource.managed_entry_store Chef::Provisioning.chef_managed_entry_store
    resource.aws_object
  end
  Chef::Resource::MachineImage.send(:define_method, :aws_object) do
    resource = Chef::Resource::AwsImage.new(name, nil)
    resource.driver driver
    resource.managed_entry_store Chef::Provisioning.chef_managed_entry_store
    resource.aws_object
  end
  Chef::Resource::LoadBalancer.send(:define_method, :aws_object) do
    resource = Chef::Resource::AwsLoadBalancer.new(name, nil)
    resource.driver driver
    resource.managed_entry_store Chef::Provisioning.chef_managed_entry_store
    resource.aws_object
  end
end

Public Instance Methods

account_id() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 872
def account_id
  begin
    # We've got an AWS account root credential or an IAM admin with access rights
    current_user = iam.get_user
    arn = current_user[:user][:arn]
  rescue ::Aws::IAM::Errors::AccessDenied => e
    # If we don't have access, the error message still tells us our account ID and user ...
    # https://forums.aws.amazon.com/thread.jspa?messageID=394344
    if e.to_s !~ /\b(arn:aws:iam::[0-9]{12}:\S*)/
      raise "IAM error response for GetUser did not include user ARN.  Can't retrieve account ID."
    end
    arn = Regexp.last_match(1)
  end
  parse_arn(arn)[:account_id]
end
allocate_image(action_handler, image_spec, image_options, machine_spec, machine_options) click to toggle source

Image methods

# File lib/chef/provisioning/aws_driver/driver.rb, line 591
def allocate_image(action_handler, image_spec, image_options, machine_spec, machine_options)
  actual_image = image_for(image_spec)
  image_options = deep_symbolize_keys(image_options)
  machine_options = deep_symbolize_keys(machine_options)
  aws_tags = image_options.delete(:aws_tags) || {}
  if actual_image.nil? || !actual_image.exists? || actual_image.state.to_sym == :failed
    action_handler.perform_action "Create image #{image_spec.name} from machine #{machine_spec.name} with options #{image_options.inspect}" do
      image_options[:name] ||= image_spec.name
      image_options[:instance_id] ||= machine_spec.reference["instance_id"]
      image_options[:description] ||= "Image #{image_spec.name} created from machine #{machine_spec.name}"
      Chef::Log.debug "AWS Image options: #{image_options.inspect}"
      image_type = ec2_client.create_image(image_options.to_hash)
      actual_image = ec2_resource.image(image_type.image_id)
      image_spec.reference = {
        "driver_version" => Chef::Provisioning::AWSDriver::VERSION,
        "image_id" => actual_image.image_id,
        "allocated_at" => Time.now.to_i,
        "from-instance" => image_options[:instance_id]
      }
      image_spec.driver_url = driver_url
    end
  end
  aws_tags["from-instance"] = image_options[:instance_id] if image_options[:instance_id]
  converge_ec2_tags(actual_image, aws_tags, action_handler)
end
allocate_load_balancer(action_handler, lb_spec, lb_options, machine_specs) click to toggle source

Load balancer methods

# File lib/chef/provisioning/aws_driver/driver.rb, line 192
def allocate_load_balancer(action_handler, lb_spec, lb_options, machine_specs)
  lb_options = deep_symbolize_keys(lb_options)
  lb_options = AWSResource.lookup_options(lb_options, managed_entry_store: lb_spec.managed_entry_store, driver: self)

  # renaming lb_options[:port] to lb_options[:load_balancer_port]
  if lb_options[:listeners]
    lb_options[:listeners].each do |listener|
      listener[:load_balancer_port] = listener.delete(:port) if listener[:port]
    end
  end
  # We delete the attributes, tags, health check, and sticky sessions here because they are not valid in the create call
  # and must be applied afterward
  lb_attributes = lb_options.delete(:attributes)
  lb_aws_tags = lb_options.delete(:aws_tags)
  health_check = lb_options.delete(:health_check)
  sticky_sessions = lb_options.delete(:sticky_sessions)

  old_elb = nil
  actual_elb = load_balancer_for(lb_spec)
  if actual_elb.nil?
    lb_options[:listeners] ||= get_listeners(:http)

    if !lb_options[:subnets] && !lb_options[:availability_zones] && machine_specs
      lb_options[:subnets] = machine_specs.map { |s| ec2_resource.instance(s.reference["instance_id"]).subnet.id }.uniq
    end

    perform_action = proc { |desc, &block| action_handler.perform_action(desc, &block) }
    Chef::Log.debug "AWS Load Balancer options: #{lb_options.inspect}"

    updates = ["create load balancer #{lb_spec.name} in #{region}"]
    updates << "  enable availability zones #{lb_options[:availability_zones]}" if lb_options[:availability_zones]
    updates << "  attach subnets #{lb_options[:subnets].join(', ')}" if lb_options[:subnets]
    updates << "  with listeners #{lb_options[:listeners]}" if lb_options[:listeners]
    updates << "  with security groups #{lb_options[:security_groups]}" if lb_options[:security_groups]
    updates << "  with tags #{lb_options[:aws_tags]}" if lb_options[:aws_tags]

    action_handler.perform_action updates do
      # IAM says the server certificate exists, but ELB throws this error
      Chef::Provisioning::AWSDriver::AWSProvider.retry_with_backoff(::Aws::ElasticLoadBalancing::Errors::CertificateNotFound) do
        lb_options[:listeners].each do |listener|
          if listener.key?(:server_certificate)
            listener[:ssl_certificate_id] = listener.delete(:server_certificate)
            listener[:ssl_certificate_id] = listener[:ssl_certificate_id][:arn]
          end
        end

        lb_options[:load_balancer_name] = lb_spec.name
        actual_elb = elb.create_load_balancer(lb_options)
      end

      # load aws object for load balancer after create
      actual_elb = load_balancer_for(lb_spec)

      lb_spec.reference = {
        "driver_version" => Chef::Provisioning::AWSDriver::VERSION,
        "allocated_at" => Time.now.utc.to_s
      }
      lb_spec.driver_url = driver_url
    end
  else
    # Header gets printed the first time we make an update
    perform_action = proc do |desc, &block|
      perform_action = proc { |desc, &block| action_handler.perform_action(desc, &block) }
      action_handler.perform_action ["Update load balancer #{lb_spec.name} in #{region}", desc].flatten, &block
    end

    # TODO: refactor this whole giant method into many smaller method calls
    if lb_options[:scheme] && lb_options[:scheme].downcase != actual_elb.scheme
      # TODO: CloudFormation automatically recreates the load_balancer, we should too
      raise "Scheme is immutable - you need to :destroy and :create the load_balancer to recreated it with the new scheme"
    end

    # Update security groups
    if lb_options[:security_groups]
      current = actual_elb.security_groups
      desired = lb_options[:security_groups]
      if current != desired
        perform_action.call("  updating security groups to #{desired.to_a}") do
          elb_client.apply_security_groups_to_load_balancer(
            load_balancer_name: actual_elb.load_balancer_name,
            security_groups: desired.to_a
          )
        end
      end
    end

    if lb_options[:availability_zones] || lb_options[:subnets]
      # A subnet always belongs to an availability zone.  When specifying a ELB spec, you can either
      # specify subnets OR AZs but not both.  You cannot specify multiple subnets in the same AZ.
      # You must specify at least 1 subnet or AZ.  On an update you cannot remove all subnets
      # or AZs - it must belong to one.
      if lb_options[:availability_zones] && lb_options[:subnets]
        # We do this check here because there is no atomic call we can make to specify both
        # subnets and AZs at the same time
        raise "You cannot specify both `availability_zones` and `subnets`"
      end

      # Users can switch from availability zones to subnets or vice versa.  To ensure we do not
      # unassign all (which causes an AWS error) we first add all available ones, then remove
      # an unecessary ones
      actual_zones_subnets = {}
      actual_elb.subnets.each do |subnet|
        actual_zones_subnets[subnet] = Chef::Resource::AwsSubnet.get_aws_object(subnet, driver: self).availability_zone
      end

      # Only 1 of subnet or AZ will be populated b/c of our check earlier
      desired_subnets_zones = {}
      if lb_options[:availability_zones]
        lb_options[:availability_zones].each do |zone|
          # If the user specifies availability zone, we find the default subnet for that
          # AZ because this duplicates the create logic
          zone = zone.downcase
          filters = [
            { name: "availabilityZone", values: [zone] },
            { name: "defaultForAz", values: ["true"] }
          ]
          default_subnet = ec2_client.describe_subnets(filters: filters)[:subnets]
          if default_subnet.size != 1
            raise "Could not find default subnet in availability zone #{zone}"
          end
          default_subnet = default_subnet[0]
          desired_subnets_zones[default_subnet[:subnet_id]] = zone
        end
      end
      unless lb_options[:subnets].nil? || lb_options[:subnets].empty?
        subnet_query = ec2_client.describe_subnets(subnet_ids: lb_options[:subnets])[:subnets]
        # AWS raises an error on an unknown subnet, but not an unknown AZ
        subnet_query.each do |subnet|
          zone = subnet[:availability_zone].downcase
          desired_subnets_zones[subnet[:subnet_id]] = zone
        end
      end

      # We only bother attaching subnets, because doing this automatically attaches the AZ
      attach_subnets = desired_subnets_zones.keys - actual_zones_subnets.keys
      unless attach_subnets.empty?
        action = "  attach subnets #{attach_subnets.join(', ')}"
        enable_zones = (desired_subnets_zones.map { |s, z| z if attach_subnets.include?(s) }).compact
        action += " (availability zones #{enable_zones.join(', ')})"
        perform_action.call(action) do
          begin
            elb.attach_load_balancer_to_subnets(
              load_balancer_name: actual_elb.load_balancer_name,
              subnets: attach_subnets
            )
          rescue ::Aws::ElasticLoadBalancing::Errors::InvalidConfigurationRequest => e
            Chef::Log.error "You cannot currently move from 1 subnet to another in the same availability zone. " \
                            "Amazon does not have an atomic operation which allows this.  You must create a new " \
                            "ELB with the correct subnets and move instances into it.  Tried to attach subets " \
                            "#{attach_subnets.join(', ')} (availability zones #{enable_zones.join(', ')}) to " \
                            "existing ELB named #{actual_elb.load_balancer_name}"
            raise e
          end
        end
      end

      detach_subnets = actual_zones_subnets.keys - desired_subnets_zones.keys
      unless detach_subnets.empty?
        action = "  detach subnets #{detach_subnets.join(', ')}"
        disable_zones = (actual_zones_subnets.map { |s, z| z if detach_subnets.include?(s) }).compact
        action += " (availability zones #{disable_zones.join(', ')})"
        perform_action.call(action) do
          elb.detach_load_balancer_from_subnets(
            load_balancer_name: actual_elb.load_balancer_name,
            subnets: detach_subnets
          )
        end
      end
    end

    # Update listeners - THIS IS NOT ATOMIC
    if lb_options[:listeners]
      add_listeners = {}
      lb_options[:listeners].each { |l| add_listeners[l[:load_balancer_port]] = l }
      actual_elb.listener_descriptions.each do |listener_description|
        listener = listener_description.listener
        desired_listener = add_listeners.delete(listener.load_balancer_port)

        if desired_listener
          # listener.(port|protocol|instance_port|instance_protocol) are immutable for the life
          # of the listener - must create a new one and delete old one
          immutable_updates = []
          if listener.protocol != desired_listener[:protocol].to_s.upcase
            immutable_updates << "    update protocol from #{listener.protocol.inspect} to #{desired_listener[:protocol].inspect}"
          end

          if listener.instance_port != desired_listener[:instance_port]
            immutable_updates << "    update instance port from #{listener.instance_port.inspect} to #{desired_listener[:instance_port].inspect}"
          end

          if listener.instance_protocol != desired_listener[:instance_protocol].to_s.upcase
            immutable_updates << "    update instance protocol from #{listener.instance_protocol.inspect} to #{desired_listener[:instance_protocol].inspect}"
          end

          if !immutable_updates.empty?
            perform_action.call(immutable_updates) do
              elb.delete_load_balancer_listeners(load_balancer_name: actual_elb.load_balancer_name, load_balancer_ports: [listener.load_balancer_port])
              elb.create_load_balancer_listeners(listeners: [desired_listener], load_balancer_name: actual_elb.load_balancer_name)
              # actual_elb.listeners.create(desired_listener)
            end
          elsif listener.ssl_certificate_id && !server_certificate_eql?(listener.ssl_certificate_id,
                                                                        server_cert_from_spec(desired_listener))
            # Server certificate is mutable - if no immutable changes required a full recreate, update cert
            perform_action.call("    update server certificate from #{listener.ssl_certificate_id} to #{server_cert_from_spec(desired_listener)}") do
              elb.set_load_balancer_listener_ssl_certificate(
                load_balancer_name: actual_elb.load_balancer_name,
                load_balancer_port: listener.load_balancer_port,
                ssl_certificate_id: server_cert_from_spec(desired_listener)
              )
            end
          end
        else
          perform_action.call("  remove listener #{listener.load_balancer_port}") do
            elb.delete_load_balancer_listeners(load_balancer_name: actual_elb.load_balancer_name, load_balancer_ports: [listener.load_balancer_port])
          end
        end
      end

      add_listeners.values.each do |listener|
        updates = ["  add listener #{listener[:load_balancer_port]}"]
        updates << "    set protocol to #{listener[:protocol].inspect}"
        updates << "    set instance port to #{listener[:instance_port].inspect}"
        updates << "    set instance protocol to #{listener[:instance_protocol].inspect}"
        updates << "    set server certificate to #{server_cert_from_spec(listener)}" if server_cert_from_spec(listener)
        perform_action.call(updates) do
          elb.create_load_balancer_listeners(listeners: [listener], load_balancer_name: actual_elb.load_balancer_name)
        end
      end
    end
  end

  converge_elb_tags(actual_elb, lb_aws_tags, action_handler)

  # Update load balancer attributes
  if lb_attributes
    current = elb.describe_load_balancer_attributes(load_balancer_name: actual_elb.load_balancer_name)[:load_balancer_attributes].to_hash
    # Need to do a deep copy w/ Marshal load/dump to avoid overwriting current
    desired = deep_merge!(lb_attributes, Marshal.load(Marshal.dump(current)))
    if current != desired
      perform_action.call("  updating attributes to #{desired.inspect}") do
        elb.modify_load_balancer_attributes(
          load_balancer_name: actual_elb.load_balancer_name,
          load_balancer_attributes: desired.to_hash
        )
      end
    end
  end

  # Update the load balancer health check, as above
  if health_check
    current = elb.describe_load_balancers(load_balancer_names: [actual_elb.load_balancer_name])[:load_balancer_descriptions][0][:health_check].to_hash
    desired = deep_merge!(health_check, Marshal.load(Marshal.dump(current)))
    if current != desired
      perform_action.call("  updating health check to #{desired.inspect}") do
        elb.configure_health_check(
          load_balancer_name: actual_elb.load_balancer_name,
          health_check: desired.to_hash
        )
      end
    end
  end

  # Update the load balancer sticky sessions
  if sticky_sessions
    policy_name = "#{actual_elb.load_balancer_name}-sticky-session-policy"
    policies = elb.describe_load_balancer_policies(load_balancer_name: actual_elb.load_balancer_name)

    existing_cookie_policy = policies[:policy_descriptions].detect { |pd| pd[:policy_type_name] == "AppCookieStickinessPolicyType" && pd[:policy_name] == policy_name }
    existing_cookie_name = existing_cookie_policy ? (existing_cookie_policy[:policy_attribute_descriptions].detect { |pad| pad[:attribute_name] == "CookieName" })[:attribute_value] : nil
    desired_cookie_name = sticky_sessions[:cookie_name]

    # Create or update the policy to have the desired cookie_name
    if existing_cookie_policy.nil?
      perform_action.call("  creating sticky sessions with cookie_name #{desired_cookie_name}") do
        elb.create_app_cookie_stickiness_policy(
          load_balancer_name: actual_elb.load_balancer_name,
          policy_name: policy_name,
          cookie_name: desired_cookie_name
        )
      end
    elsif existing_cookie_name && existing_cookie_name != desired_cookie_name
      perform_action.call("  updating sticky sessions from cookie_name #{existing_cookie_name} to cookie_name #{desired_cookie_name}") do
        elb.delete_load_balancer_policy(
          load_balancer_name: actual_elb.load_balancer_name,
          policy_name: policy_name
        )
        elb.create_app_cookie_stickiness_policy(
          load_balancer_name: actual_elb.load_balancer_name,
          policy_name: policy_name,
          cookie_name: desired_cookie_name
        )
      end
    end

    # Ensure the policy is attached to the appropriate listener
    elb_description = elb.describe_load_balancers(load_balancer_names: [actual_elb.load_balancer_name])[:load_balancer_descriptions].first
    listeners = elb_description[:listener_descriptions]

    sticky_sessions[:ports].each do |ss_port|
      listener = listeners.detect { |ld| ld[:listener][:load_balancer_port] == ss_port }

      next if listener.nil?
      policy_names = listener[:policy_names]

      next if policy_names.include?(policy_name)
      policy_names << policy_name

      elb.set_load_balancer_policies_of_listener(
        load_balancer_name: actual_elb.load_balancer_name,
        load_balancer_port: ss_port,
        policy_names: policy_names
      )
    end
  end

  # Update instance list, but only if there are machines specified
  if machine_specs
    instances_to_add = []
    if actual_elb.instances
      assigned_instance_ids = actual_elb.instances.map(&:instance_id)
      instances_to_add = machine_specs.reject { |s| assigned_instance_ids.include?(s.reference["instance_id"]) }
      instance_ids_to_remove = assigned_instance_ids - machine_specs.map { |s| s.reference["instance_id"] }
    end

    unless instances_to_add.empty?
      perform_action.call("  add machines #{instances_to_add.map(&:name).join(', ')}") do
        instance_ids_to_add = instances_to_add.map { |s| s.reference["instance_id"] }
        Chef::Log.debug("Adding instances #{instance_ids_to_add.join(', ')} to load balancer #{actual_elb.load_balancer_name} in region #{region}")
        instances_to_add.each do |instance|
          elb.register_instances_with_load_balancer(instances: [{ instance_id: instance.reference["instance_id"] }], load_balancer_name: actual_elb.load_balancer_name)
        end
      end
    end

    unless instance_ids_to_remove.empty?
      perform_action.call("  remove instances #{instance_ids_to_remove}") do
        instances_to_remove = Hash[instance_ids_to_remove.map { |id| [:instance_id, id] }]
        elb.deregister_instances_from_load_balancer(instances: [instances_to_remove], load_balancer_name: actual_elb.load_balancer_name)
      end
    end
  end

  # We have successfully switched all our instances to the (possibly) new LB
  # so it is safe to delete the old one.
  old_elb.delete unless old_elb.nil?
ensure
  # Something went wrong before we could moved instances from the old ELB to the new one
  # Don't delete the old ELB, but warn users there could now be 2 ELBs with the same name
  unless old_elb.nil?
    Chef::Log.warn("It is possible there are now 2 ELB instances - #{old_elb.load_balancer_name} and #{actual_elb.load_balancer_name}. " \
    "Determine which is correct and manually clean up the other.")
  end
end
allocate_machine(action_handler, machine_spec, machine_options) click to toggle source

Machine methods

# File lib/chef/provisioning/aws_driver/driver.rb, line 706
def allocate_machine(action_handler, machine_spec, machine_options)
  machine_options = deep_symbolize_keys(machine_options)
  instance = instance_for(machine_spec)
  bootstrap_options = bootstrap_options_for(action_handler, machine_spec, machine_options)

  if instance.nil? || !instance.exists? || instance.state.name == "terminated"
    action_handler.perform_action "Create #{machine_spec.name} with AMI #{bootstrap_options[:image_id]} in #{region}" do
      Chef::Log.debug "Creating instance with bootstrap options #{bootstrap_options}"
      instance = create_instance_and_reference(bootstrap_options, action_handler, machine_spec, machine_options)
    end
  end
  converge_ec2_tags(instance, machine_options[:aws_tags], action_handler)
end
allocate_machines(action_handler, specs_and_options, parallelizer) { |machine_spec| ... } click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 720
def allocate_machines(action_handler, specs_and_options, parallelizer)
  create_servers(action_handler, specs_and_options, parallelizer) do |machine_spec, _server|
    yield machine_spec
  end
  specs_and_options.keys
end
auto_scaling() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 853
def auto_scaling
  @auto_scaling ||= ::Aws::AutoScaling.new(config: aws_config)
end
aws_credentials() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1043
def aws_credentials
  # Grab the list of possible credentials
  @aws_credentials ||= if driver_options[:aws_credentials]
                         driver_options[:aws_credentials]
                       else
                         credentials = Credentials.new
                         if driver_options[:aws_config_file]
                           credentials.load_ini(driver_options[:aws_config_file])
                         elsif driver_options[:aws_csv_file]
                           credentials.load_csv(driver_options[:aws_csv_file])
                         else
                           credentials.load_default
                         end
                         credentials
                       end
end
bootstrap_options_for(action_handler, machine_spec, machine_options) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 905
def bootstrap_options_for(action_handler, machine_spec, machine_options)
  bootstrap_options = deep_symbolize_keys(machine_options[:bootstrap_options])
  bootstrap_options = Hash({}) if bootstrap_options.nil?
  # These are hardcoded for now - only 1 machine at a time
  bootstrap_options[:min_count] = bootstrap_options[:max_count] = 1
  bootstrap_options[:instance_type] ||= default_instance_type
  image_id = machine_options[:from_image] || bootstrap_options[:image_id] || machine_options[:image_id] || default_ami_for_region(region)
  bootstrap_options[:image_id] = image_id
  bootstrap_options.delete(:key_path)
  unless bootstrap_options[:key_name]
    Chef::Log.debug("No key specified, generating a default one...")
    bootstrap_options[:key_name] = default_aws_keypair(action_handler, machine_spec)
  end
  if bootstrap_options[:user_data]
    bootstrap_options[:user_data] = Base64.encode64(bootstrap_options[:user_data])
  end

  # V1 -> V2 backwards compatability support
  unless bootstrap_options.fetch(:monitoring_enabled, nil).nil?
    bootstrap_options[:monitoring] = { enabled: bootstrap_options.delete(:monitoring_enabled) }
  end
  placement = {}
  if bootstrap_options[:availability_zone]
    placement[:availability_zone] = bootstrap_options.delete(:availability_zone)
  end
  if bootstrap_options[:placement_group]
    placement[:group_name] = bootstrap_options.delete(:placement_group)
  end
  unless bootstrap_options.fetch(:dedicated_tenancy, nil).nil?
    placement[:tenancy] = bootstrap_options.delete(:dedicated_tenancy) ? "dedicated" : "default"
  end
  bootstrap_options[:placement] = placement unless placement.empty?
  if bootstrap_options[:subnet]
    bootstrap_options[:subnet_id] = bootstrap_options.delete(:subnet)
  end
  if bootstrap_options[:iam_instance_profile] && bootstrap_options[:iam_instance_profile].is_a?(String)
    bootstrap_options[:iam_instance_profile] = { name: bootstrap_options[:iam_instance_profile] }
  end

  if machine_options[:is_windows]
    Chef::Log.debug "Setting Default windows userdata based on WinRM transport"
    if bootstrap_options[:user_data].nil?
      data = case machine_options[:winrm_transport]
             when "https"
               https_user_data
             else
               user_data
             end
      bootstrap_options[:user_data] = Base64.encode64(data)
    end
  else
    Chef::Log.debug "Non-windows, not setting Default userdata"
  end

  bootstrap_options = AWSResource.lookup_options(bootstrap_options, managed_entry_store: machine_spec.managed_entry_store, driver: self)

  # In the migration from V1 to V2 we still support associate_public_ip_address at the top level
  # we do this after the lookup because we have to copy any present subnets, etc. into the
  # network interfaces block
  unless bootstrap_options.fetch(:associate_public_ip_address, nil).nil?
    if bootstrap_options[:network_interfaces]
      raise "If you specify network_interfaces you must specify associate_public_ip_address in that list"
    end
    network_interface = {
      device_index: 0,
      associate_public_ip_address: bootstrap_options.delete(:associate_public_ip_address),
      delete_on_termination: true
    }
    if bootstrap_options[:subnet_id]
      network_interface[:subnet_id] = bootstrap_options.delete(:subnet_id)
    end
    if bootstrap_options[:private_ip_address]
      network_interface[:private_ip_address] = bootstrap_options.delete(:private_ip_address)
    end
    if bootstrap_options[:security_group_ids]
      network_interface[:groups] = bootstrap_options.delete(:security_group_ids)
    end
    bootstrap_options[:network_interfaces] = [network_interface]
  end

  Chef::Log.debug "AWS Bootstrap options: #{bootstrap_options.inspect}"
  deep_symbolize_keys(bootstrap_options)
end
build_arn(partition: "aws", service: nil, region: aws_config[:region], account_id: self.account_id, resource: nil) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 857
def build_arn(partition: "aws", service: nil, region: aws_config[:region], account_id: self.account_id, resource: nil)
  "arn:#{partition}:#{service}:#{region}:#{account_id}:#{resource}"
end
cloudsearch() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 161
def cloudsearch
  @cloudsearch ||= Aws::CloudSearch::Client.new(aws_config)
end
connect_to_machine(name, chef_server = nil) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 766
def connect_to_machine(name, chef_server = nil)
  machine_spec = if name.is_a?(MachineSpec)
                   name
                 else
                   Chef::Provisioning::ChefMachineSpec.get(name, chef_server)
                 end

  machine_for(machine_spec, machine_spec.reference)
end
converge_ec2_tags(aws_object, tags, action_handler) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1498
def converge_ec2_tags(aws_object, tags, action_handler)
  ec2_strategy = Chef::Provisioning::AWSDriver::TaggingStrategy::EC2.new(
    ec2_client,
    aws_object.id,
    tags
  )
  aws_tagger = Chef::Provisioning::AWSDriver::AWSTagger.new(ec2_strategy, action_handler)
  aws_tagger.converge_tags
end
converge_elb_tags(aws_object, tags, action_handler) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1508
def converge_elb_tags(aws_object, tags, action_handler)
  elb_strategy = Chef::Provisioning::AWSDriver::TaggingStrategy::ELB.new(
    elb_client,
    aws_object.load_balancer_name,
    tags
  )
  aws_tagger = Chef::Provisioning::AWSDriver::AWSTagger.new(elb_strategy, action_handler)
  aws_tagger.converge_tags
end
convergence_strategy_for(machine_spec, machine_options) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1309
def convergence_strategy_for(machine_spec, machine_options)
  # Tell Ohai that this is an EC2 instance so that it runs the EC2 plugin
  convergence_options = Cheffish::MergedConfig.new(
    machine_options[:convergence_options] || {},
    ohai_hints: { "ec2" => "" }
  )
  convergence_options = deep_symbolize_keys(convergence_options)

  # Defaults
  unless machine_spec.reference
    return Chef::Provisioning::ConvergenceStrategy::NoConverge.new(convergence_options, config)
  end

  if machine_spec.reference["is_windows"]
    Chef::Provisioning::ConvergenceStrategy::InstallMsi.new(convergence_options, config)
  elsif machine_options[:cached_installer] == true
    Chef::Provisioning::ConvergenceStrategy::InstallCached.new(convergence_options, config)
  else
    Chef::Provisioning::ConvergenceStrategy::InstallSh.new(convergence_options, config)
  end
end
create_instance_and_reference(bootstrap_options, action_handler, machine_spec, machine_options) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1518
def create_instance_and_reference(bootstrap_options, action_handler, machine_spec, machine_options)
  instance = nil
  # IAM says the instance profile is ready, but EC2 doesn't think it is
  # Not using retry_with_backoff here because we need to match on a string
  Retryable.retryable(
    tries: 10,
    sleep: ->(n) { [2**n, 16].min },
    on: ::Aws::EC2::Errors::InvalidParameterValue,
    matching: /Invalid IAM Instance Profile name/
  ) do |_retries, exception|
    Chef::Log.debug("Instance creation InvalidParameterValue exception is #{exception.inspect}")
    instance = ec2_resource.create_instances(bootstrap_options.to_hash)[0]
  end

  # Make sure the instance is ready to be tagged
  instance.wait_until_exists

  # Sometimes tagging fails even though the instance 'exists'
  Chef::Provisioning::AWSDriver::AWSProvider.retry_with_backoff(::Aws::EC2::Errors::InvalidInstanceIDNotFound) do
    instance.create_tags(tags: [{ key: "Name", value: machine_spec.name }])
  end
  if machine_options.key?(:source_dest_check)
    instance.modify_attribute(
      source_dest_check: {
        value: machine_options[:source_dest_check]
      }
    )
  end
  machine_spec.reference = {
    "driver_version" => Chef::Provisioning::AWSDriver::VERSION,
    "allocated_at" => Time.now.utc.to_s,
    "host_node" => action_handler.host_node,
    "image_id" => bootstrap_options[:image_id],
    "instance_id" => instance.id
  }
  machine_spec.driver_url = driver_url
  machine_spec.reference["key_name"] = bootstrap_options[:key_name] if bootstrap_options[:key_name]
  # TODO: 2.0 We no longer support `use_private_ip_for_ssh`, only `transport_address_location`
  if machine_options[:use_private_ip_for_ssh]
    unless @transport_address_location_warned
      Chef::Log.warn("The machine_option ':use_private_ip_for_ssh' has been deprecated, use ':transport_address_location'")
      @transport_address_location_warned = true
    end
    machine_options[:transport_address_location] ||= :private_ip
  end
  %w{is_windows winrm_username winrm_port winrm_password ssh_username sudo transport_address_location ssh_gateway}.each do |key|
    machine_spec.reference[key] = machine_options[key.to_sym] if machine_options[key.to_sym]
  end
  instance
end
create_servers(action_handler, specs_and_options, parallelizer) { |machine_spec, instance| ... } click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1440
def create_servers(action_handler, specs_and_options, parallelizer)
  specs_and_servers = instances_for(specs_and_options.keys)

  by_bootstrap_options = {}
  specs_and_options.each do |machine_spec, machine_options|
    instance = specs_and_servers[machine_spec]
    if instance
      if instance.state.name == "terminated"
        Chef::Log.warn "Machine #{machine_spec.name} (#{instance.id}) is terminated.  Recreating ..."
      else
        # Even though the instance has been created the tags could be incorrect if it
        # was created before tags were introduced
        converge_ec2_tags(instance, machine_options[:aws_tags], action_handler)
        yield machine_spec, instance if block_given?
        next
      end
    elsif machine_spec.reference
      Chef::Log.warn "Machine #{machine_spec.name} (#{machine_spec.reference['instance_id']} on #{driver_url}) no longer exists.  Recreating ..."
    end

    bootstrap_options = bootstrap_options_for(action_handler, machine_spec, machine_options)
    by_bootstrap_options[bootstrap_options] ||= []
    by_bootstrap_options[bootstrap_options] << machine_spec
  end

  # Create the servers in parallel
  parallelizer.parallelize(by_bootstrap_options) do |bootstrap_options, machine_specs|
    machine_description = if machine_specs.size == 1
                            "machine #{machine_specs.first.name}"
                          else
                            "machines #{machine_specs.map(&:name).join(', ')}"
    end
    description = ["creating #{machine_description} on #{driver_url}"]
    bootstrap_options.each_pair { |key, value| description << "  #{key}: #{value.inspect}" }
    action_handler.report_progress description
    if action_handler.should_perform_actions
      # Actually create the servers
      parallelizer.parallelize(1.upto(machine_specs.size)) do |_i|
        # Assign each one to a machine spec
        machine_spec = machine_specs.pop
        machine_options = specs_and_options[machine_spec]

        clean_bootstrap_options = Marshal.load(Marshal.dump(bootstrap_options))
        instance = create_instance_and_reference(clean_bootstrap_options, action_handler, machine_spec, machine_options)
        converge_ec2_tags(instance, machine_options[:aws_tags], action_handler)

        action_handler.performed_action "machine #{machine_spec.name} created as #{instance.id} on #{driver_url}"

        yield machine_spec, instance if block_given?
      end.to_a

      unless machine_specs.empty?
        raise "Not all machines were created by create_servers"
      end
    end
  end.to_a
end
create_ssh_transport(machine_spec, machine_options, instance) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1221
def create_ssh_transport(machine_spec, machine_options, instance)
  ssh_options = ssh_options_for(machine_spec, machine_options, instance)
  username = machine_spec.reference["ssh_username"] || machine_options[:ssh_username] || default_ssh_username
  if machine_options.key?(:ssh_username) && machine_options[:ssh_username] != machine_spec.reference["ssh_username"]
    Chef::Log.warn("Server #{machine_spec.name} was created with SSH username #{machine_spec.reference['ssh_username']} and machine_options specifies username #{machine_options[:ssh_username]}.  Using #{machine_spec.reference['ssh_username']}.  Please edit the node and change the chef_provisioning.reference.ssh_username attribute if you want to change it.")
  end
  options = {}
  if machine_spec.reference[:sudo] || (!machine_spec.reference.key?(:sudo) && username != "root")
    options[:prefix] = "sudo "
  end

  remote_host = determine_remote_host(machine_spec, instance)

  # Enable pty by default
  options[:ssh_pty_enable] = true

  if machine_spec.reference.key?("ssh_gateway")
    options[:ssh_gateway] = machine_spec.reference["ssh_gateway"]
  elsif machine_options[:ssh_gateway]
    options[:ssh_gateway] = machine_options[:ssh_gateway]
  end

  Chef::Provisioning::Transport::SSH.new(remote_host, username, ssh_options, options, config)
end
create_winrm_transport(machine_spec, machine_options, instance) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1098
def create_winrm_transport(machine_spec, machine_options, instance)
  remote_host = determine_remote_host(machine_spec, instance)
  username = machine_spec.reference["winrm_username"] ||
    machine_options[:winrm_username] ||
    default_winrm_username
  # default to http for now, should upgrade to https when knife support self-signed
  transport_type = machine_spec.reference["winrm_transport"] ||
    machine_options[:winrm_transport] ||
    default_winrm_transport
  type = case transport_type
         when "http"
           :plaintext
         when "https"
           :ssl
         end
  port = machine_spec.reference["winrm_port"] ||
    machine_options[:winrm_port] ||
    case transport_type
    when "http"
      "5985"
    when "https"
      "5986"
    end
  endpoint = "#{transport_type}://#{remote_host}:#{port}/wsman"

  pem_bytes = get_private_key(instance.key_name)

  password = machine_spec.reference["winrm_password"] ||
    machine_options[:winrm_password] ||
    begin
      if machine_spec.reference["winrm_encrypted_password"]
        decoded = Base64.decode64(machine_spec.reference["winrm_encrypted_password"])
      else
        encrypted_admin_password = instance.password_data.password_data
        if encrypted_admin_password.nil? || encrypted_admin_password.empty?
          raise "You did not specify winrm_password in the machine options and no encrytpted password could be fetched from the instance"
        end
        machine_spec.reference["winrm_encrypted_password"] ||= encrypted_admin_password
        # ^^ saves encrypted password to the machine_spec
        decoded = Base64.decode64(encrypted_admin_password)
      end
      # decrypt so we can utilize
      private_key = OpenSSL::PKey::RSA.new(get_private_key(instance.key_name))
      private_key.private_decrypt decoded
    end

  disable_sspi =  machine_spec.reference["winrm_disable_sspi"] ||
    machine_options[:winrm_disable_sspi] ||
    false # default to Negotiate
  basic_auth_only = machine_spec.reference["winrm_basic_auth_only"] ||
    machine_options[:winrm_basic_auth_only] ||
    false # disallow Basic auth by default
  no_ssl_peer_verification = machine_spec.reference["winrm_no_ssl_peer_verification"] ||
    machine_options[:winrm_no_ssl_peer_verification] ||
    false # disallow MITM potential by default

  winrm_options = {
    user: username,
    pass: password,
    disable_sspi: disable_sspi,
    basic_auth_only: basic_auth_only,
    no_ssl_peer_verification: no_ssl_peer_verification
  }

  if no_ssl_peer_verification || (type != :ssl)
    # =>  we won't verify certs at all
    Chef::Log.info "No SSL or no peer verification"
  elsif machine_spec.reference["winrm_ssl_thumbprint"]
    # we have stored the cert
    Chef::Log.info "Using stored fingerprint"
  else
    # we need to retrieve the cert and verify it by connecting just to
    # retrieve the ssl certificate and compare it to what we see in the
    # console logs
    instance.console_output.data.output
    # again this seem to need to be run twice, to ensure
    encoded_output = instance.console_output.data.output
    console_lines = Base64.decode64(encoded_output).lines
    fp_context = OpenSSL::SSL::SSLContext.new
    tcp_connection = TCPSocket.new(instance.private_ip_address, port)
    ssl_connection = OpenSSL::SSL::SSLSocket.new(tcp_connection, fp_context)

    begin
      ssl_connection.connect
    rescue OpenSSL::SSL::SSLError => e
      raise e unless e.message =~ /bad signature/
    ensure
      tcp_connection.close
    end

    winrm_cert = ssl_connection.peer_cert_chain.first

    rdp_thumbprint = console_lines.grep(
      /RDPCERTIFICATE-THUMBPRINT/
    )[-1].split(": ").last.chomp
    rdp_subject = console_lines.grep(
      /RDPCERTIFICATE-SUBJECTNAME/
    )[-1].split(": ").last.chomp
    winrm_subject = winrm_cert.subject.to_s.split("=").last.upcase
    winrm_thumbprint = OpenSSL::Digest::SHA1.new(winrm_cert.to_der).to_s.upcase

    if (rdp_subject != winrm_subject) || (rdp_thumbprint != winrm_thumbprint)
      Chef::Log.fatal "Winrm ssl port certificate differs from rdp console logs"
    end
    # now cache these for later use in the reference
    if machine_spec.reference["winrm_ssl_subject"] != winrm_subject
      machine_spec.reference["winrm_ssl_subject"] = winrm_subject
    end
    if machine_spec.reference["winrm_ssl_thumbprint"] != winrm_thumbprint
      machine_spec.reference["winrm_ssl_thumbprint"] = winrm_thumbprint
    end
    if machine_spec.reference["winrm_ssl_cert"] != winrm_cert.to_pem
      machine_spec.reference["winrm_ssl_cert"] = winrm_cert.to_pem
    end
  end

  if machine_spec.reference["winrm_ssl_thumbprint"]
    winrm_options[:ssl_peer_fingerprint] = machine_spec.reference["winrm_ssl_thumbprint"]
  end

  Chef::Provisioning::Transport::WinRM.new(endpoint.to_s, type, winrm_options, {})
end
deep_symbolize_keys(hash_like) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 169
def deep_symbolize_keys(hash_like)
  # Process arrays first...
  if hash_like.is_a?(Array)
    # Node attributes are an ImmutableArray so lets convert them to an array first
    hash_like = hash_like.to_a
    hash_like.length.times do |e|
      hash_like[e] = deep_symbolize_keys(hash_like[e]) if hash_like[e].respond_to?(:values) || hash_like[e].is_a?(Array)
    end
    return hash_like
  end
  # Otherwise return ourselves if not a hash
  return hash_like unless hash_like.respond_to?(:values)
  # Otherwise we are hash like, push on through...
  return {} if hash_like.nil? || hash_like.empty?
  r = {}
  hash_like.each do |key, value|
    value = deep_symbolize_keys(value) if value.respond_to?(:values) || value.is_a?(Array)
    r[key.to_sym] = value
  end
  r
end
default_ami_arch() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1060
def default_ami_arch
  "amd64"
end
default_ami_for_criteria(region, arch, release, root_store, virtualization_type) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1076
def default_ami_for_criteria(region, arch, release, root_store, virtualization_type)
  ami = Ubuntu.release(release).amis.find do |ami|
    ami.arch == arch &&
      ami.root_store == root_store &&
      ami.region == region &&
      ami.virtualization_type == virtualization_type
  end

  ami.name || raise("Default AMI not found")
end
default_ami_for_region(region, criteria = {}) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1087
def default_ami_for_region(region, criteria = {})
  Chef::Log.debug("Choosing default AMI for region '#{region}'")

  arch = criteria["arch"] || default_ami_arch
  release = criteria["release"] || default_ami_release
  root_store = criteria["root_store"] || default_ami_root_store
  virtualization_type = criteria["virtualization_type"] || default_ami_virtualization_type

  default_ami_for_criteria(region, arch, release, root_store, virtualization_type)
end
default_ami_release() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1064
def default_ami_release
  "vivid"
end
default_ami_root_store() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1068
def default_ami_root_store
  "ebs"
end
default_ami_virtualization_type() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1072
def default_ami_virtualization_type
  "hvm"
end
default_aws_keypair(action_handler, machine_spec) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1419
def default_aws_keypair(action_handler, machine_spec)
  driver = self
  default_key_name = default_aws_keypair_name(machine_spec)
  updated = @@chef_default_lock.synchronize do
    Provisioning.inline_resource(action_handler) do
      aws_key_pair default_key_name do
        driver driver
        chef_server machine_spec.managed_entry_store.chef_server
        managed_entry_store machine_spec.managed_entry_store
        allow_overwrite true
      end
    end
  end

  # Only warn the first time
  default_warning = "Using default key, which is not shared between machines!  It is recommended to create an AWS key pair with the aws_key_pair resource, and set :bootstrap_options => { :key_name => <key name> }"
  Chef::Log.warn(default_warning) if updated

  default_key_name
end
default_aws_keypair_name(machine_spec) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1410
def default_aws_keypair_name(machine_spec)
  if machine_spec.reference &&
      Gem::Version.new(machine_spec.reference["driver_version"]) < Gem::Version.new("0.10")
    "metal_default"
  else
    "chef_default"
  end
end
default_instance_type() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1626
def default_instance_type
  "t2.micro"
end
default_ssh_username() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 989
def default_ssh_username
  "ubuntu"
end
default_winrm_transport() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 997
def default_winrm_transport
  "http"
end
default_winrm_username() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 993
def default_winrm_username
  "Administrator"
end
destroy_image(action_handler, image_spec, image_options) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 633
def destroy_image(action_handler, image_spec, image_options)
  image_options = deep_symbolize_keys(image_options)
  # TODO: the driver should automatically be set by `inline_resource`
  d = self
  Provisioning.inline_resource(action_handler) do
    aws_image image_spec.name do
      action :destroy
      driver d
      chef_server image_spec.managed_entry_store.chef_server
      managed_entry_store image_spec.managed_entry_store
    end
  end
end
destroy_load_balancer(action_handler, lb_spec, lb_options) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 574
def destroy_load_balancer(action_handler, lb_spec, lb_options)
  lb_options = deep_symbolize_keys(lb_options)
  return if lb_spec.nil?

  actual_elb = load_balancer_for(lb_spec)
  if actual_elb
    # Remove ELB from AWS
    action_handler.perform_action "Deleting EC2 ELB #{lb_spec.id}" do
      elb.delete_load_balancer(load_balancer_name: actual_elb.load_balancer_name)
    end
  end

  # Remove LB spec from databag
  lb_spec.delete(action_handler)
end
destroy_machine(action_handler, machine_spec, machine_options) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 790
def destroy_machine(action_handler, machine_spec, machine_options)
  machine_options = deep_symbolize_keys(machine_options)
  d = self
  Provisioning.inline_resource(action_handler) do
    aws_instance machine_spec.name do
      action :destroy
      driver d
      chef_server machine_spec.managed_entry_store.chef_server
      managed_entry_store machine_spec.managed_entry_store
    end
  end

  # TODO: move this into the aws_instance provider somehow
  strategy = convergence_strategy_for(machine_spec, machine_options)
  strategy.cleanup_convergence(action_handler, machine_spec)
end
determine_remote_host(machine_spec, instance) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1246
def determine_remote_host(machine_spec, instance)
  transport_address_location = (machine_spec.reference["transport_address_location"] || :none).to_sym
  if machine_spec.reference["use_private_ip_for_ssh"]
    # The machine_spec has the old config key, lets update it - a successful chef converge will save the machine_spec
    # TODO in 2.0 get rid of this update
    machine_spec.reference.delete("use_private_ip_for_ssh")
    machine_spec.reference["transport_address_location"] = :private_ip
    instance.private_ip_address
  elsif transport_address_location == :private_ip
    instance.private_ip_address
  elsif transport_address_location == :dns
    instance.dns_name
  elsif !instance.public_ip_address && instance.private_ip_address
    Chef::Log.warn("Server #{machine_spec.name} has no public ip address.  Using private ip '#{instance.private_ip_address}'.  Set machine_options ':transport_address_location => :private_ip' if this will always be the case ...")
    instance.private_ip_address
  elsif instance.public_ip_address
    instance.public_ip_address
  else
    raise "Server #{instance.id} has no private or public IP address!"
  end
end
ec2() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 807
def ec2
  @ec2 ||= ::Aws::EC2::Client.new(aws_config)
end
elasticache() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 829
def elasticache
  @elasticache ||= ::Aws::ElastiCache::Client.new(aws_config)
end
elb() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 825
def elb
  @elb ||= ::Aws::ElasticLoadBalancing::Client.new(aws_config)
end
get_listener(listener) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1590
def get_listener(listener)
  result = {}

  case listener
  when Hash
    result.merge!(listener)
  when Array
    result[:load_balancer_port] = listener[0] if listener.size >= 1
    result[:protocol] = listener[1] if listener.size >= 2
  when Symbol, String
    result[:protocol] = listener
  when Integer
    result[:load_balancer_port] = listener
  else
    raise "Invalid listener #{listener}"
  end

  # If either port or protocol are set, set the other
  if result[:load_balancer_port] && !result[:protocol]
    result[:protocol] = PROTOCOL_DEFAULTS[result[:load_balancer_port]]
  elsif result[:protocol] && !result[:load_balancer_port]
    result[:load_balancer_port] = PORT_DEFAULTS[result[:protocol]]
  end
  if result[:instance_port] && !result[:instance_protocol]
    result[:instance_protocol] = PROTOCOL_DEFAULTS[result[:instance_port]]
  elsif result[:instance_protocol] && !result[:instance_port]
    result[:instance_port] = PORT_DEFAULTS[result[:instance_protocol]]
  end

  # If instance_port is still unset, copy port/protocol over
  result[:instance_port] ||= result[:load_balancer_port]
  result[:instance_protocol] ||= result[:protocol]

  result
end
get_listeners(listeners) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1569
def get_listeners(listeners)
  case listeners
  when Hash
    listeners.map do |from, to|
      from = get_listener(from)
      from.delete(:instance_port)
      from.delete(:instance_protocol)
      to = get_listener(to)
      to.delete(:load_balancer_port)
      to.delete(:protocol)
      to.merge(from)
    end
  when Array
    listeners.map { |listener| get_listener(listener) }
  when nil
    nil
  else
    [get_listener(listeners)]
  end
end
https_user_data() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 667
        def https_user_data
          <<EOD
<powershell>
winrm quickconfig -q
winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="300"}'
winrm set winrm/config '@{MaxTimeoutms="1800000"}'

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

$SourceStoreScope = 'LocalMachine'
$SourceStorename = 'Remote Desktop'

$SourceStore = New-Object  -TypeName System.Security.Cryptography.X509Certificates.X509Store  -ArgumentList $SourceStorename, $SourceStoreScope
$SourceStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly)

$cert = $SourceStore.Certificates | Where-Object  -FilterScript {
$_.subject -like '*'
}

$DestStoreScope = 'LocalMachine'
$DestStoreName = 'My'

$DestStore = New-Object  -TypeName System.Security.Cryptography.X509Certificates.X509Store  -ArgumentList $DestStoreName, $DestStoreScope
$DestStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
$DestStore.Add($cert)

$SourceStore.Close()
$DestStore.Close()

winrm create winrm/config/listener?Address=*+Transport=HTTPS  `@`{Hostname=`"($certId)`"`;CertificateThumbprint=`"($cert.Thumbprint)`"`}

net stop winrm
sc config winrm start=auto
net start winrm
</powershell>
EOD
        end
iam() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 833
def iam
  @iam ||= ::Aws::IAM::Client.new(aws_config)
end
image_for(image_spec) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1031
def image_for(image_spec)
  Chef::Resource::AwsImage.get_aws_object(image_spec.name, driver: self, managed_entry_store: image_spec.managed_entry_store, required: false)
end
instance_for(machine_spec) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1016
def instance_for(machine_spec)
  if machine_spec.reference
    if machine_spec.driver_url != driver_url
      raise "Switching a machine's driver from #{machine_spec.driver_url} to #{driver_url} is not currently supported!  Use machine :destroy and then re-create the machine on the new driver."
    end
    Chef::Resource::AwsInstance.get_aws_object(machine_spec.reference["instance_id"], driver: self, managed_entry_store: machine_spec.managed_entry_store, required: false)
  end
end
instances_for(machine_specs) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1025
def instances_for(machine_specs)
  result = {}
  machine_specs.each { |machine_spec| result[machine_spec] = instance_for(machine_spec) }
  result
end
keypair_for(bootstrap_options) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1001
def keypair_for(bootstrap_options)
  if bootstrap_options[:key_name]
    keypair_name = bootstrap_options[:key_name]
    actual_key_pair = ec2_resource.key_pair(keypair_name)
    unless actual_key_pair.exists?
      ec2_resource.key_pairs.create(keypair_name)
    end
    actual_key_pair
  end
end
load_balancer_for(lb_spec) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1012
def load_balancer_for(lb_spec)
  Chef::Resource::AwsLoadBalancer.get_aws_object(lb_spec.name, driver: self, managed_entry_store: lb_spec.managed_entry_store, required: false)
end
machine_for(machine_spec, machine_options, instance = nil) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 891
def machine_for(machine_spec, machine_options, instance = nil)
  instance ||= instance_for(machine_spec)

  unless instance
    raise "Instance for node #{machine_spec.name} has not been created!"
  end

  if machine_spec.reference["is_windows"]
    Chef::Provisioning::Machine::WindowsMachine.new(machine_spec, transport_for(machine_spec, machine_options, instance), convergence_strategy_for(machine_spec, machine_options))
  else
    Chef::Provisioning::Machine::UnixMachine.new(machine_spec, transport_for(machine_spec, machine_options, instance), convergence_strategy_for(machine_spec, machine_options))
  end
end
parse_arn(arn) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 861
def parse_arn(arn)
  parts = arn.split(":", 6)
  {
    partition: parts[1],
    service: parts[2],
    region: parts[3],
    account_id: parts[4],
    resource: parts[5]
  }
end
private_key_for(machine_spec, machine_options, instance) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1268
def private_key_for(machine_spec, machine_options, instance)
  if instance.respond_to?(:private_key) && instance.private_key
    instance.private_key
  elsif instance.respond_to?(:key_name) && instance.key_name
    key = get_private_key(instance.key_name)
    unless key
      raise "Server has key name '#{instance.key_name}', but the corresponding private key was not found locally.  Check if the key is in Chef::Config.private_key_paths: #{Chef::Config.private_key_paths.join(', ')}"
    end
    key
  elsif machine_spec.reference["key_name"]
    key = get_private_key(machine_spec.reference["key_name"])
    unless key
      raise "Server was created with key name '#{machine_spec.reference['key_name']}', but the corresponding private key was not found locally.  Check if the key is in Chef::Config.private_key_paths: #{Chef::Config.private_key_paths.join(', ')}"
    end
    key
  elsif machine_options[:bootstrap_options] && machine_options[:bootstrap_options][:key_path]
    IO.read(machine_options[:bootstrap_options][:key_path])
  elsif machine_options[:bootstrap_options] && machine_options[:bootstrap_options][:key_name]
    get_private_key(machine_options[:bootstrap_options][:key_name])
  else
    # TODO: make a way to suggest other keys to try ...
    raise "No key found to connect to #{machine_spec.name} (#{machine_spec.reference.inspect})!"
  end
end
rds() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 837
def rds
  @rds ||= ::Aws::RDS::Client.new(aws_config)
end
ready_image(action_handler, image_spec, image_options) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 617
def ready_image(action_handler, image_spec, image_options)
  actual_image = image_for(image_spec)
  if actual_image.nil? || !actual_image.exists?
    raise "Cannot ready an image that does not exist"
  else
    image_options = deep_symbolize_keys(image_options)
    aws_tags = image_options.delete(:aws_tags) || {}
    aws_tags["from-instance"] = image_spec.reference["from-instance"] if image_spec.reference["from-instance"]
    converge_ec2_tags(actual_image, aws_tags, action_handler)
    if actual_image.state.to_sym != :available
      action_handler.report_progress "Waiting for image to be ready ..."
      wait_until_ready_image(action_handler, image_spec, actual_image)
    end
  end
end
ready_load_balancer(action_handler, lb_spec, lb_options, machine_spec) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 572
def ready_load_balancer(action_handler, lb_spec, lb_options, machine_spec); end
ready_machine(action_handler, machine_spec, machine_options) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 727
def ready_machine(action_handler, machine_spec, machine_options)
  machine_options = deep_symbolize_keys(machine_options)
  instance = instance_for(machine_spec)
  converge_ec2_tags(instance, machine_options[:aws_tags], action_handler)

  if instance.nil?
    raise "Machine #{machine_spec.name} does not have an instance associated with it, or instance does not exist."
  end

  if instance.state.name != "running"
    wait_until_machine(action_handler, machine_spec, "finish stopping", instance) { |instance| instance.state.name != "stopping" }
    if instance.state.name == "stopped"
      action_handler.perform_action "Start #{machine_spec.name} (#{machine_spec.reference['instance_id']}) in #{region} ..." do
        instance.start
      end
    end
    wait_until_instance_running(action_handler, machine_spec, instance)
  end

  # Windows machines potentially do a bunch of extra stuff - setting hostname,
  # sending out encrypted password, restarting instance, etc.
  if machine_spec.reference["is_windows"]
    wait_until_machine(action_handler, machine_spec, "receive 'Windows is ready' message from the AWS console", instance) do |instance|
      instance.console_output.output
      # seems to be a bug as we need to run this twice
      # to consistently ensure the output is fully pulled
      encoded_output = instance.console_output.output
      if encoded_output.nil? || encoded_output.empty?
        false
      else
        output = Base64.decode64(encoded_output)
        output =~ /Message: Windows is Ready to use/
      end
    end
  end
  wait_for_transport(action_handler, machine_spec, machine_options, instance)
  machine_for(machine_spec, machine_options, instance)
end
region() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 157
def region
  aws_config_2[:region]
end
s3_client() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 841
def s3_client
  @s3 ||= ::Aws::S3::Client.new(aws_config)
end
server_cert_from_spec(spec) click to toggle source

Retreive the server certificate from a listener spec, prefering the server_certificate key.

# File lib/chef/provisioning/aws_driver/driver.rb, line 564
def server_cert_from_spec(spec)
  if spec[:server_certificate]
    spec[:server_certificate]
  elsif spec[:ssl_certificate_id]
    spec[:ssl_certificate_id]
  end
end
server_cert_to_string(cert) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 554
def server_cert_to_string(cert)
  if cert.is_a?(Hash) && cert.key?(:arn)
    cert[:arn]
  else
    cert
  end
end
server_certificate_eql?(cert1, cert2) click to toggle source

Compare two server certificates by casting them both to strings.

The parameters should either be a String containing the certificate ARN, or a IAM::ServerCertificate object.

# File lib/chef/provisioning/aws_driver/driver.rb, line 550
def server_certificate_eql?(cert1, cert2)
  server_cert_to_string(cert1) == server_cert_to_string(cert2)
end
sns() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 845
def sns
  @sns ||= ::Aws::SNS::Client.new(aws_config)
end
sqs() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 849
def sqs
  @sqs ||= ::Aws::SQS::Client.new(aws_config)
end
ssh_options_for(machine_spec, machine_options, instance) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1293
def ssh_options_for(machine_spec, machine_options, instance)
  result = {
    # TODO: create a user known hosts file
    #          :user_known_hosts_file => vagrant_ssh_config['UserKnownHostsFile'],
    #          :paranoid => true,
    auth_methods: ["publickey"],
    keys_only: true,
    host_key_alias: "#{instance.id}.AWS"
  }.merge(machine_options[:ssh_options] || {})
  unless result.key?(:key_data)
    result[:keys_only] = true
    result[:key_data] = [private_key_for(machine_spec, machine_options, instance)]
  end
  result
end
stop_machine(action_handler, machine_spec, machine_options) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 776
def stop_machine(action_handler, machine_spec, machine_options)
  machine_options = deep_symbolize_keys(machine_options)
  instance = instance_for(machine_spec)
  if instance && instance.exists?
    wait_until_machine(action_handler, machine_spec, "finish coming up so we can stop it", instance) { |instance| instance.state.name != "pending" }
    if instance.state.name == "running"
      action_handler.perform_action "Stop #{machine_spec.name} (#{instance.id}) in #{region} ..." do
        instance.stop
      end
    end
    wait_until_machine(action_handler, machine_spec, "stop", instance) { |instance| %w{stopped terminated}.include?(instance.state.name) }
  end
end
transport_for(machine_spec, machine_options, instance) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1035
def transport_for(machine_spec, machine_options, instance)
  if machine_spec.reference["is_windows"]
    create_winrm_transport(machine_spec, machine_options, instance)
  else
    create_ssh_transport(machine_spec, machine_options, instance)
  end
end
user_data() click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 647
        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_transport(action_handler, machine_spec, machine_options, instance = nil) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1388
def wait_for_transport(action_handler, machine_spec, machine_options, instance = nil)
  instance ||= instance_for(machine_spec)
  sleep_time = 10
  transport = transport_for(machine_spec, machine_options, instance)
  unless instance.state.name.eql?("running") && transport.available?
    if action_handler.should_perform_actions
      action_handler.report_progress "waiting for #{machine_spec.name} (#{instance.id} on #{driver_url}) to be connectable (transport up and running) ..."
      max_wait_time = Chef::Config.chef_provisioning[:machine_max_wait_time] || 120
      Retryable.retryable(
        tries: (max_wait_time / sleep_time).to_i,
        sleep: sleep_time,
        matching: /did not become connectable within/
      ) do |retries, _exception|
        action_handler.report_progress "been waiting #{sleep_time * retries}/#{max_wait_time} -- sleeping #{sleep_time} seconds for #{machine_spec.name} (#{instance.id} on #{driver_url}) to become connectable ..."
        unless transport.available?
          raise "Instance #{machine_spec.name} (#{instance.id} on #{driver_url}) did not become connectable within #{max_wait_time} seconds"
        end
      end
    end
  end
end
wait_until_image(action_handler, image_spec, image = nil) { |image| ... } click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1336
def wait_until_image(action_handler, image_spec, image = nil)
  image ||= image_for(image_spec)
  sleep_time = 10
  unless yield(image)
    if action_handler.should_perform_actions
      action_handler.report_progress "waiting for #{image_spec.name} (#{image.id} on #{driver_url}) to be ready ..."
      max_wait_time = Chef::Config.chef_provisioning[:image_max_wait_time] || 300
      Retryable.retryable(
        tries: (max_wait_time / sleep_time).to_i,
        sleep: sleep_time,
        matching: /did not become ready within/
      ) do |retries, _exception|
        action_handler.report_progress "been waiting #{retries * sleep_time}/#{max_wait_time} -- sleeping #{sleep_time} seconds for #{image_spec.name} (#{image.id} on #{driver_url}) to become ready ..."
        # We have to manually reload the instance each loop, otherwise data is stale
        image.reload
        unless yield(image)
          raise "Image #{image.id} did not become ready within #{max_wait_time} seconds"
        end
      end
    end
  end
end
wait_until_instance_running(action_handler, machine_spec, instance = nil) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1359
def wait_until_instance_running(action_handler, machine_spec, instance = nil)
  wait_until_machine(action_handler, machine_spec, "become ready", instance) do |instance|
    instance.state.name == "running"
  end
end
wait_until_machine(action_handler, machine_spec, output_msg, instance = nil) { |instance| ... } click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1365
def wait_until_machine(action_handler, machine_spec, output_msg, instance = nil)
  instance ||= instance_for(machine_spec)
  sleep_time = 10
  unless yield(instance)
    if action_handler.should_perform_actions
      action_handler.report_progress "waiting for #{machine_spec.name} (#{instance.id} on #{driver_url}) to #{output_msg} ..."
      max_wait_time = Chef::Config.chef_provisioning[:machine_max_wait_time] || 120
      Retryable.retryable(
        tries: (max_wait_time / sleep_time).to_i,
        sleep: sleep_time,
        matching: /did not #{output_msg} within/
      ) do |retries, _exception|
        action_handler.report_progress "been waiting #{sleep_time * retries}/#{max_wait_time} -- sleeping #{sleep_time} seconds for #{machine_spec.name} (#{instance.id} on #{driver_url}) to #{output_msg} ..."
        # We have to manually reload the instance each loop, otherwise data is stale
        instance.reload
        unless yield(instance)
          raise "Instance #{machine_spec.name} (#{instance.id} on #{driver_url}) did not #{output_msg} within #{max_wait_time} seconds"
        end
      end
    end
  end
end
wait_until_ready_image(action_handler, image_spec, image = nil) click to toggle source
# File lib/chef/provisioning/aws_driver/driver.rb, line 1331
def wait_until_ready_image(action_handler, image_spec, image = nil)
  wait_until_image(action_handler, image_spec, image) { |image| image.state.to_sym == :available }
  action_handler.report_progress "Image #{image_spec.name} is now ready"
end