Ruby版ec2-describe-spot-price-history

#!/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 # EC2Client

# main
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$