Skip to content
This repository has been archived by the owner on Feb 11, 2022. It is now read-only.

Spot Instances #514

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ EC2 and VPC.
* Define region-specific configurations so Vagrant can manage machines
in multiple regions.
* Package running instances into new vagrant-aws friendly boxes
* Spot Instance Support

## Usage

Expand Down Expand Up @@ -170,6 +171,11 @@ This provider exposes quite a few provider-specific configuration options:
when you initiate shutdown from the instance.
* `endpoint` - The endpoint URL for connecting to AWS (or an AWS-like service). Only required for non AWS clouds, such as [eucalyptus](https://github.com/eucalyptus/eucalyptus/wiki).

* `spot_instance` - Boolean value; indicates whether the config is for a spot instance, or on-demand. For more information about spot instances, see the [AWS Documentation](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/how-spot-instances-work.html)
* `spot_max_price` - Decimal value; state the maximum bid for your spot instance. If nil, it will compute average price in `region` for selected `instance_type`.
* `spot_price_product_description` - The product description for the spot price history used to compute average price. Defaults to 'Linux/UNIX'.
* `spot_valid_until` - Timestamp; when this spot instance request should expire, destroying any related instances. Ignored if `spot_instance` is not true.

These can be set like typical provider-specific configuration:

```ruby
Expand Down
87 changes: 86 additions & 1 deletion lib/vagrant-aws/action/run_instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,11 @@ def call(env)
end

begin
server = env[:aws_compute].servers.create(options)
server = if region_config.spot_instance
server_from_spot_request(env, region_config, options)
else
env[:aws_compute].servers.create(options)
end
rescue Fog::Compute::AWS::NotFound => e
# Invalid subnet doesn't have its own error so we catch and
# check the error message here.
Expand Down Expand Up @@ -212,6 +216,87 @@ def call(env)
@app.call(env)
end

# returns a fog server or nil
def server_from_spot_request(env, config, options)
if config.spot_max_price.nil?
spot_price_current = env[:aws_compute].describe_spot_price_history({
'StartTime' => Time.now.iso8601,
'EndTime' => Time.now.iso8601,
'InstanceType' => [config.instance_type],
'ProductDescription' => [config.spot_price_product_description.nil? ? 'Linux/UNIX' : config.spot_price_product_description]
})

spot_price_current.body['spotPriceHistorySet'].each do |set|
(@price_set ||= []) << set['spotPrice'].to_f
end

if @price_set.nil?
raise Errors::FogError,
:message => "Could not find any history spot prices."
end

avg_price = @price_set.inject(0.0) { |sum, el| sum + el } / @price_set.size

# make the bid 10% higher than the average
price = (avg_price * 1.1).round(4)
else
price = config.spot_max_price
end

options.merge!({
:price => price,
:valid_until => config.spot_valid_until
})

env[:ui].info(I18n.t("vagrant_aws.launching_spot_instance"))
env[:ui].info(" -- Price: #{price}")
env[:ui].info(" -- Valid until: #{config.spot_valid_until}") if config.spot_valid_until

# create the spot instance
spot_req = env[:aws_compute].spot_requests.create(options)

@logger.info("Spot request ID: #{spot_req.id}")

# initialize state
status_code = ""
while true
sleep 5

spot_req.reload()

# display something whenever the status code changes
if status_code != spot_req.state
env[:ui].info(spot_req.fault)
status_code = spot_req.state
end
spot_state = spot_req.state.to_sym
case spot_state
when :not_created, :open
@logger.debug("Spot request #{spot_state} #{status_code}, waiting")
when :active
break; # :)
when :closed, :cancelled, :failed
msg = "Spot request #{spot_state} #{status_code}, aborting"
@logger.error(msg)
raise Errors::FogError, :message => msg
else
@logger.debug("Unknown spot state #{spot_state} #{status_code}, waiting")
end
end
# cancel the spot request but let the server go thru
spot_req.destroy()

server = env[:aws_compute].servers.get(spot_req.instance_id)

# Spot Instances don't support tagging arguments on creation
# Retrospectively tag the server to handle this
if !config.tags.empty?
env[:aws_compute].create_tags(server.identity, config.tags)
end

server
end

def recover(env)
return if env["vagrant.error"].is_a?(Vagrant::Errors::VagrantError)

Expand Down
39 changes: 37 additions & 2 deletions lib/vagrant-aws/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,26 @@ class Config < Vagrant.plugin("2", :config)
# @return [String]
attr_accessor :aws_profile

# Launch as spot instance
#
# @return [Boolean]
attr_accessor :spot_instance

# Spot request max price
#
# @return [String]
attr_accessor :spot_max_price

# Spot request validity
#
# @return [Time]
attr_accessor :spot_valid_until

# The product description for the spot price history
#
# @return [String]
attr_accessor :spot_price_product_description

def initialize(region_specific=false)
@access_key_id = UNSET_VALUE
@ami = UNSET_VALUE
Expand Down Expand Up @@ -233,6 +253,9 @@ def initialize(region_specific=false)
@tenancy = UNSET_VALUE
@aws_dir = UNSET_VALUE
@aws_profile = UNSET_VALUE
@spot_instance = UNSET_VALUE
@spot_max_price = UNSET_VALUE
@spot_valid_until = UNSET_VALUE

# Internal state (prefix with __ so they aren't automatically
# merged)
Expand Down Expand Up @@ -409,6 +432,18 @@ def finalize!
# default to nil
@kernel_id = nil if @kernel_id == UNSET_VALUE

# By default don't use spot requests
@spot_instance = false if @spot_instance == UNSET_VALUE

# default to nil
@spot_max_price = nil if @spot_max_price == UNSET_VALUE

# Default: Request is effective indefinitely.
@spot_valid_until = nil if @spot_valid_until == UNSET_VALUE

# default to nil
@spot_price_product_description = nil if @spot_price_product_description == UNSET_VALUE

# Compile our region specific configurations only within
# NON-REGION-SPECIFIC configurations.
if !@__region_specific
Expand Down Expand Up @@ -479,14 +514,14 @@ def get_region_config(name)


class Credentials < Vagrant.plugin("2", :config)
# This module reads AWS config and credentials.
# This module reads AWS config and credentials.
# Behaviour aims to mimic what is described in AWS documentation:
# http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html
# http://docs.aws.amazon.com/cli/latest/topic/config-vars.html
# Which is the following (stopping at the first successful case):
# 1) read config and credentials from environment variables
# 2) read config and credentials from files at location defined by environment variables
# 3) read config and credentials from files at default location
# 3) read config and credentials from files at default location
#
# The mandatory fields for a successful "get credentials" are the id and the secret keys.
# Region is not required since Config#finalize falls back to sensible defaults.
Expand Down
4 changes: 3 additions & 1 deletion locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ en:

launching_instance: |-
Launching an instance with the following settings...
launching_spot_instance: |-
Launching a spot request instance with the following settings...
launch_no_keypair: |-
Warning! You didn't specify a keypair to launch your instance with.
This can sometimes result in not being able to access your instance.
Expand Down Expand Up @@ -104,7 +106,7 @@ en:
Error: %{err}
instance_package_timeout: |-
The AMI failed to become "ready" in AWS. The timeout currently
set waiting for the instance to become ready is %{timeout} seconds. For
set waiting for the instance to become ready is %{timeout} seconds. For
larger instances AMI burning may take long periods of time. Please
ensure the timeout is set high enough, it can be changed by adjusting
the `instance_package_timeout` configuration on the AWS provider.
Expand Down
4 changes: 4 additions & 0 deletions spec/vagrant-aws/config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@
its("associate_public_ip") { should == false }
its("unregister_elb_from_az") { should == true }
its("tenancy") { should == "default" }
its("spot_instance") { should == false }
its("spot_max_price") { should be_nil }
its("spot_price_product_description") { should be_nil }
its("spot_valid_until") { should be_nil }
end

describe "overriding defaults" do
Expand Down