# # 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 = "2009-04-02" 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 # * :multi_thread: true=HTTP connection per thread, false=per process # * :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.blank? } unless params.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}".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 self.escape(text) # :nodoc: REXML::Text::normalize(text) end def self.unescape(text) # :nodoc: REXML::Text::unnormalize(text) 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 def config_to_xml(config) # :nodoc: cnames = '' unless config[:cnames].blank? config[:cnames].to_a.each { |cname| cnames += " #{cname}\n" } end # logging logging = '' unless config[:logging].blank? logging = " \n" + " #{config[:logging][:bucket]}\n" + " #{config[:logging][:prefix]}\n" + " \n" end # xml "\n" + "\n" + " #{config[:origin]}\n" + " #{config[:caller_reference]}\n" + " #{AcfInterface::escape(config[:comment].to_s)}\n" + " #{config[:enabled]}\n" + cnames + logging + "" end #----------------------------------------------------------------- # API Calls: #----------------------------------------------------------------- # List all 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 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", # :logging=>{}, # :origin=>"my-bucket.s3.amazonaws.com", # :domain_name=>"d1s5gmdtmafnre.6hops.net", # :comment=>"ONE LINE OF COMMENT", # :last_modified_time=>Wed Oct 22 19:31:23 UTC 2008, # :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 # false # 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].blank? end last_response end # Create a new distribution. # Returns the just created distribution or RightAws::AwsError exception. # # acf.create_distribution('my-bucket.s3.amazonaws.com', 'Woo-Hoo!', true, ['web1.my-awesome-site.net'], # { :prefix=>"log/", :bucket=>"my-logs.s3.amazonaws.com" } ) #=> # {: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"], # :logging => { :prefix => "log/", # :bucket => "my-logs.s3.amazonaws.com"}, # :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, logging={}) config = { :origin => origin, :comment => comment, :enabled => enabled, :cnames => cnames.to_a, :caller_reference => caller_reference } config[:logging] = logging unless logging.blank? create_distribution_by_config(config) end def create_distribution_by_config(config) config[:caller_reference] ||= generate_call_reference link = generate_request('POST', 'distribution', {}, config_to_xml(config)) merge_headers(request_info(link, AcfDistributionListParser.new(:logger => @logger))[:distributions].first) 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) 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') #=> # {: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) 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 # (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) link = generate_request('PUT', "distribution/#{aws_id}/config", {}, 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) if name == 'DistributionSummary' || name == 'Distribution' || (name == 'DistributionConfig' && @xmlpath.blank?) @distribution = { :cnames => [], :logging => {} } 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' ? true : false 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 'CallerReference' then @distribution[:caller_reference] = @text when 'CNAME' then @distribution[:cnames] << @text when 'Enabled' then @distribution[:enabled] = @text == 'true' ? true : false when 'Bucket' then @distribution[:logging][:bucket] = @text when 'Prefix' then @distribution[:logging][:prefix] = @text end if name == 'DistributionSummary' || name == 'Distribution' || (name == 'DistributionConfig' && @xmlpath.blank?) @result[:distributions] << @distribution end end end end end