Skip to content

Commit

Permalink
Add spot instance support
Browse files Browse the repository at this point in the history
rebased on current 0.5.0 with original work at mitchellh#32
  • Loading branch information
nabeken committed Jul 29, 2014
1 parent 7c36e2a commit 8f675c1
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 1 deletion.
73 changes: 72 additions & 1 deletion lib/vagrant-aws/action/run_instance.rb
Expand Up @@ -100,7 +100,12 @@ def call(env)
begin
env[:ui].warn(I18n.t("vagrant_aws.warn_ssh_access")) unless allows_ssh_port?(env, security_groups, subnet_id)

server = env[:aws_compute].servers.create(options)
server = if region_config.spot_instance
server_from_spot_request(env, region_config)
else
env[:aws_compute].servers.create(options)
end
raise Errors::FogError, :message => "server is nil" unless server
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 @@ -176,6 +181,72 @@ def call(env)
@app.call(env)
end

# returns a fog server or nil
def server_from_spot_request(env, config)
# prepare request args
options = {
'InstanceCount' => 1,
'LaunchSpecification.KeyName' => config.keypair_name,
'LaunchSpecification.Placement.AvailabilityZone' => config.availability_zone,
'LaunchSpecification.UserData' => config.user_data,
'LaunchSpecification.SubnetId' => config.subnet_id,
'ValidUntil' => config.spot_valid_until
}
security_group_key = config.subnet_id.nil? ? 'LaunchSpecification.SecurityGroup' : 'LaunchSpecification.SecurityGroupId'
options[security_group_key] = config.security_groups
options.delete_if { |key, value| value.nil? }

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

# create the spot instance
spot_req = env[:aws_compute].request_spot_instances(
config.ami,
config.instance_type,
config.spot_max_price,
options).body["spotInstanceRequestSet"].first

spot_request_id = spot_req["spotInstanceRequestId"]
@logger.info("Spot request ID: #{spot_request_id}")

# initialize state
status_code = ""
while true
sleep 5 # TODO make it a param

raise Errors::FogError, :message => "Interrupted" if env[:interrupted]
spot_req = env[:aws_compute].describe_spot_instance_requests(
'spot-instance-request-id' => [spot_request_id]).body["spotInstanceRequestSet"].first

# waiting for spot request ready
next unless spot_req

# display something whenever the status code changes
if status_code != spot_req["state"]
env[:ui].info(spot_req["fault"]["message"])
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
env[:aws_compute].cancel_spot_instance_requests(spot_request_id)
env[:aws_compute].servers.get(spot_req["instanceId"])
end

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

Expand Down
29 changes: 29 additions & 0 deletions lib/vagrant-aws/config.rb
Expand Up @@ -108,6 +108,21 @@ class Config < Vagrant.plugin("2", :config)
# @return [Array<Hash>]
attr_accessor :block_device_mapping

# 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

# Indicates whether an instance stops or terminates when you initiate shutdown from the instance
#
# @return [bool]
Expand Down Expand Up @@ -162,6 +177,10 @@ def initialize(region_specific=false)
@user_data = UNSET_VALUE
@use_iam_profile = UNSET_VALUE
@block_device_mapping = []
@use_iam_profile = UNSET_VALUE
@spot_instance = UNSET_VALUE
@spot_max_price = UNSET_VALUE
@spot_valid_until = UNSET_VALUE
@elastic_ip = UNSET_VALUE
@iam_instance_profile_arn = UNSET_VALUE
@iam_instance_profile_name = UNSET_VALUE
Expand Down Expand Up @@ -297,6 +316,15 @@ def finalize!
# User Data is nil by default
@user_data = nil if @user_data == UNSET_VALUE

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

# Required, no default
@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 false
@terminate_on_shutdown = false if @terminate_on_shutdown == UNSET_VALUE

Expand Down Expand Up @@ -362,6 +390,7 @@ def validate(machine)
end

errors << I18n.interpolate("vagrant_aws.config.ami_required", :region => @region) if config.ami.nil?
errors << I18n.interpolate("vagrant_aws.config.spot_price_required") if config.spot_instance && config.spot_max_price.nil?
end

{ "AWS Provider" => errors }
Expand Down
4 changes: 4 additions & 0 deletions locales/en.yml
Expand Up @@ -16,6 +16,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 @@ -59,6 +61,8 @@ en:
An access key ID must be specified via "access_key_id"
ami_required: |-
An AMI must be configured via "ami" (region: #{region})
spot_price_required: |-
Spot request is missing "spot_max_price"
private_key_missing: |-
The specified private key for AWS could not be found
region_required: |-
Expand Down
3 changes: 3 additions & 0 deletions spec/vagrant-aws/config_spec.rb
Expand Up @@ -32,6 +32,9 @@
its("user_data") { should be_nil }
its("use_iam_profile") { should be_false }
its("block_device_mapping") {should == [] }
its("spot_instance") { should be_false }
its("spot_max_price") { should be_nil }
its("spot_valid_until") { should be_nil }
its("elastic_ip") { should be_nil }
its("terminate_on_shutdown") { should == false }
its("ssh_host_attribute") { should be_nil }
Expand Down

0 comments on commit 8f675c1

Please sign in to comment.