lib/kitchen/driver/ec2.rb in kitchen-ec2-3.6.0 vs lib/kitchen/driver/ec2.rb in kitchen-ec2-3.7.0

- old
+ new

@@ -225,47 +225,31 @@ config[:aws_ssh_key_id] = nil end if config[:spot_price] # Spot instance when a price is set - server = with_request_limit_backoff(state) { submit_spots(state) } + server = with_request_limit_backoff(state) { submit_spots } else # On-demand instance server = with_request_limit_backoff(state) { submit_server } end info("Instance <#{server.id}> requested.") with_request_limit_backoff(state) do logging_proc = ->(attempts) { info("Polling AWS for existence, attempt #{attempts}...") } server.wait_until_exists(before_attempt: logging_proc) end + state[:server_id] = server.id + info("EC2 instance <#{state[:server_id]}> created.") + # See https://github.com/aws/aws-sdk-ruby/issues/859 - # Tagging can fail with a NotFound error even though we waited until the server exists - # Waiting can also fail, so we have to also retry on that. If it means we re-tag the - # instance, so be it. - # Tagging an instance is possible before volumes are attached. Tagging the volumes after - # instance creation is consistent. + # Waiting can fail, so we have to retry on that. Retryable.retryable( tries: 10, sleep: lambda { |n| [2**n, 30].min }, on: ::Aws::EC2::Errors::InvalidInstanceIDNotFound ) do |r, _| - info("Attempting to tag the instance, #{r} retries") - tag_server(server) - - # Get information about the AMI (image) used to create the image. - image_data = ec2.client.describe_images({ image_ids: [server.image_id] })[0][0] - - state[:server_id] = server.id - info("EC2 instance <#{state[:server_id]}> created.") - - # instance-store backed images do not have attached volumes, so only - # wait for the volumes to be ready if the instance EBS-backed. - if image_data.root_device_type == "ebs" - wait_until_volumes_ready(server, state) - tag_volumes(server) - end wait_until_ready(server, state) end if windows_os? && instance.transport[:username] =~ /administrator/i && @@ -295,17 +279,10 @@ server.terminate rescue ::Aws::EC2::Errors::InvalidInstanceIDNotFound => e warn("Received #{e}, instance was probably already destroyed. Ignoring") end end - if state[:spot_request_id] - debug("Deleting spot request <#{state[:server_id]}>") - ec2.client.cancel_spot_instance_requests( - spot_instance_request_ids: [state[:spot_request_id]] - ) - state.delete(:spot_request_id) - end # If we are going to clean up an automatic security group, we need # to wait for the instance to shut down. This slightly breaks the # subsystem encapsulation, sorry not sorry. if state[:auto_security_group_id] && server server.wait_until_terminated do |waiter| @@ -407,19 +384,18 @@ def instance_generator @instance_generator = Aws::InstanceGenerator.new(config, ec2, instance.logger) end - # Fog AWS helper for creating the instance + # AWS helper for creating the instance def submit_server instance_data = instance_generator.ec2_instance_data debug("Creating EC2 instance in region #{config[:region]} with properties:") instance_data.each do |key, value| debug("- #{key} = #{value.inspect}") end - instance_data[:min_count] = 1 - instance_data[:max_count] = 1 + ec2.create_instance(instance_data) end def config return super unless @config @@ -443,11 +419,11 @@ end configs end - def submit_spots(state) + def submit_spots configs = [config] expanded = [] keys = %i{instance_type subnet_id} keys.each do |key| @@ -460,97 +436,58 @@ errs = [] configs.each do |conf| begin @config = conf - return submit_spot(state) + return submit_spot rescue => e errs.append(e) end end raise ["Could not create a spot instance:", errs].flatten.join("\n") end - def submit_spot(state) + def submit_spot debug("Creating EC2 Spot Instance..") + instance_data = instance_generator.ec2_instance_data - spot_request_id = create_spot_request - # deleting the instance cancels the request, but deleting the request - # does not affect the instance - state[:spot_request_id] = spot_request_id - ec2.client.wait_until( - :spot_instance_request_fulfilled, - spot_instance_request_ids: [spot_request_id] - ) do |w| - w.max_attempts = config[:spot_wait] / config[:retryable_sleep] - w.delay = config[:retryable_sleep] - w.before_attempt do |attempts| - c = attempts * config[:retryable_sleep] - t = config[:spot_wait] - info "Waited #{c}/#{t}s for spot request <#{spot_request_id}> to become fulfilled." - end - end - ec2.get_instance_from_spot_request(spot_request_id) - end - - def create_spot_request request_duration = config[:spot_wait] config_spot_price = config[:spot_price].to_s if %w{ondemand on-demand}.include?(config_spot_price) spot_price = "" else spot_price = config_spot_price end - request_data = { - spot_price: spot_price, - launch_specification: instance_generator.ec2_instance_data, + spot_options = { + spot_instance_type: "persistent", # Cannot use one-time with valid_until valid_until: Time.now + request_duration, + instance_interruption_behavior: "stop", } if config[:block_duration_minutes] - request_data[:block_duration_minutes] = config[:block_duration_minutes] + spot_options[:block_duration_minutes] = config[:block_duration_minutes] end - - response = ec2.client.request_spot_instances(request_data) - response[:spot_instance_requests][0][:spot_instance_request_id] - end - - def tag_server(server) - if config[:tags] && !config[:tags].empty? - tags = config[:tags].map do |k, v| - # we convert the value to a string because - # nils should be passed as an empty String - # and Integers need to be represented as Strings - { key: k.to_s, value: v.to_s } - end - server.create_tags(tags: tags) + unless spot_price == "" # i.e. on-demand + spot_options[:max_price] = spot_price end - end - def tag_volumes(server) - if config[:tags] && !config[:tags].empty? - tags = config[:tags].map do |k, v| - { key: k.to_s, value: v.to_s } - end - server.volumes.each do |volume| - volume.create_tags(tags: tags) - end - end - end + instance_data[:instance_market_options] = { + market_type: "spot", + spot_options: spot_options, + } - # Compares the requested volume count vs what has actually been set to be - # attached to the instance. The information requested through - # ec2.client.described_volumes is updated before the instance volume - # information. - def wait_until_volumes_ready(server, state) - wait_with_destroy(server, state, "volumes to be ready") do |aws_instance| - described_volume_count = 0 - ready_volume_count = 0 - if aws_instance.exists? - described_volume_count = ec2.client.describe_volumes(filters: [ - { name: "attachment.instance-id", values: ["#{state[:server_id]}"] }]).volumes.length - aws_instance.volumes.each { ready_volume_count += 1 } - end - (described_volume_count > 0) && (described_volume_count == ready_volume_count) + # The preferred way to create a spot instance is via request_spot_instances() + # However, it does not allow for tagging to occur at creation time. + # create_instances() allows creation of tagged spot instances, but does + # not retry if the price could not be satisfied immediately. + Retryable.retryable( + tries: config[:spot_wait] / config[:retryable_sleep], + sleep: lambda { |_n| config[:retryable_sleep] }, + on: ::Aws::EC2::Errors::SpotMaxPriceTooLow + ) do |retries| + c = retries * config[:retryable_sleep] + t = config[:spot_wait] + info "Waited #{c}/#{t}s for spot request to become fulfilled." + ec2.create_instance(instance_data) end end # Normally we could use `server.wait_until_running` but we actually need # to check more than just the instance state