#
# Copyright (c) 2008 RightScale Inc
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
module RightAws
# = RightAws::AcfInterface -- RightScale Amazon's CloudFront interface
# The AcfInterface class provides a complete interface to Amazon's
# CloudFront service.
#
# For explanations of the semantics of each call, please refer to
# Amazon's documentation at
# http://developer.amazonwebservices.com/connect/kbcategory.jspa?categoryID=211
#
# Example:
#
# acf = RightAws::AcfInterface.new('1E3GDYEOGFJPIT7XXXXXX','hgTHt68JY07JKUY08ftHYtERkjgtfERn57XXXXXX')
#
# list = acf.list_distributions #=>
# [{:status => "Deployed",
# :domain_name => "d74zzrxmpmygb.6hops.net",
# :aws_id => "E4U91HCJHGXVC",
# :s3_origin => {:dns_name=>"bucket-for-konstantin-00.s3.amazonaws.com"},
# :cnames => ["x1.my-awesome-site.net", "x1.my-awesome-site.net"]
# :comment => "My comments",
# :last_modified_time => "2008-09-10T17:00:04.000Z" }, ..., {...} ]
#
# distibution = list.first
#
# info = acf.get_distribution(distibution[:aws_id]) #=>
# {:last_modified_time=>"2010-05-19T18:54:38.242Z",
# :status=>"Deployed",
# :domain_name=>"dpzl38cuix402.cloudfront.net",
# :caller_reference=>"201005181943052207677116",
# :e_tag=>"EJSXFGM5JL8ER",
# :s3_origin=>
# {:dns_name=>"bucket-for-konstantin-eu.s3.amazonaws.com",
# :origin_access_identity=>
# "origin-access-identity/cloudfront/E3JPJZ80ZBX24G"},
# :aws_id=>"E5P8HQ3ZAZIXD",
# :enabled=>false}
#
# config = acf.get_distribution_config(distibution[:aws_id]) #=>
# {:enabled => true,
# :caller_reference => "200809102100536497863003",
# :e_tag => "E39OHHU1ON65SI",
# :cnames => ["web1.my-awesome-site.net", "web2.my-awesome-site.net"]
# :comment => "Woo-Hoo!",
# :s3_origin => {:dns_name => "my-bucket.s3.amazonaws.com"}}
#
# config[:comment] = 'Olah-lah!'
# config[:enabled] = false
# config[:cnames] << "web3.my-awesome-site.net"
#
# acf.set_distribution_config(distibution[:aws_id], config) #=> true
#
class AcfInterface < RightAwsBase
include RightAwsBaseInterface
API_VERSION = "2010-11-01"
DEFAULT_HOST = 'cloudfront.amazonaws.com'
DEFAULT_PORT = 443
DEFAULT_PROTOCOL = 'https'
DEFAULT_PATH = '/'
@@bench = AwsBenchmarkingBlock.new
def self.bench_xml
@@bench.xml
end
def self.bench_service
@@bench.service
end
# Create a new handle to a CloudFront account. All handles share the same per process or per thread
# HTTP connection to CloudFront. Each handle is for a specific account. The params have the
# following options:
# * :endpoint_url a fully qualified url to Amazon API endpoint (this overwrites: :server, :port, :service, :protocol). Example: 'https://cloudfront.amazonaws.com'
# * :server: CloudFront service host, default: DEFAULT_HOST
# * :port: CloudFront service port, default: DEFAULT_PORT
# * :protocol: 'http' or 'https', default: DEFAULT_PROTOCOL
# * :logger: for log messages, default: RAILS_DEFAULT_LOGGER else STDOUT
#
# acf = RightAws::AcfInterface.new('1E3GDYEOGFJPIT7XXXXXX','hgTHt68JY07JKUY08ftHYtERkjgtfERn57XXXXXX',
# {:logger => Logger.new('/tmp/x.log')}) #=> #
#
def initialize(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
init({ :name => 'ACF',
:default_host => ENV['ACF_URL'] ? URI.parse(ENV['ACF_URL']).host : DEFAULT_HOST,
:default_port => ENV['ACF_URL'] ? URI.parse(ENV['ACF_URL']).port : DEFAULT_PORT,
:default_service => ENV['ACF_URL'] ? URI.parse(ENV['ACF_URL']).path : DEFAULT_PATH,
:default_protocol => ENV['ACF_URL'] ? URI.parse(ENV['ACF_URL']).scheme : DEFAULT_PROTOCOL,
:default_api_version => ENV['ACF_API_VERSION'] || API_VERSION },
aws_access_key_id || ENV['AWS_ACCESS_KEY_ID'],
aws_secret_access_key || ENV['AWS_SECRET_ACCESS_KEY'],
params)
end
#-----------------------------------------------------------------
# Requests
#-----------------------------------------------------------------
# Generates request hash for REST API.
def generate_request(method, path, params={}, body=nil, headers={}) # :nodoc:
# Params
params.delete_if{ |key, val| val.right_blank? }
unless params.right_blank?
path += "?" + params.to_a.collect{ |key,val| "#{AwsUtils::amz_escape(key)}=#{AwsUtils::amz_escape(val.to_s)}" }.join("&")
end
# Headers
headers['content-type'] ||= 'text/xml' if body
headers['date'] = Time.now.httpdate
# Auth
signature = AwsUtils::sign(@aws_secret_access_key, headers['date'])
headers['Authorization'] = "AWS #{@aws_access_key_id}:#{signature}"
# Request
path = "#{@params[:service]}#{@params[:api_version]}/#{path}"
request = "Net::HTTP::#{method.capitalize}".right_constantize.new(path)
request.body = body if body
# Set request headers
headers.each { |key, value| request[key.to_s] = value }
# prepare output hash
{ :request => request,
:server => @params[:server],
:port => @params[:port],
:protocol => @params[:protocol] }
end
# Sends request to Amazon and parses the response.
# Raises AwsError if any banana happened.
def request_info(request, parser, &block) # :nodoc:
request_info_impl(:acf_connection, @@bench, request, parser, &block)
end
#-----------------------------------------------------------------
# Helpers:
#-----------------------------------------------------------------
def generate_call_reference # :nodoc:
result = Time.now.strftime('%Y%m%d%H%M%S')
10.times{ result << rand(10).to_s }
result
end
def merge_headers(hash) # :nodoc:
hash[:location] = @last_response['Location'] if @last_response['Location']
hash[:e_tag] = @last_response['ETag'] if @last_response['ETag']
hash
end
def distribution_config_to_xml(config, xml_wrapper='DistributionConfig') # :nodoc:
cnames = logging = trusted_signers = s3_origin = custom_origin = default_root_object = ''
# CNAMES
unless config[:cnames].right_blank?
Array(config[:cnames]).each { |cname| cnames += " #{cname}\n" }
end
# Logging
unless config[:logging].right_blank?
logging = " \n" +
" #{config[:logging][:bucket]}\n" +
" #{config[:logging][:prefix]}\n" +
" \n"
end
unless config[:required_protocols].right_blank?
required_protocols = " \n" +
" #{config[:required_protocols]}\n" +
" \n"
else required_protocols = ""
end
# Default Root Object
unless config[:default_root_object].right_blank?
default_root_object = " #{config[:default_root_object]}\n" unless config[:default_root_object].right_blank?
end
# Trusted Signers
unless config[:trusted_signers].right_blank?
trusted_signers = " \n"
Array(config[:trusted_signers]).each do |trusted_signer|
trusted_signers += if trusted_signer.to_s[/self/i]
" \n"
else
" #{trusted_signer}\n"
end
end
trusted_signers += " \n"
end
# S3Origin
unless config[:s3_origin].right_blank?
origin_access_identity = ''
# Origin Access Identity
unless config[:s3_origin][:origin_access_identity].right_blank?
origin_access_identity = config[:s3_origin][:origin_access_identity]
unless origin_access_identity[%r{^origin-access-identity}]
origin_access_identity = "origin-access-identity/cloudfront/#{origin_access_identity}"
end
origin_access_identity = " #{origin_access_identity}\n"
end
s3_origin = " \n" +
" #{config[:s3_origin][:dns_name]}\n" +
"#{origin_access_identity}" +
" \n"
end
# Custom Origin
unless config[:custom_origin].right_blank?
http_port = https_port = origin_protocol_policy = ''
http_port = " #{config[:custom_origin][:http_port]}\n" unless config[:custom_origin][:http_port].right_blank?
https_port = " #{config[:custom_origin][:https_port]}" unless config[:custom_origin][:https_port].right_blank?
origin_protocol_policy = " #{config[:custom_origin][:origin_protocol_policy]}\n" unless config[:custom_origin][:origin_protocol_policy].right_blank?
custom_origin = " \n" +
" #{config[:custom_origin][:dns_name]}\n" +
"#{http_port}" +
"#{https_port}" +
"#{origin_protocol_policy}" +
" \n"
end
# XML
"\n" +
"<#{xml_wrapper} xmlns=\"http://#{@params[:server]}/doc/#{API_VERSION}/\">\n" +
" #{config[:caller_reference]}\n" +
" #{AwsUtils::xml_escape(config[:comment].to_s)}\n" +
" #{config[:enabled]}\n" +
s3_origin +
custom_origin +
default_root_object +
cnames +
logging +
required_protocols +
trusted_signers +
"#{xml_wrapper}>"
end
#-----------------------------------------------------------------
# API Calls:
#-----------------------------------------------------------------
# List all distributions.
# Returns an array of distributions or RightAws::AwsError exception.
#
# acf.list_distributions #=>
# [{:status=>"Deployed",
# :domain_name=>"dgmde.6os.net",
# :comment=>"ONE LINE OF COMMENT",
# :last_modified_time=>"2009-06-16T16:10:02.210Z",
# :s3_origin=>{:dns_name=>"example.s3.amazonaws.com"},
# :aws_id=>"12Q05OOMFN7SYL",
# :enabled=>true}, ... ]
#
def list_distributions
result = []
incrementally_list_distributions do |response|
result += response[:distributions]
true
end
result
end
# Incrementally list distributions.
#
# Optional params: +:marker+ and +:max_items+.
#
# # get first distribution
# incrementally_list_distributions(:max_items => 1) #=>
# {:distributions=>
# [{:status=>"Deployed",
# :aws_id=>"E2Q0AOOMFNPSYL",
# :s3_origin=>{:dns_name=>"example.s3.amazonaws.com"},
# :domain_name=>"d1s5gmdtmafnre.6hops.net",
# :comment=>"ONE LINE OF COMMENT",
# :last_modified_time=>"2008-10-22T19:31:23.000Z",
# :enabled=>true,
# :cnames=>[]}],
# :is_truncated=>true,
# :max_items=>1,
# :marker=>"",
# :next_marker=>"E2Q0AOOMFNPSYL"}
#
# # get max 100 distributions (the list will be restricted by a default MaxItems value ==100 )
# incrementally_list_distributions
#
# # list distributions by 10
# incrementally_list_distributions(:max_items => 10) do |response|
# puts response.inspect # a list of 10 distributions
# true # return false if the listing should be broken otherwise use true
# end
#
def incrementally_list_distributions(params={}, &block)
opts = {}
opts['MaxItems'] = params[:max_items] if params[:max_items]
opts['Marker'] = params[:marker] if params[:marker]
last_response = nil
loop do
link = generate_request('GET', 'distribution', opts)
last_response = request_info(link, AcfDistributionListParser.new(:logger => @logger))
opts['Marker'] = last_response[:next_marker]
break unless block && block.call(last_response) && !last_response[:next_marker].right_blank?
end
last_response
end
# Create a new distribution.
# Returns the just created distribution or RightAws::AwsError exception.
#
# # S3 Origin
#
# config = { :comment => "kd: delete me please",
# :s3_origin => { :dns_name => "devs-us-east.s3.amazonaws.com",
# :origin_access_identity => "origin-access-identity/cloudfront/E3JPJZ80ZBX24G"},
# :enabled => true,
# :logging => { :prefix => "kd/log/",
# :bucket => "devs-us-west.s3.amazonaws.com"}}
# acf.create_distribution(config) #=>
# { :status=>"InProgress",
# :enabled=>true,
# :caller_reference=>"201012071910051044304704",
# :logging=>{:prefix=>"kd/log/", :bucket=>"devs-us-west.s3.amazonaws.com"},
# :e_tag=>"ESCTG5WJCFWJK",
# :location=> "https://cloudfront.amazonaws.com/2010-11-01/distribution/E3KUBANZ7N1B2",
# :comment=>"kd: delete me please",
# :domain_name=>"d3stykk6upgs20.cloudfront.net",
# :aws_id=>"E3KUBANZ7N1B2",
# :s3_origin=>
# {:origin_access_identity=>"origin-access-identity/cloudfront/E3JPJZ80ZBX24G",
# :dns_name=>"devs-us-east.s3.amazonaws.com"},
# :last_modified_time=>"2010-12-07T16:10:07.087Z",
# :in_progress_invalidation_batches=>0}
#
# # Custom Origin
#
# custom_config = { :comment => "kd: delete me please",
# :custom_origin => { :dns_name => "custom_origin.my-site.com",
# :http_port => 80,
# :https_port => 443,
# :origin_protocol_policy => 'match-viewer' },
# :enabled => true,
# :logging => { :prefix => "kd/log/",
# :bucket => "my-bucket.s3.amazonaws.com"}} #=>
# { :last_modified_time=>"2010-12-08T14:23:43.522Z",
# :status=>"InProgress",
# :custom_origin=>
# {:http_port=>"80",
# :https_port=>"443",
# :origin_protocol_policy=>"match-viewer",
# :dns_name=>"custom_origin.my-site.com"},
# :enabled=>true,
# :caller_reference=>"201012081723428499167245",
# :in_progress_invalidation_batches=>0,
# :e_tag=>"E1ZCJ8N5E52KO6",
# :location=>
# "https://cloudfront.amazonaws.com/2010-11-01/distribution/EK0AJ4RMNIF2P",
# :logging=>{:prefix=>"kd/log/", :bucket=>"my-bucket.s3.amazonaws.com"},
# :domain_name=>"do36k7s2wxklg.cloudfront.net",
# :comment=>"kd: delete me please",
# :aws_id=>"EK0AJ4RMNIF2P"}
#
def create_distribution(config)
config[:caller_reference] ||= generate_call_reference
link = generate_request('POST', 'distribution', {}, distribution_config_to_xml(config))
merge_headers(request_info(link, AcfDistributionListParser.new(:logger => @logger))[:distributions].first)
end
alias_method :create_distribution_by_config, :create_distribution
# Get a distribution's information.
# Returns a distribution's information or RightAws::AwsError exception.
#
# acf.get_distribution('E2REJM3VUN5RSI') #=>
# {:last_modified_time=>"2010-05-19T18:54:38.242Z",
# :status=>"Deployed",
# :domain_name=>"dpzl38cuix402.cloudfront.net",
# :caller_reference=>"201005181943052207677116",
# :e_tag=>"EJSXFGM5JL8ER",
# :s3_origin=>
# {:dns_name=>"bucket-for-konstantin-eu.s3.amazonaws.com",
# :origin_access_identity=>
# "origin-access-identity/cloudfront/E3JPJZ80ZBX24G"},
# :aws_id=>"E5P8HQ3ZAZIXD",
# :enabled=>false}
#
# acf.get_distribution('E2FNSBHNVVF11E') #=>
# {:e_tag=>"E1Q2DJEPTQOLJD",
# :status=>"InProgress",
# :last_modified_time=>"2010-04-17T17:24:25.000Z",
# :cnames=>["web1.my-awesome-site.net", "web2.my-awesome-site.net"],
# :aws_id=>"E2FNSBHNVVF11E",
# :logging=>{:prefix=>"xlog/", :bucket=>"my-bucket.s3.amazonaws.com"},
# :enabled=>true,
# :active_trusted_signers=>
# [{:aws_account_number=>"120288270000",
# :key_pair_ids=>["APKAJTD5OHNDX0000000", "APKAIK74BJWCL0000000"]},
# {:aws_account_number=>"self"},
# {:aws_account_number=>"648772220000"}],
# :caller_reference=>"201004171154450740700072",
# :domain_name=>"d1f6lpevremt5m.cloudfront.net",
# :s3_origin=>
# {:dns_name=>"bucket-for-konstantin-eu.s3.amazonaws.com",
# :origin_access_identity=>
# "origin-access-identity/cloudfront/E3JPJZ80ZBX24G"},
# :trusted_signers=>["self", "648772220000", "120288270000"]}
#
def get_distribution(aws_id)
link = generate_request('GET', "distribution/#{aws_id}")
merge_headers(request_info(link, AcfDistributionListParser.new(:logger => @logger))[:distributions].first)
end
# Get a distribution's configuration.
# Returns a distribution's configuration or RightAws::AwsError exception.
#
# acf.get_distribution_config('E2REJM3VUN5RSI') #=>
# {:caller_reference=>"201005181943052207677116",
# :e_tag=>"EJSXFGM5JL8ER",
# :s3_origin=>
# {:dns_name=>"bucket-for-konstantin-eu.s3.amazonaws.com",
# :origin_access_identity=>
# "origin-access-identity/cloudfront/E3JPJZ80ZBX24G"},
# :enabled=>false}
#
# acf.get_distribution_config('E2FNSBHNVVF11E') #=>
# {:e_tag=>"E1Q2DJEPTQOLJD",
# :logging=>{:prefix=>"xlog/", :bucket=>"my-bucket.s3.amazonaws.com"},
# :enabled=>true,
# :caller_reference=>"201004171154450740700072",
# :trusted_signers=>["self", "648772220000", "120288270000"],
# :s3_origin=>
# {:dns_name=>"bucket-for-konstantin-eu.s3.amazonaws.com",
# :origin_access_identity=>
# "origin-access-identity/cloudfront/E3JPJZ80ZBX24G"}}
#
def get_distribution_config(aws_id)
link = generate_request('GET', "distribution/#{aws_id}/config")
merge_headers(request_info(link, AcfDistributionListParser.new(:logger => @logger))[:distributions].first)
end
# Set a distribution's configuration
# Returns +true+ on success or RightAws::AwsError exception.
#
# config = acf.get_distribution_config('E2REJM3VUN5RSI') #=>
# {:enabled => true,
# :caller_reference => "200809102100536497863003",
# :e_tag => "E39OHHU1ON65SI",
# :cnames => ["web1.my-awesome-site.net", "web2.my-awesome-site.net"]
# :comment => "Woo-Hoo!",
# :s3_origin => { :dns_name => "my-bucket.s3.amazonaws.com"}}
#
# config[:comment] = 'Olah-lah!'
# config[:enabled] = false
# config[:s3_origin][:origin_access_identity] = "origin-access-identity/cloudfront/E3JPJZ80ZBX24G"
# # or just
# # config[:s3_origin][:origin_access_identity] = "E3JPJZ80ZBX24G"
# config[:trusted_signers] = ['self', '648772220000', '120288270000']
# config[:logging] = { :bucket => 'my-bucket.s3.amazonaws.com', :prefix => 'xlog/' }
#
# acf.set_distribution_config('E2REJM3VUN5RSI', config) #=> true
#
def set_distribution_config(aws_id, config)
link = generate_request('PUT', "distribution/#{aws_id}/config", {}, distribution_config_to_xml(config),
'If-Match' => config[:e_tag])
request_info(link, RightHttp2xxParser.new(:logger => @logger))
end
# Delete a distribution. The enabled distribution cannot be deleted.
# Returns +true+ on success or RightAws::AwsError exception.
#
# acf.delete_distribution('E2REJM3VUN5RSI', 'E39OHHU1ON65SI') #=> true
#
def delete_distribution(aws_id, e_tag)
link = generate_request('DELETE', "distribution/#{aws_id}", {}, nil,
'If-Match' => e_tag)
request_info(link, RightHttp2xxParser.new(:logger => @logger))
end
#-----------------------------------------------------------------
# PARSERS:
#-----------------------------------------------------------------
class AcfDistributionListParser < RightAWSParser # :nodoc:
def reset
@result = { :distributions => [] }
end
def tagstart(name, attributes)
case full_tag_name
when %r{/Signer$}
@active_signer = {}
when %r{(Streaming)?DistributionSummary$},
%r{^(Streaming)?Distribution$},
%r{^(Streaming)?DistributionConfig$}
@distribution = { }
when %r{/S3Origin$} then @distribution[:s3_origin] = {}
when %r{/CustomOrigin$} then @distribution[:custom_origin] = {}
end
end
def tagend(name)
case name
when 'Marker' then @result[:marker] = @text
when 'NextMarker' then @result[:next_marker] = @text
when 'MaxItems' then @result[:max_items] = @text.to_i
when 'IsTruncated' then @result[:is_truncated] = (@text == 'true')
when 'Id' then @distribution[:aws_id] = @text
when 'Status' then @distribution[:status] = @text
when 'LastModifiedTime' then @distribution[:last_modified_time] = @text
when 'DomainName' then @distribution[:domain_name] = @text
when 'Comment' then @distribution[:comment] = AwsUtils::xml_unescape(@text)
when 'CallerReference' then @distribution[:caller_reference] = @text
when 'CNAME' then (@distribution[:cnames] ||= []) << @text
when 'Enabled' then @distribution[:enabled] = (@text == 'true')
when 'Bucket' then (@distribution[:logging] ||= {})[:bucket] = @text
when 'Prefix' then (@distribution[:logging] ||= {})[:prefix] = @text
when 'Protocol' then (@distribution[:required_protocols] ||= {})[:protocol] = @text
when 'InProgressInvalidationBatches' then @distribution[:in_progress_invalidation_batches] = @text.to_i
when 'DefaultRootObject' then @distribution[:default_root_object] = @text
else
case full_tag_name
when %r{/S3Origin/DNSName$} then @distribution[:s3_origin][:dns_name] = @text
when %r{/S3Origin/OriginAccessIdentity$} then @distribution[:s3_origin][:origin_access_identity] = @text
when %r{/CustomOrigin/DNSName$} then @distribution[:custom_origin][:dns_name] = @text
when %r{/CustomOrigin/HTTPPort} then @distribution[:custom_origin][:http_port] = @text
when %r{/CustomOrigin/HTTPSPort$} then @distribution[:custom_origin][:https_port] = @text
when %r{/CustomOrigin/OriginProtocolPolicy$} then @distribution[:custom_origin][:origin_protocol_policy] = @text
when %r{/TrustedSigners/Self$} then (@distribution[:trusted_signers] ||= []) << 'self'
when %r{/TrustedSigners/AwsAccountNumber$} then (@distribution[:trusted_signers] ||= []) << @text
when %r{/Signer/Self$} then @active_signer[:aws_account_number] = 'self'
when %r{/Signer/AwsAccountNumber$} then @active_signer[:aws_account_number] = @text
when %r{/Signer/KeyPairId$} then (@active_signer[:key_pair_ids] ||= []) << @text
when %r{/Signer$} then (@distribution[:active_trusted_signers] ||= []) << @active_signer
when %r{(Streaming)?DistributionSummary$},
%r{^(Streaming)?Distribution$},
%r{^(Streaming)?DistributionConfig$}
@result[:distributions] << @distribution
end
end
end
end
end
end