From 8f675c16210ff19aa619b52a01a120689a2aeff0 Mon Sep 17 00:00:00 2001 From: TANABE Ken-ichi Date: Tue, 29 Jul 2014 17:11:51 +0900 Subject: [PATCH] Add spot instance support rebased on current 0.5.0 with original work at https://github.com/mitchellh/vagrant-aws/issues/32 --- lib/vagrant-aws/action/run_instance.rb | 73 +++++++++++++++++++++++++- lib/vagrant-aws/config.rb | 29 ++++++++++ locales/en.yml | 4 ++ spec/vagrant-aws/config_spec.rb | 3 ++ 4 files changed, 108 insertions(+), 1 deletion(-) diff --git a/lib/vagrant-aws/action/run_instance.rb b/lib/vagrant-aws/action/run_instance.rb index 0eede22b..1370f10b 100644 --- a/lib/vagrant-aws/action/run_instance.rb +++ b/lib/vagrant-aws/action/run_instance.rb @@ -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. @@ -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) diff --git a/lib/vagrant-aws/config.rb b/lib/vagrant-aws/config.rb index 52200942..85ea341a 100644 --- a/lib/vagrant-aws/config.rb +++ b/lib/vagrant-aws/config.rb @@ -108,6 +108,21 @@ class Config < Vagrant.plugin("2", :config) # @return [Array] 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] @@ -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 @@ -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 @@ -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 } diff --git a/locales/en.yml b/locales/en.yml index ecd498a5..ef0f8ec6 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -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. @@ -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: |- diff --git a/spec/vagrant-aws/config_spec.rb b/spec/vagrant-aws/config_spec.rb index 531fe62d..0406cc5f 100644 --- a/spec/vagrant-aws/config_spec.rb +++ b/spec/vagrant-aws/config_spec.rb @@ -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 }