#!/usr/bin/env ruby
require 'cgi'
require 'base64'
require 'net/https'
require 'openssl'
require 'optparse'
require 'rexml/parsers/pullparser'
require 'time'
require 'yaml'
Net::HTTP.version_1_2
class EC2Client
API_VERSION = '2012-12-01'
SIGNATURE_VERSION = 2
SIGNATURE_ALGORITHM = :SHA256
def initialize(accessKeyId, secretAccessKey, endpoint = nil)
@accessKeyId = accessKeyId
@secretAccessKey = secretAccessKey
@endpoint = endpoint
if /\A[^.]+\Z/ =~ @endpoint
@endpoint = "ec2.#{@endpoint}.amazonaws.com"
end
end
def spot_price_history(params = {})
source = query('DescribeSpotPriceHistory', params)
parser = REXML::Parsers::PullParser.new(source)
instance_type = nil
product_description = nil
spot_price = nil
timestamp = nil
availability_zone = nil
while parser.has_next?
event = parser.pull
next if event.event_type != :start_element
case event[0]
when 'instanceType'
instance_type = parser.pull[0]
when 'productDescription'
product_description = parser.pull[0]
when 'spotPrice'
spot_price = parser.pull[0].to_f
when 'timestamp'
timestamp = Time.parse(parser.pull[0]).localtime.iso8601
when 'availabilityZone'
availability_zone = parser.pull[0]
yield [instance_type, product_description, spot_price, timestamp, availability_zone]
end
end
end
private
def query(action, params = {})
params = {
:Action => action,
:Version => API_VERSION,
:Timestamp => Time.now.getutc.strftime('%Y-%m-%dT%H:%M:%SZ'),
:SignatureVersion => SIGNATURE_VERSION,
:SignatureMethod => "Hmac#{SIGNATURE_ALGORITHM}",
:AWSAccessKeyId => @accessKeyId,
}.merge(params)
signature = aws_sign(params)
params[:Signature] = signature
https = Net::HTTP.new(@endpoint, 443)
https.use_ssl = true
https.verify_mode = OpenSSL::SSL::VERIFY_NONE
https.start do |w|
req = Net::HTTP::Post.new('/',
'Host' => @endpoint,
'Content-Type' => 'application/x-www-form-urlencoded'
)
req.set_form_data(params)
res = w.request(req)
res.body
end
end
def aws_sign(params)
params = params.sort_by {|a, b| a.to_s }.map {|k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join('&')
string_to_sign = "POST\n#{@endpoint}\n/\n#{params}"
digest = OpenSSL::HMAC.digest(OpenSSL::Digest.const_get(SIGNATURE_ALGORITHM).new, @secretAccessKey, string_to_sign)
Base64.encode64(digest).gsub("\n", '')
end
end
access_key = nil
secret_key = nil
endpoint = nil
params = {}
ARGV.options do |opt|
begin
opt.on('-k', '--access-key ACCESS_KEY') {|v| access_key = v }
opt.on('-s', '--secret-key SECRET_KEY') {|v| secret_key = v }
opt.on('-r', '--region REGION') {|v| endpoint = v }
opt.on('' , '--start-time TIME') {|v| params['StartTime'] = Time.parse(v).iso8601 }
opt.on('' , '--end-time TIME') {|v| params['EndTime'] = Time.parse(v).iso8601 }
opt.on('-t', '--types TYPE_LIST', Array) {|v| v.each_with_index {|t, i| params["InstanceType.#{i + 1}"] = t } }
opt.on('-d', '--descs DESC_LIST', Array) {|v| v.each_with_index {|t, i| params["ProductDescription.#{i + 1}"] = t } }
opt.on('-z', '--zone AVAILABILITY_ZONE') {|v| params['AvailabilityZone'] = v }
opt.parse!
access_key ||= ENV['AWS_ACCESS_KEY_ID']
secret_key ||= ENV['AWS_SECRET_ACCESS_KEY']
endpoint ||= (ENV['REGION_NAME'] || ENV['EC2_ENDPOINT'])
unless access_key and secret_key and endpoint
puts opt.help
exit 1
end
if (availability_zone = params['AvailabilityZone'])
region = endpoint
if region =~ /\Aec2\.([^.]+)\.amazonaws\.com\Z/
region = $1
end
if availability_zone =~ /\A[a-z]\Z/i
params['AvailabilityZone'] = region + availability_zone
end
end
rescue => e
$stderr.puts e
exit 1
end
end
def to_yaml_style; :inline; end
ec2cli = EC2Client.new(access_key, secret_key, endpoint)
puts '---'
ec2cli.spot_price_history(params) do |row|
puts YAML.dump(row).slice(2..-1)
end
~/work$ ./ec2-describe-spot-price-history -k "..." -s "..." -r us-west-1 -t t1.micro,c1.xlarge -d Linux/UNIX,Windows --start-time=10:00 --end-time=16:00
---
- [c1.xlarge, Linux/UNIX, 0.11, "2013-01-16T13:33:46+09:00", us-west-1c]
- [t1.micro, Windows, 0.007, "2013-01-16T12:49:00+09:00", us-west-1b]
- [c1.xlarge, Windows, 0.24, "2013-01-16T10:30:29+09:00", us-west-1c]
- [c1.xlarge, Windows, 0.24, "2013-01-16T10:30:29+09:00", us-west-1a]
- [t1.micro, Windows, 0.007, "2013-01-16T10:28:50+09:00", us-west-1c]
- [t1.micro, Windows, 0.007, "2013-01-16T10:01:03+09:00", us-west-1a]
- [c1.xlarge, Windows, 0.24, "2013-01-16T03:49:35+09:00", us-west-1b]
- [t1.micro, Linux/UNIX, 0.004, "2013-01-16T02:24:39+09:00", us-west-1b]
- [t1.micro, Linux/UNIX, 0.004, "2013-01-15T19:52:38+09:00", us-west-1c]
- [c1.xlarge, Linux/UNIX, 0.11, "2013-01-15T19:09:04+09:00", us-west-1b]
- [c1.xlarge, Linux/UNIX, 0.11, "2013-01-15T17:14:45+09:00", us-west-1a]
- [t1.micro, Linux/UNIX, 0.004, "2013-01-15T17:13:26+09:00", us-west-1a]
- [c1.xlarge, Linux/UNIX, 0.11, "2013-01-15T13:32:47+09:00", us-west-1c]
- [t1.micro, Windows, 0.007, "2013-01-15T12:48:13+09:00", us-west-1b]
- [c1.xlarge, Windows, 0.24, "2013-01-15T10:30:04+09:00", us-west-1c]
- [c1.xlarge, Windows, 0.24, "2013-01-15T10:30:04+09:00", us-west-1a]
- [t1.micro, Windows, 0.007, "2013-01-15T10:28:27+09:00", us-west-1c]
- [t1.micro, Windows, 0.007, "2013-01-15T10:00:57+09:00", us-west-1a]
~/work$