mperham-right_aws 1.10.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,112 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require "rake/testtask"
6
+ require 'rcov/rcovtask'
7
+ $: << File.dirname(__FILE__)
8
+ require 'lib/right_aws.rb'
9
+
10
+ testglobs = ["test/ts_right_aws.rb"]
11
+
12
+
13
+ # Suppress Hoe's self-inclusion as a dependency for our Gem. This also keeps
14
+ # Rake & rubyforge out of the dependency list. Users must manually install
15
+ # these gems to run tests, etc.
16
+ # TRB 2/24/09: also do this for the extra_dev_deps array present in newer hoes.
17
+ # Older versions of RubyGems will try to install developer-dependencies as
18
+ # required runtime dependencies. It would be great to take advantage of the extra dev deps, but not
19
+ # at the cost of requiring many additional dependencies if the user is running an older RubyGems.
20
+ class Hoe
21
+ def extra_deps
22
+ @extra_deps.reject do |x|
23
+ Array(x).first == 'hoe'
24
+ end
25
+ end
26
+ def extra_dev_deps
27
+ @extra_dev_deps.reject do |x|
28
+ Array(x).first == 'hoe'
29
+ end
30
+ end
31
+ end
32
+
33
+ Hoe.new('right_aws', RightAws::VERSION::STRING) do |p|
34
+ p.rubyforge_name = 'rightscale'
35
+ p.author = 'RightScale, Inc.'
36
+ p.email = 'rubygems@rightscale.com'
37
+ p.summary = 'Interface classes for the Amazon EC2/EBS, SQS, S3, SDB, and ACF Web Services'
38
+ p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
39
+ p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
40
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
41
+ p.remote_rdoc_dir = "/right_aws_gem_doc"
42
+ p.extra_deps = [['right_http_connection','>= 1.2.4']]
43
+ p.test_globs = testglobs
44
+ end
45
+
46
+ desc "Analyze code coverage of the unit tests."
47
+ Rcov::RcovTask.new do |t|
48
+ t.test_files = FileList[testglobs]
49
+ #t.verbose = true # uncomment to see the executed command
50
+ end
51
+
52
+ desc "Test just the SQS interface"
53
+ task :testsqs do
54
+ require 'test/test_credentials'
55
+ require 'test/http_connection'
56
+ TestCredentials.get_credentials
57
+ require 'test/sqs/test_right_sqs.rb'
58
+ end
59
+
60
+ desc "Test just the second generation SQS interface"
61
+ task :testsqs2 do
62
+ require 'test/test_credentials'
63
+ require 'test/http_connection'
64
+ TestCredentials.get_credentials
65
+ require 'test/sqs/test_right_sqs_gen2.rb'
66
+ end
67
+
68
+ desc "Test just the S3 interface"
69
+ task :tests3 do
70
+ require 'test/test_credentials'
71
+ require 'test/http_connection'
72
+ TestCredentials.get_credentials
73
+ require 'test/s3/test_right_s3.rb'
74
+ end
75
+
76
+ desc "Test just the S3 interface using local stubs"
77
+ task :tests3local do
78
+ require 'test/test_credentials'
79
+ require 'test/http_connection'
80
+ TestCredentials.get_credentials
81
+ require 'test/s3/test_right_s3_stubbed.rb'
82
+ end
83
+
84
+ desc "Test just the EC2 interface"
85
+ task :testec2 do
86
+ require 'test/test_credentials'
87
+ TestCredentials.get_credentials
88
+ require 'test/ec2/test_right_ec2.rb'
89
+ end
90
+
91
+ desc "Test just the SDB interface"
92
+ task :testsdb do
93
+ require 'test/test_credentials'
94
+ TestCredentials.get_credentials
95
+ require 'test/sdb/test_right_sdb.rb'
96
+ end
97
+
98
+ desc "Test active SDB interface"
99
+ task :testactivesdb do
100
+ require 'test/test_credentials'
101
+ TestCredentials.get_credentials
102
+ require 'test/sdb/test_active_sdb.rb'
103
+ end
104
+
105
+ desc "Test CloudFront interface"
106
+ task :testacf do
107
+ require 'test/test_credentials'
108
+ TestCredentials.get_credentials
109
+ require 'test/acf/test_right_acf.rb'
110
+ end
111
+
112
+ # vim: syntax=Ruby
@@ -0,0 +1,379 @@
1
+ #
2
+ # Copyright (c) 2008 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #
23
+
24
+ module RightAws
25
+
26
+ # = RightAws::AcfInterface -- RightScale Amazon's CloudFront interface
27
+ # The AcfInterface class provides a complete interface to Amazon's
28
+ # CloudFront service.
29
+ #
30
+ # For explanations of the semantics of each call, please refer to
31
+ # Amazon's documentation at
32
+ # http://developer.amazonwebservices.com/connect/kbcategory.jspa?categoryID=211
33
+ #
34
+ # Example:
35
+ #
36
+ # acf = RightAws::AcfInterface.new('1E3GDYEOGFJPIT7XXXXXX','hgTHt68JY07JKUY08ftHYtERkjgtfERn57XXXXXX')
37
+ #
38
+ # list = acf.list_distributions #=>
39
+ # [{:status => "Deployed",
40
+ # :domain_name => "d74zzrxmpmygb.6hops.net",
41
+ # :aws_id => "E4U91HCJHGXVC",
42
+ # :origin => "my-bucket.s3.amazonaws.com",
43
+ # :cnames => ["x1.my-awesome-site.net", "x1.my-awesome-site.net"]
44
+ # :comment => "My comments",
45
+ # :last_modified_time => Wed Sep 10 17:00:04 UTC 2008 }, ..., {...} ]
46
+ #
47
+ # distibution = list.first
48
+ #
49
+ # info = acf.get_distribution(distibution[:aws_id]) #=>
50
+ # {:enabled => true,
51
+ # :caller_reference => "200809102100536497863003",
52
+ # :e_tag => "E39OHHU1ON65SI",
53
+ # :status => "Deployed",
54
+ # :domain_name => "d3dxv71tbbt6cd.6hops.net",
55
+ # :cnames => ["web1.my-awesome-site.net", "web2.my-awesome-site.net"]
56
+ # :aws_id => "E2REJM3VUN5RSI",
57
+ # :comment => "Woo-Hoo!",
58
+ # :origin => "my-bucket.s3.amazonaws.com",
59
+ # :last_modified_time => Wed Sep 10 17:00:54 UTC 2008 }
60
+ #
61
+ # config = acf.get_distribution_config(distibution[:aws_id]) #=>
62
+ # {:enabled => true,
63
+ # :caller_reference => "200809102100536497863003",
64
+ # :e_tag => "E39OHHU1ON65SI",
65
+ # :cnames => ["web1.my-awesome-site.net", "web2.my-awesome-site.net"]
66
+ # :comment => "Woo-Hoo!",
67
+ # :origin => "my-bucket.s3.amazonaws.com"}
68
+ #
69
+ # config[:comment] = 'Olah-lah!'
70
+ # config[:enabled] = false
71
+ # config[:cnames] << "web3.my-awesome-site.net"
72
+ #
73
+ # acf.set_distribution_config(distibution[:aws_id], config) #=> true
74
+ #
75
+ class AcfInterface < RightAwsBase
76
+
77
+ include RightAwsBaseInterface
78
+
79
+ API_VERSION = "2008-06-30"
80
+ DEFAULT_HOST = 'cloudfront.amazonaws.com'
81
+ DEFAULT_PORT = 443
82
+ DEFAULT_PROTOCOL = 'https'
83
+ DEFAULT_PATH = '/'
84
+
85
+ @@bench = AwsBenchmarkingBlock.new
86
+ def self.bench_xml
87
+ @@bench.xml
88
+ end
89
+ def self.bench_service
90
+ @@bench.service
91
+ end
92
+
93
+ # Create a new handle to a CloudFront account. All handles share the same per process or per thread
94
+ # HTTP connection to CloudFront. Each handle is for a specific account. The params have the
95
+ # following options:
96
+ # * <tt>:server</tt>: CloudFront service host, default: DEFAULT_HOST
97
+ # * <tt>:port</tt>: CloudFront service port, default: DEFAULT_PORT
98
+ # * <tt>:protocol</tt>: 'http' or 'https', default: DEFAULT_PROTOCOL
99
+ # * <tt>:multi_thread</tt>: true=HTTP connection per thread, false=per process
100
+ # * <tt>:logger</tt>: for log messages, default: RAILS_DEFAULT_LOGGER else STDOUT
101
+ # * <tt>:cache</tt>: true/false: caching for list_distributions method, default: false.
102
+ #
103
+ # acf = RightAws::AcfInterface.new('1E3GDYEOGFJPIT7XXXXXX','hgTHt68JY07JKUY08ftHYtERkjgtfERn57XXXXXX',
104
+ # {:multi_thread => true, :logger => Logger.new('/tmp/x.log')}) #=> #<RightAws::AcfInterface::0xb7b3c30c>
105
+ #
106
+ def initialize(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
107
+ init({ :name => 'ACF',
108
+ :default_host => ENV['ACF_URL'] ? URI.parse(ENV['ACF_URL']).host : DEFAULT_HOST,
109
+ :default_port => ENV['ACF_URL'] ? URI.parse(ENV['ACF_URL']).port : DEFAULT_PORT,
110
+ :default_service => ENV['ACF_URL'] ? URI.parse(ENV['ACF_URL']).path : DEFAULT_PATH,
111
+ :default_protocol => ENV['ACF_URL'] ? URI.parse(ENV['ACF_URL']).scheme : DEFAULT_PROTOCOL },
112
+ aws_access_key_id || ENV['AWS_ACCESS_KEY_ID'],
113
+ aws_secret_access_key || ENV['AWS_SECRET_ACCESS_KEY'],
114
+ params)
115
+ end
116
+
117
+ #-----------------------------------------------------------------
118
+ # Requests
119
+ #-----------------------------------------------------------------
120
+
121
+ # Generates request hash for REST API.
122
+ def generate_request(method, path, body=nil, headers={}) # :nodoc:
123
+ headers['content-type'] ||= 'text/xml' if body
124
+ headers['date'] = Time.now.httpdate
125
+ # Auth
126
+ signature = AwsUtils::sign(@aws_secret_access_key, headers['date'])
127
+ headers['Authorization'] = "AWS #{@aws_access_key_id}:#{signature}"
128
+ # Request
129
+ path = "#{@params[:default_service]}/#{API_VERSION}/#{path}"
130
+ request = "Net::HTTP::#{method.capitalize}".constantize.new(path)
131
+ request.body = body if body
132
+ # Set request headers
133
+ headers.each { |key, value| request[key.to_s] = value }
134
+ # prepare output hash
135
+ { :request => request,
136
+ :server => @params[:server],
137
+ :port => @params[:port],
138
+ :protocol => @params[:protocol] }
139
+ end
140
+
141
+ # Sends request to Amazon and parses the response.
142
+ # Raises AwsError if any banana happened.
143
+ def request_info(request, parser, &block) # :nodoc:
144
+ thread = @params[:multi_thread] ? Thread.current : Thread.main
145
+ thread[:acf_connection] ||= Rightscale::HttpConnection.new(:exception => RightAws::AwsError, :logger => @logger)
146
+ request_info_impl(thread[:acf_connection], @@bench, request, parser, &block)
147
+ end
148
+
149
+ #-----------------------------------------------------------------
150
+ # Helpers:
151
+ #-----------------------------------------------------------------
152
+
153
+ def self.escape(text) # :nodoc:
154
+ REXML::Text::normalize(text)
155
+ end
156
+
157
+ def self.unescape(text) # :nodoc:
158
+ REXML::Text::unnormalize(text)
159
+ end
160
+
161
+ def xmlns # :nodoc:
162
+ %Q{"http://#{@params[:server]}/doc/#{API_VERSION}/"}
163
+ end
164
+
165
+ def generate_call_reference # :nodoc:
166
+ result = Time.now.strftime('%Y%m%d%H%M%S')
167
+ 10.times{ result << rand(10).to_s }
168
+ result
169
+ end
170
+
171
+ def merge_headers(hash) # :nodoc:
172
+ hash[:location] = @last_response['Location'] if @last_response['Location']
173
+ hash[:e_tag] = @last_response['ETag'] if @last_response['ETag']
174
+ hash
175
+ end
176
+
177
+ #-----------------------------------------------------------------
178
+ # API Calls:
179
+ #-----------------------------------------------------------------
180
+
181
+ # List distributions.
182
+ # Returns an array of distributions or RightAws::AwsError exception.
183
+ #
184
+ # acf.list_distributions #=>
185
+ # [{:status => "Deployed",
186
+ # :domain_name => "d74zzrxmpmygb.6hops.net",
187
+ # :aws_id => "E4U91HCJHGXVC",
188
+ # :cnames => ["web1.my-awesome-site.net", "web2.my-awesome-site.net"]
189
+ # :origin => "my-bucket.s3.amazonaws.com",
190
+ # :comment => "My comments",
191
+ # :last_modified_time => Wed Sep 10 17:00:04 UTC 2008 }, ..., {...} ]
192
+ #
193
+ def list_distributions
194
+ request_hash = generate_request('GET', 'distribution')
195
+ request_cache_or_info :list_distributions, request_hash, AcfDistributionListParser, @@bench
196
+ end
197
+
198
+ # Create a new distribution.
199
+ # Returns the just created distribution or RightAws::AwsError exception.
200
+ #
201
+ # acf.create_distribution('bucket-for-k-dzreyev.s3.amazonaws.com', 'Woo-Hoo!', true, ['web1.my-awesome-site.net'] ) #=>
202
+ # {:comment => "Woo-Hoo!",
203
+ # :enabled => true,
204
+ # :location => "https://cloudfront.amazonaws.com/2008-06-30/distribution/E2REJM3VUN5RSI",
205
+ # :status => "InProgress",
206
+ # :aws_id => "E2REJM3VUN5RSI",
207
+ # :domain_name => "d3dxv71tbbt6cd.6hops.net",
208
+ # :origin => "my-bucket.s3.amazonaws.com",
209
+ # :cnames => ["web1.my-awesome-site.net"]
210
+ # :last_modified_time => Wed Sep 10 17:00:54 UTC 2008,
211
+ # :caller_reference => "200809102100536497863003"}
212
+ #
213
+ def create_distribution(origin, comment='', enabled=true, cnames=[], caller_reference=nil)
214
+ # join CNAMES
215
+ cnames_str = ''
216
+ unless cnames.blank?
217
+ cnames.to_a.each { |cname| cnames_str += "\n <CNAME>#{cname}</CNAME>" }
218
+ end
219
+ # reference
220
+ caller_reference ||= generate_call_reference
221
+ body = <<-EOXML
222
+ <?xml version="1.0" encoding="UTF-8"?>
223
+ <DistributionConfig xmlns=#{xmlns}>
224
+ <Origin>#{origin}</Origin>
225
+ <CallerReference>#{caller_reference}</CallerReference>
226
+ #{cnames_str.lstrip}
227
+ <Comment>#{AcfInterface::escape(comment.to_s)}</Comment>
228
+ <Enabled>#{enabled}</Enabled>
229
+ </DistributionConfig>
230
+ EOXML
231
+ request_hash = generate_request('POST', 'distribution', body.strip)
232
+ merge_headers(request_info(request_hash, AcfDistributionParser.new))
233
+ end
234
+
235
+ # Get a distribution's information.
236
+ # Returns a distribution's information or RightAws::AwsError exception.
237
+ #
238
+ # acf.get_distribution('E2REJM3VUN5RSI') #=>
239
+ # {:enabled => true,
240
+ # :caller_reference => "200809102100536497863003",
241
+ # :e_tag => "E39OHHU1ON65SI",
242
+ # :status => "Deployed",
243
+ # :domain_name => "d3dxv71tbbt6cd.6hops.net",
244
+ # :cnames => ["web1.my-awesome-site.net", "web2.my-awesome-site.net"]
245
+ # :aws_id => "E2REJM3VUN5RSI",
246
+ # :comment => "Woo-Hoo!",
247
+ # :origin => "my-bucket.s3.amazonaws.com",
248
+ # :last_modified_time => Wed Sep 10 17:00:54 UTC 2008 }
249
+ #
250
+ def get_distribution(aws_id)
251
+ request_hash = generate_request('GET', "distribution/#{aws_id}")
252
+ merge_headers(request_info(request_hash, AcfDistributionParser.new))
253
+ end
254
+
255
+ # Get a distribution's configuration.
256
+ # Returns a distribution's configuration or RightAws::AwsError exception.
257
+ #
258
+ # acf.get_distribution_config('E2REJM3VUN5RSI') #=>
259
+ # {:enabled => true,
260
+ # :caller_reference => "200809102100536497863003",
261
+ # :e_tag => "E39OHHU1ON65SI",
262
+ # :cnames => ["web1.my-awesome-site.net", "web2.my-awesome-site.net"]
263
+ # :comment => "Woo-Hoo!",
264
+ # :origin => "my-bucket.s3.amazonaws.com"}
265
+ #
266
+ def get_distribution_config(aws_id)
267
+ request_hash = generate_request('GET', "distribution/#{aws_id}/config")
268
+ merge_headers(request_info(request_hash, AcfDistributionConfigParser.new))
269
+ end
270
+
271
+ # Set a distribution's configuration
272
+ # (the :origin and the :caller_reference cannot be changed).
273
+ # Returns +true+ on success or RightAws::AwsError exception.
274
+ #
275
+ # config = acf.get_distribution_config('E2REJM3VUN5RSI') #=>
276
+ # {:enabled => true,
277
+ # :caller_reference => "200809102100536497863003",
278
+ # :e_tag => "E39OHHU1ON65SI",
279
+ # :cnames => ["web1.my-awesome-site.net", "web2.my-awesome-site.net"]
280
+ # :comment => "Woo-Hoo!",
281
+ # :origin => "my-bucket.s3.amazonaws.com"}
282
+ # config[:comment] = 'Olah-lah!'
283
+ # config[:enabled] = false
284
+ # acf.set_distribution_config('E2REJM3VUN5RSI', config) #=> true
285
+ #
286
+ def set_distribution_config(aws_id, config)
287
+ # join CNAMES
288
+ cnames_str = ''
289
+ unless config[:cnames].blank?
290
+ config[:cnames].to_a.each { |cname| cnames_str += "\n <CNAME>#{cname}</CNAME>" }
291
+ end
292
+ # format request's XML body
293
+ body = <<-EOXML
294
+ <?xml version="1.0" encoding="UTF-8"?>
295
+ <DistributionConfig xmlns=#{xmlns}>
296
+ <Origin>#{config[:origin]}</Origin>
297
+ <CallerReference>#{config[:caller_reference]}</CallerReference>
298
+ #{cnames_str.lstrip}
299
+ <Comment>#{AcfInterface::escape(config[:comment].to_s)}</Comment>
300
+ <Enabled>#{config[:enabled]}</Enabled>
301
+ </DistributionConfig>
302
+ EOXML
303
+ request_hash = generate_request('PUT', "distribution/#{aws_id}/config", body.strip,
304
+ 'If-Match' => config[:e_tag])
305
+ request_info(request_hash, RightHttp2xxParser.new)
306
+ end
307
+
308
+ # Delete a distribution. The enabled distribution cannot be deleted.
309
+ # Returns +true+ on success or RightAws::AwsError exception.
310
+ #
311
+ # acf.delete_distribution('E2REJM3VUN5RSI', 'E39OHHU1ON65SI') #=> true
312
+ #
313
+ def delete_distribution(aws_id, e_tag)
314
+ request_hash = generate_request('DELETE', "distribution/#{aws_id}", nil,
315
+ 'If-Match' => e_tag)
316
+ request_info(request_hash, RightHttp2xxParser.new)
317
+ end
318
+
319
+ #-----------------------------------------------------------------
320
+ # PARSERS:
321
+ #-----------------------------------------------------------------
322
+
323
+ class AcfDistributionListParser < RightAWSParser # :nodoc:
324
+ def reset
325
+ @result = []
326
+ end
327
+ def tagstart(name, attributes)
328
+ @distribution = { :cnames => [] } if name == 'DistributionSummary'
329
+ end
330
+ def tagend(name)
331
+ case name
332
+ when 'Id' then @distribution[:aws_id] = @text
333
+ when 'Status' then @distribution[:status] = @text
334
+ when 'LastModifiedTime' then @distribution[:last_modified_time] = Time.parse(@text)
335
+ when 'DomainName' then @distribution[:domain_name] = @text
336
+ when 'Origin' then @distribution[:origin] = @text
337
+ when 'Comment' then @distribution[:comment] = AcfInterface::unescape(@text)
338
+ when 'CNAME' then @distribution[:cnames] << @text
339
+ when 'DistributionSummary' then @result << @distribution
340
+ end
341
+ end
342
+ end
343
+
344
+ class AcfDistributionParser < RightAWSParser # :nodoc:
345
+ def reset
346
+ @result = { :cnames => [] }
347
+ end
348
+ def tagend(name)
349
+ case name
350
+ when 'Id' then @result[:aws_id] = @text
351
+ when 'Status' then @result[:status] = @text
352
+ when 'LastModifiedTime' then @result[:last_modified_time] = Time.parse(@text)
353
+ when 'DomainName' then @result[:domain_name] = @text
354
+ when 'Origin' then @result[:origin] = @text
355
+ when 'CallerReference' then @result[:caller_reference] = @text
356
+ when 'Comment' then @result[:comment] = AcfInterface::unescape(@text)
357
+ when 'Enabled' then @result[:enabled] = @text == 'true' ? true : false
358
+ when 'CNAME' then @result[:cnames] << @text
359
+ end
360
+ end
361
+ end
362
+
363
+ class AcfDistributionConfigParser < RightAWSParser # :nodoc:
364
+ def reset
365
+ @result = { :cnames => [] }
366
+ end
367
+ def tagend(name)
368
+ case name
369
+ when 'Origin' then @result[:origin] = @text
370
+ when 'CallerReference' then @result[:caller_reference] = @text
371
+ when 'Comment' then @result[:comment] = AcfInterface::unescape(@text)
372
+ when 'Enabled' then @result[:enabled] = @text == 'true' ? true : false
373
+ when 'CNAME' then @result[:cnames] << @text
374
+ end
375
+ end
376
+ end
377
+
378
+ end
379
+ end