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

Spot Instances, includes tagging and EBS #430

Open
wants to merge 4 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
5 changes: 5 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 @@ -153,6 +154,10 @@ This provider exposes quite a few provider-specific configuration options:
* `unregister_elb_from_az` - Removes the ELB from the AZ on removal of the last instance if true (default). In non default VPC this has to be false.
* `terminate_on_shutdown` - Indicates whether an instance stops or terminates
when you initiate shutdown from the instance.

* `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. Ignored if `spot_instance` is not true.
* `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:

Expand Down
80 changes: 79 additions & 1 deletion lib/vagrant-aws/action/run_instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,12 @@ 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)
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 All @@ -129,6 +134,10 @@ def call(env)
# Immediately save the ID since it is created at this point.
env[:machine].id = server.id

# Spot Instances don't support tagging arguments on creation
# Retrospectively tag the server to handle this
env[:aws_compute].create_tags(server.id,tags)

# Wait for the instance to be ready first
env[:metrics]["instance_ready_time"] = Util::Timer.time do
tries = region_config.instance_ready_timeout / 2
Expand Down Expand Up @@ -213,6 +222,75 @@ 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,
'LaunchSpecification.BlockDeviceMapping' => config.block_device_mapping,
'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)
server = env[:aws_compute].servers.get(spot_req["instanceId"])
env[:aws_compute].create_tags(server.identity, config.tags)
server
end

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

Expand Down
28 changes: 28 additions & 0 deletions lib/vagrant-aws/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,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 @@ -207,6 +222,9 @@ def initialize(region_specific=false)
@user_data = UNSET_VALUE
@use_iam_profile = UNSET_VALUE
@block_device_mapping = []
@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 @@ -357,6 +375,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 @@ -433,6 +460,7 @@ def validate(machine)
end

errors << I18n.t("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
2 changes: 1 addition & 1 deletion lib/vagrant-aws/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module VagrantPlugins
module AWS
VERSION = '0.7.0'
VERSION = '0.7.0.spot'
end
end
4 changes: 4 additions & 0 deletions 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 @@ -68,6 +70,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
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,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