From f44715ac93b5c1f93159a56cd17f2bb2bcec3779 Mon Sep 17 00:00:00 2001 From: Jonathan Riley Date: Wed, 9 Dec 2015 13:06:48 +0000 Subject: [PATCH 1/3] Forked original vagrant-aws repo to be able to submit pull request. Added code changes from https://github.com/3vcloud/vagrant-aws , thanks to https://github.com/KariusDx/vagrant-aws This commit allows spot instances to be created; the process in synchronous - waiting for the spot request to be successful before continuing. Cancels spot request after server is up. --- README.md | 5 ++ lib/vagrant-aws/action/run_instance.rb | 80 +++++++++++++++++++++++++- lib/vagrant-aws/config.rb | 28 +++++++++ lib/vagrant-aws/version.rb | 2 +- locales/en.yml | 4 ++ spec/vagrant-aws/config_spec.rb | 3 + 6 files changed, 120 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 62a5e936..0ebbf3d4 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) +* `max_spot_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: diff --git a/lib/vagrant-aws/action/run_instance.rb b/lib/vagrant-aws/action/run_instance.rb index f5dab0b4..3436fc7a 100644 --- a/lib/vagrant-aws/action/run_instance.rb +++ b/lib/vagrant-aws/action/run_instance.rb @@ -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. @@ -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 @@ -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) diff --git a/lib/vagrant-aws/config.rb b/lib/vagrant-aws/config.rb index 86bb12c9..772081dc 100644 --- a/lib/vagrant-aws/config.rb +++ b/lib/vagrant-aws/config.rb @@ -129,6 +129,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] @@ -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 @@ -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 @@ -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 } diff --git a/lib/vagrant-aws/version.rb b/lib/vagrant-aws/version.rb index 2a770d69..b765771e 100644 --- a/lib/vagrant-aws/version.rb +++ b/lib/vagrant-aws/version.rb @@ -1,5 +1,5 @@ module VagrantPlugins module AWS - VERSION = '0.6.1' + VERSION = '0.6.2' end end diff --git a/locales/en.yml b/locales/en.yml index 6536e801..1a2fb9c6 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -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. @@ -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: |- diff --git a/spec/vagrant-aws/config_spec.rb b/spec/vagrant-aws/config_spec.rb index 4703d90d..ef6982aa 100644 --- a/spec/vagrant-aws/config_spec.rb +++ b/spec/vagrant-aws/config_spec.rb @@ -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 } From 7225563c1fba8d0f98fb9c4b59c4f31b721b3484 Mon Sep 17 00:00:00 2001 From: Jonathan Riley Date: Wed, 9 Dec 2015 13:19:02 +0000 Subject: [PATCH 2/3] Added .spot onto version number to avoid plugin install version conflicts when cloning this repo instead of the original. --- lib/vagrant-aws/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/vagrant-aws/version.rb b/lib/vagrant-aws/version.rb index b765771e..1bc5bb85 100644 --- a/lib/vagrant-aws/version.rb +++ b/lib/vagrant-aws/version.rb @@ -1,5 +1,5 @@ module VagrantPlugins module AWS - VERSION = '0.6.2' + VERSION = '0.6.2.spot' end end From 779b4f71d90a92cf61fc3c441bfc082c4b020e58 Mon Sep 17 00:00:00 2001 From: Jon Riley Date: Mon, 18 Jan 2016 19:59:57 +0000 Subject: [PATCH 3/3] max_spot_price > spot_max_price, thx @mkubenka --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ebbf3d4..3bbafc96 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ This provider exposes quite a few provider-specific configuration options: 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) -* `max_spot_price` - Decimal value; state the maximum bid for your spot instance. Ignored if `spot_instance` is not true. +* `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: