Skip to content

Commit 4f7e296

Browse files
authored
Merge pull request #31 from CU-CloudCollab/spot-reservation
added spot market functions to the library
2 parents cae231a + 24f5a12 commit 4f7e296

8 files changed

Lines changed: 637 additions & 1 deletion

File tree

cucloud.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Gem::Specification.new do |spec|
2626

2727
spec.add_dependency 'aws-sdk', '~> 2'
2828
spec.add_dependency 'uuid', '~> 2.3'
29+
spec.add_dependency 'descriptive_statistics', '~> 2.5'
2930

3031
spec.add_development_dependency 'bundler', '~> 1.11'
3132
spec.add_development_dependency 'coveralls'

lib/cucloud.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'aws-sdk'
2+
require 'descriptive_statistics'
23

34
# Main Cucloud Module namespace and defaults
45
module Cucloud
@@ -15,6 +16,7 @@ module Cucloud
1516
require 'cucloud/rds_utils'
1617
require 'cucloud/lambda_utils'
1718
require 'cucloud/cfn_utils'
19+
require 'cucloud/utilities'
1820

1921
# This is the default region API calls are made against
2022
DEFAULT_REGION = 'us-east-1'.freeze

lib/cucloud/ec2_utils.rb

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ class Ec2Utils
1111
WAITER_MAX_ATTEMPS = 240
1212
# Delay between calls used by waiter to check status
1313
WAITER_DELAY = 15
14+
# Two weeks in hours
15+
TWO_WEEKS = 336
16+
# Default OS to use
17+
DEFAULT_OS = 'Linux/UNIX'.freeze
1418

1519
def initialize(ec2_client = Aws::EC2::Client.new, ssm_utils = Cucloud::SSMUtils.new)
1620
@ec2 = ec2_client
@@ -253,5 +257,64 @@ def find_ebs_snapshots(options = {})
253257
end
254258
found_snapshots
255259
end
260+
261+
# Get a recommendation for a spot bid request. Given an instance type and
262+
# OS we will grab data from a period specified, default is from two weeks to now,
263+
# and calculate recommendations for the AZs in the current region
264+
# @param instance_type [String] Insrance type to get bid for
265+
# @param os [String] OS you whish to run, default linux
266+
# @param num_hours [Integer] How many hours to look back, default two weeks
267+
# @return [Hash] Reccomendations by region, empty if no viable recommendations
268+
def best_spot_bid_price(instance_type, os = DEFAULT_OS, num_hours = TWO_WEEKS)
269+
price_history_by_az = {}
270+
recommendations = {}
271+
272+
options = {
273+
end_time: Time.now.utc,
274+
instance_types: [
275+
instance_type
276+
],
277+
product_descriptions: [
278+
os
279+
],
280+
start_time: (Time.now - num_hours * 60).utc
281+
}
282+
283+
loop do
284+
price_history = @ec2.describe_spot_price_history(options)
285+
price_history.spot_price_history.each do |price|
286+
price_history_by_az[price.availability_zone] = [] unless price_history_by_az[price.availability_zone]
287+
price_history_by_az[price.availability_zone].push(price.spot_price.to_f)
288+
end
289+
290+
break if price_history.next_token.nil? || price_history.next_token.empty?
291+
options[:next_token] = price_history.next_token
292+
end
293+
294+
price_history_by_az.each do |key, data|
295+
stats = data.descriptive_statistics
296+
next unless stats[:number] > 30
297+
confidence_interval = Cucloud::Utilities.confidence_interval_99(
298+
stats[:mean],
299+
stats[:standard_deviation],
300+
stats[:number]
301+
)
302+
recommendations[key] = confidence_interval[1]
303+
end
304+
305+
recommendations
306+
end
307+
308+
# Make spot instance request
309+
# @param options [Hash] Options to provide to the API
310+
# see http://docs.aws.amazon.com/sdkforruby/api/Aws/EC2/Client.html#request_spot_instances-instance_method
311+
# @return [Hash] Description of the spot request
312+
def make_spot_instance_request(options)
313+
spot_requests = @ec2.request_spot_instances(options)
314+
request_ids = [spot_requests.spot_instance_requests[0].spot_instance_request_id]
315+
316+
@ec2.wait_until(:spot_instance_request_fulfilled, spot_instance_request_ids: request_ids)
317+
@ec2.describe_spot_instance_requests(spot_instance_request_ids: request_ids)
318+
end
256319
end
257320
end

