# # 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 + "" 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