Skip to content

Commit

Permalink
Merge remote-tracking branch 'nabeken/spot-2' into spot-instances
Browse files Browse the repository at this point in the history
  • Loading branch information
varju committed May 14, 2014
2 parents d125a2f + 86ff6b6 commit 0bb9f27
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 22 deletions.
119 changes: 97 additions & 22 deletions lib/vagrant-aws/action/run_instance.rb
Expand Up @@ -97,31 +97,42 @@ def call(env)
options[security_group_key] = security_groups
end

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)
rescue Fog::Compute::AWS::NotFound => e
# Invalid subnet doesn't have its own error so we catch and
# check the error message here.
if e.message =~ /subnet ID/
raise Errors::FogError,
:message => "Subnet ID not found: #{subnet_id}"
end
if region_config.spot_instance
server = server_from_spot_request(env, region_config)
else
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)
rescue Fog::Compute::AWS::NotFound => e
# Invalid subnet doesn't have its own error so we catch and
# check the error message here.
if e.message =~ /subnet ID/
raise Errors::FogError,
:message => "Subnet ID not found: #{subnet_id}"
end

raise
rescue Fog::Compute::AWS::Error => e
raise Errors::FogError, :message => e.message
rescue Excon::Errors::HTTPStatusError => e
raise Errors::InternalFogError,
:error => e.message,
:response => e.response.body
raise
rescue Fog::Compute::AWS::Error => e
raise Errors::FogError, :message => e.message
rescue Excon::Errors::HTTPStatusError => e
raise Errors::InternalFogError,
:error => e.message,
:response => e.response.body
end
end

# Immediately save the ID since it is created at this point.
env[:machine].id = server.id
if server
# Immediately save the ID since it is created at this point.
env[:machine].id = server.id

# Wait for the instance to be ready first
# Wait for the instance to be ready first
wait_server_ready(env, region_config, server, elastic_ip)
end
@app.call(env)
end

def wait_server_ready(env, region_config, server, elastic_ip)
env[:metrics]["instance_ready_time"] = Util::Timer.time do
tries = region_config.instance_ready_timeout / 2

Expand Down Expand Up @@ -172,8 +183,72 @@ def call(env)

# Terminate the instance if we were interrupted
terminate(env) if env[:interrupted]
end

@app.call(env)
# 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.Monitoring.Enabled' => config.monitoring,
'LaunchSpecification.Placement.AvailabilityZone' => config.availability_zone,
# 'LaunchSpecification.EbsOptimized' => config.ebs_optimized,
'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}")
env[:ui].info("Status: #{spot_req["fault"]["message"]}")
status_code = spot_req["fault"]["code"] # fog uses "fault" instead of "status"
while true
sleep 5 # TODO make it a param
break 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["fault"]["code"]
env[:ui].info("Status: #{spot_req["fault"]["message"]}")
status_code = spot_req["fault"]["code"]
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
@logger.error("Spot request #{spot_state} #{status_code}, aborting")
break; # :(
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)
# tries to return a server
spot_req["instanceId"] ? env[:aws_compute].servers.get(spot_req["instanceId"]) : nil
end

def recover(env)
Expand Down
32 changes: 32 additions & 0 deletions lib/vagrant-aws/config.rb
Expand Up @@ -138,6 +138,21 @@ class Config < Vagrant.plugin("2", :config)
# @return [Boolean]
attr_accessor :associate_public_ip

# 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

def initialize(region_specific=false)
@access_key_id = UNSET_VALUE
@ami = UNSET_VALUE
Expand All @@ -164,6 +179,9 @@ def initialize(region_specific=false)
@monitoring = UNSET_VALUE
@ebs_optimized = UNSET_VALUE
@associate_public_ip = 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 @@ -287,6 +305,9 @@ def finalize!
# By default we don't use an IAM profile
@use_iam_profile = false if @use_iam_profile == UNSET_VALUE

# No monitoring by default
@monitoring = false if @monitoring == UNSET_VALUE

# User Data is nil by default
@user_data = nil if @user_data == UNSET_VALUE

Expand All @@ -305,6 +326,15 @@ def finalize!
# default false
@associate_public_ip = false if @associate_public_ip == 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

# Compile our region specific configurations only within
# NON-REGION-SPECIFIC configurations.
if !@__region_specific
Expand Down Expand Up @@ -352,6 +382,8 @@ def validate(machine)
end

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

errors << I18n.t("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 @@ -4,6 +4,8 @@ en:
The machine is already %{status}.
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 @@ -47,6 +49,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 @@ -38,6 +38,9 @@
its("monitoring") { should == false }
its("ebs_optimized") { should == false }
its("associate_public_ip") { should == false }
its("spot_instance") { should be_false }
its("spot_max_price") { should be_nil }
its("spot_valid_until") { should be_nil }
end

describe "overriding defaults" do
Expand Down

0 comments on commit 0bb9f27

Please sign in to comment.