lib/cucloud/utilities.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
module Cucloud
2+
# Utilities class - for basice shared utilities
3+
class Utilities
4+
# Z Score to calculate 99% confidence interval
5+
Z_SCORE_99 = 2.576
6+
# Z Score to calculate 99% confidence interval
7+
Z_SCORE_96 = 1.96
8+
9+
# Calculate 99% confidence interval
10+
# @param mean [Float] sample mean
11+
# @param stdev [Float] sample standard deviation
12+
# @param sample_size [Integer] sample size
13+
# @return [Array] Two element array representing the computed confidence interval
14+
def self.confidence_interval_99(mean, stdev, sample_size)
15+
confidence_interval(mean, stdev, sample_size, Z_SCORE_99)
16+
end
17+
18+
# Calculate 95% confidence interval
19+
# @param mean [Float] sample mean
20+
# @param stdev [Float] sample standard deviation
21+
# @param sample_size [Integer] sample size
22+
# @return [Array] Two element array representing the computed confidence interval
23+
def self.confidence_interval_95(mean, stdev, sample_size)
24+
confidence_interval(mean, stdev, sample_size, Z_SCORE_96)
25+
end
26+
27+
private_class_method
28+
29+
# Calculate confidence interval for given zscore
30+
# @param mean [Float] sample mean
31+
# @param stdev [Float] sample standard deviation
32+
# @param sample_size [Integer] sample size
33+
# @return [Array] Two element array representing the computed confidence interval
34+
def self.confidence_interval(mean, stdev, sample_size, zscore)
35+
delta = zscore * stdev / Math.sqrt(sample_size - 1)
36+
[mean - delta, mean + delta]
37+
end
38+
end
39+
end

lib/cucloud/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module Cucloud
22
# Disable mutable constant warning - freezing this oddly breaks bundler
33
# rubocop:disable Style/MutableConstant
4-
VERSION = '0.7.5'
4+
VERSION = '0.7.6'
55
end

spec/ec2_utils_spec.rb

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,88 @@
412412
expect(ec_util.get_instance_name('i-2')).to be_nil
413413
end
414414

415+
describe 'make_spot_instance_request' do
416+
it 'should make a spot instnace request' do
417+
ec2_client.stub_responses(
418+
:describe_spot_instance_requests,
419+
spot_instance_requests: [{
420+
status: {
421+
code: 'fulfilled'
422+
}
423+
}]
424+
)
425+
426+
ec2_client.stub_responses(
427+
:request_spot_instances,
428+
spot_instance_requests: [{
429+
spot_instance_request_id: 'sir-1212121'
430+
}]
431+
)
432+
433+
options = {
434+
instance_count: 1,
435+
launch_specification: {
436+
437+
image_id: 'ami-275ffe31',
438+
instance_type: 'm3.medium'
439+
},
440+
spot_price: '0.016',
441+
type: 'one-time'
442+
}
443+
444+
expect { ec_util.make_spot_instance_request(options) }.not_to raise_error
445+
end
446+
end
447+
448+
describe 'best_spot_bid_price' do
449+
it 'should return a list of bid recommendations' do
450+
ec2_client.stub_responses(
451+
:describe_spot_price_history,
452+
JSON.parse(File.read(File.join(File.dirname(__FILE__), '/fixtures/bid_history.json')), symbolize_names: true)
453+
)
454+
455+
bid_prices = ec_util.best_spot_bid_price('m3.medium')
456+
expect(bid_prices).to match_array([
457+
['us-west-1a', 0.08244842404174188],
458+
['us-west-1c', 0.07917263606261282]
459+
])
460+
end
461+
462+
it 'should return a list of bid recommendations (paginate)' do
463+
ec2_client.stub_responses(
464+
:describe_spot_price_history,
465+
[
466+
{
467+
next_token: '1123123',
468+
spot_price_history: [
469+
{
470+
availability_zone: 'us-west-1a',
471+
instance_type: 'm3.medium',
472+
product_description: 'Linux/UNIX (Amazon VPC)',
473+
spot_price: '0.080000',
474+
timestamp: Time.parse('2014-01-06T04:32:53.000Z')
475+
}
476+
]
477+
},
478+
{
479+
next_token: '',
480+
spot_price_history: [
481+
{
482+
availability_zone: 'us-west-1a',
483+
instance_type: 'm3.medium',
484+
product_description: 'Linux/UNIX (Amazon VPC)',
485+
spot_price: '0.080000',
486+
timestamp: Time.parse('2014-01-06T04:32:53.000Z')
487+
}
488+
]
489+
}
490+
]
491+
)
492+
493+
expect { ec_util.best_spot_bid_price('m3.medium') }.not_to raise_error
494+
end
495+
end
496+
415497
describe '#instances_to_patch_by_tag' do
416498
it 'should run without an error with no valid targets' do
417499
expect { ec_util.instances_to_patch_by_tag }.not_to raise_error

0 commit comments

Comments
 (0)