# # 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", # :origin => "my-bucket.s3.amazonaws.com", # :cnames => ["x1.my-awesome-site.net", "x1.my-awesome-site.net"] # :comment => "My comments", # :last_modified_time => Wed Sep 10 17:00:04 UTC 2008 }, ..., {...} ] # # distibution = list.first # # info = acf.get_distribution(distibution[:aws_id]) #=> # {:enabled => true, # :caller_reference => "200809102100536497863003", # :e_tag => "E39OHHU1ON65SI", # :status => "Deployed", # :domain_name => "d3dxv71tbbt6cd.6hops.net", # :cnames => ["web1.my-awesome-site.net", "web2.my-awesome-site.net"] # :aws_id => "E2REJM3VUN5RSI", # :comment => "Woo-Hoo!", # :origin => "my-bucket.s3.amazonaws.com", # :last_modified_time => Wed Sep 10 17:00:54 UTC 2008 } # # 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!", # :origin => "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 = "2008-06-30" 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: # * :server: CloudFront service host, default: DEFAULT_HOST # * :port: CloudFront service port, default: DEFAULT_PORT # * :protocol: 'http' or 'https', default: DEFAULT_PROTOCOL # * :multi_thread: true=HTTP connection per thread, false=per process # * :logger: for log messages, default: RAILS_DEFAULT_LOGGER else STDOUT # * :cache: true/false: caching for list_distributions method, default: false. # # acf = RightAws::AcfInterface.new('1E3GDYEOGFJPIT7XXXXXX','hgTHt68JY07JKUY08ftHYtERkjgtfERn57XXXXXX', # {:multi_thread => true, :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 }, 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, body=nil, headers={}) # :nodoc: 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[:default_service]}/#{API_VERSION}/#{path}" request = "Net::HTTP::#{method.capitalize}".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: thread = @params[:multi_thread] ? Thread.current : Thread.main thread[:acf_connection] ||= Rightscale::HttpConnection.new(:exception => RightAws::AwsError, :logger => @logger) request_info_impl(thread[:acf_connection], @@bench, request, parser, &block) end #----------------------------------------------------------------- # Helpers: #----------------------------------------------------------------- def self.escape(text) # :nodoc: REXML::Text::normalize(text) end def self.unescape(text) # :nodoc: REXML::Text::unnormalize(text) end def xmlns # :nodoc: %Q{"http://#{@params[:server]}/doc/#{API_VERSION}/"} end 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 #----------------------------------------------------------------- # API Calls: #----------------------------------------------------------------- # List distributions. # Returns an array of distributions or RightAws::AwsError exception. # # acf.list_distributions #=> # [{:status => "Deployed", # :domain_name => "d74zzrxmpmygb.6hops.net", # :aws_id => "E4U91HCJHGXVC", # :cnames => ["web1.my-awesome-site.net", "web2.my-awesome-site.net"] # :origin => "my-bucket.s3.amazonaws.com", # :comment => "My comments", # :last_modified_time => Wed Sep 10 17:00:04 UTC 2008 }, ..., {...} ] # def list_distributions request_hash = generate_request('GET', 'distribution') request_cache_or_info :list_distributions, request_hash, AcfDistributionListParser, @@bench end # Create a new distribution. # Returns the just created distribution or RightAws::AwsError exception. # # acf.create_distribution('bucket-for-k-dzreyev.s3.amazonaws.com', 'Woo-Hoo!', true, ['web1.my-awesome-site.net'] ) #=> # {:comment => "Woo-Hoo!", # :enabled => true, # :location => "https://cloudfront.amazonaws.com/2008-06-30/distribution/E2REJM3VUN5RSI", # :status => "InProgress", # :aws_id => "E2REJM3VUN5RSI", # :domain_name => "d3dxv71tbbt6cd.6hops.net", # :origin => "my-bucket.s3.amazonaws.com", # :cnames => ["web1.my-awesome-site.net"] # :last_modified_time => Wed Sep 10 17:00:54 UTC 2008, # :caller_reference => "200809102100536497863003"} # def create_distribution(origin, comment='', enabled=true, cnames=[], caller_reference=nil) # join CNAMES cnames_str = '' unless cnames.blank? cnames.to_a.each { |cname| cnames_str += "\n #{cname}" } end # reference caller_reference ||= generate_call_reference body = <<-EOXML #{origin} #{caller_reference} #{cnames_str.lstrip} #{AcfInterface::escape(comment.to_s)} #{enabled} EOXML request_hash = generate_request('POST', 'distribution', body.strip) merge_headers(request_info(request_hash, AcfDistributionParser.new)) end # Get a distribution's information. # Returns a distribution's information or RightAws::AwsError exception. # # acf.get_distribution('E2REJM3VUN5RSI') #=> # {:enabled => true, # :caller_reference => "200809102100536497863003", # :e_tag => "E39OHHU1ON65SI", # :status => "Deployed", # :domain_name => "d3dxv71tbbt6cd.6hops.net", # :cnames => ["web1.my-awesome-site.net", "web2.my-awesome-site.net"] # :aws_id => "E2REJM3VUN5RSI", # :comment => "Woo-Hoo!", # :origin => "my-bucket.s3.amazonaws.com", # :last_modified_time => Wed Sep 10 17:00:54 UTC 2008 } # def get_distribution(aws_id) request_hash = generate_request('GET', "distribution/#{aws_id}") merge_headers(request_info(request_hash, AcfDistributionParser.new)) end # Get a distribution's configuration. # Returns a distribution's configuration or RightAws::AwsError exception. # # 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!", # :origin => "my-bucket.s3.amazonaws.com"} # def get_distribution_config(aws_id) request_hash = generate_request('GET', "distribution/#{aws_id}/config") merge_headers(request_info(request_hash, AcfDistributionConfigParser.new)) end # Set a distribution's configuration # (the :origin and the :caller_reference cannot be changed). # 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!", # :origin => "my-bucket.s3.amazonaws.com"} # config[:comment] = 'Olah-lah!' # config[:enabled] = false # acf.set_distribution_config('E2REJM3VUN5RSI', config) #=> true # def set_distribution_config(aws_id, config) # join CNAMES cnames_str = '' unless config[:cnames].blank? config[:cnames].to_a.each { |cname| cnames_str += "\n #{cname}" } end # format request's XML body body = <<-EOXML #{config[:origin]} #{config[:caller_reference]} #{cnames_str.lstrip} #{AcfInterface::escape(config[:comment].to_s)} #{config[:enabled]} EOXML request_hash = generate_request('PUT', "distribution/#{aws_id}/config", body.strip, 'If-Match' => config[:e_tag]) request_info(request_hash, RightHttp2xxParser.new) 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) request_hash = generate_request('DELETE', "distribution/#{aws_id}", nil, 'If-Match' => e_tag) request_info(request_hash, RightHttp2xxParser.new) end #----------------------------------------------------------------- # PARSERS: #----------------------------------------------------------------- class AcfDistributionListParser < RightAWSParser # :nodoc: def reset @result = [] end def tagstart(name, attributes) @distribution = { :cnames => [] } if name == 'DistributionSummary' end def tagend(name) case name when 'Id' then @distribution[:aws_id] = @text when 'Status' then @distribution[:status] = @text when 'LastModifiedTime' then @distribution[:last_modified_time] = Time.parse(@text) when 'DomainName' then @distribution[:domain_name] = @text when 'Origin' then @distribution[:origin] = @text when 'Comment' then @distribution[:comment] = AcfInterface::unescape(@text) when 'CNAME' then @distribution[:cnames] << @text when 'DistributionSummary' then @result << @distribution end end end class AcfDistributionParser < RightAWSParser # :nodoc: def reset @result = { :cnames => [] } end def tagend(name) case name when 'Id' then @result[:aws_id] = @text when 'Status' then @result[:status] = @text when 'LastModifiedTime' then @result[:last_modified_time] = Time.parse(@text) when 'DomainName' then @result[:domain_name] = @text when 'Origin' then @result[:origin] = @text when 'CallerReference' then @result[:caller_reference] = @text when 'Comment' then @result[:comment] = AcfInterface::unescape(@text) when 'Enabled' then @result[:enabled] = @text == 'true' ? true : false when 'CNAME' then @result[:cnames] << @text end end end class AcfDistributionConfigParser < RightAWSParser # :nodoc: def reset @result = { :cnames => [] } end def tagend(name) case name when 'Origin' then @result[:origin] = @text when 'CallerReference' then @result[:caller_reference] = @text when 'Comment' then @result[:comment] = AcfInterface::unescape(@text) when 'Enabled' then @result[:enabled] = @text == 'true' ? true : false when 'CNAME' then @result[:cnames] << @text end end end end end