ijin-right_aws 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. data/History.txt +239 -0
  2. data/Manifest.txt +46 -0
  3. data/README.txt +167 -0
  4. data/Rakefile +125 -0
  5. data/VERSION +1 -0
  6. data/lib/acf/right_acf_interface.rb +413 -0
  7. data/lib/acw/right_acw_interface.rb +249 -0
  8. data/lib/as/right_as_interface.rb +690 -0
  9. data/lib/awsbase/benchmark_fix.rb +39 -0
  10. data/lib/awsbase/right_awsbase.rb +931 -0
  11. data/lib/awsbase/support.rb +115 -0
  12. data/lib/ec2/right_ec2.rb +617 -0
  13. data/lib/ec2/right_ec2_ebs.rb +451 -0
  14. data/lib/ec2/right_ec2_images.rb +373 -0
  15. data/lib/ec2/right_ec2_instances.rb +760 -0
  16. data/lib/ec2/right_ec2_monitoring.rb +70 -0
  17. data/lib/ec2/right_ec2_reserved_instances.rb +167 -0
  18. data/lib/ec2/right_ec2_vpc.rb +571 -0
  19. data/lib/elb/right_elb_interface.rb +407 -0
  20. data/lib/rds/right_rds_interface.rb +998 -0
  21. data/lib/right_aws.rb +79 -0
  22. data/lib/s3/right_s3.rb +1102 -0
  23. data/lib/s3/right_s3_interface.rb +1195 -0
  24. data/lib/sdb/active_sdb.rb +930 -0
  25. data/lib/sdb/right_sdb_interface.rb +672 -0
  26. data/lib/sqs/right_sqs.rb +388 -0
  27. data/lib/sqs/right_sqs_gen2.rb +343 -0
  28. data/lib/sqs/right_sqs_gen2_interface.rb +523 -0
  29. data/lib/sqs/right_sqs_interface.rb +594 -0
  30. data/test/acf/test_helper.rb +2 -0
  31. data/test/acf/test_right_acf.rb +146 -0
  32. data/test/awsbase/test_helper.rb +2 -0
  33. data/test/awsbase/test_right_awsbase.rb +12 -0
  34. data/test/ec2/test_helper.rb +2 -0
  35. data/test/ec2/test_right_ec2.rb +108 -0
  36. data/test/http_connection.rb +87 -0
  37. data/test/rds/test_helper.rb +2 -0
  38. data/test/rds/test_right_rds.rb +120 -0
  39. data/test/s3/test_helper.rb +2 -0
  40. data/test/s3/test_right_s3.rb +419 -0
  41. data/test/s3/test_right_s3_stubbed.rb +95 -0
  42. data/test/sdb/test_active_sdb.rb +299 -0
  43. data/test/sdb/test_helper.rb +3 -0
  44. data/test/sdb/test_right_sdb.rb +247 -0
  45. data/test/sqs/test_helper.rb +2 -0
  46. data/test/sqs/test_right_sqs.rb +291 -0
  47. data/test/sqs/test_right_sqs_gen2.rb +276 -0
  48. data/test/test_credentials.rb +37 -0
  49. data/test/ts_right_aws.rb +14 -0
  50. metadata +122 -0
@@ -0,0 +1,39 @@
1
+ #
2
+ # Copyright (c) 2007-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
+
25
+
26
+ # A hack because there's a bug in add! in Benchmark::Tms
27
+ module Benchmark #:nodoc:
28
+ class Tms #:nodoc:
29
+ def add!(&blk)
30
+ t = Benchmark::measure(&blk)
31
+ @utime = utime + t.utime
32
+ @stime = stime + t.stime
33
+ @cutime = cutime + t.cutime
34
+ @cstime = cstime + t.cstime
35
+ @real = real + t.real
36
+ self
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,931 @@
1
+ #
2
+ # Copyright (c) 2007-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
+ # Test
25
+ module RightAws
26
+ require 'md5'
27
+ require 'pp'
28
+
29
+ class AwsUtils #:nodoc:
30
+ @@digest1 = OpenSSL::Digest::Digest.new("sha1")
31
+ @@digest256 = nil
32
+ if OpenSSL::OPENSSL_VERSION_NUMBER > 0x00908000
33
+ @@digest256 = OpenSSL::Digest::Digest.new("sha256") rescue nil # Some installation may not support sha256
34
+ end
35
+
36
+ def self.sign(aws_secret_access_key, auth_string)
37
+ Base64.encode64(OpenSSL::HMAC.digest(@@digest1, aws_secret_access_key, auth_string)).strip
38
+ end
39
+
40
+ # Escape a string accordingly Amazon rulles
41
+ # http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
42
+ def self.amz_escape(param)
43
+ param.to_s.gsub(/([^a-zA-Z0-9._~-]+)/n) do
44
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
45
+ end
46
+ end
47
+
48
+ # Set a timestamp and a signature version
49
+ def self.fix_service_params(service_hash, signature)
50
+ service_hash["Timestamp"] ||= Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.000Z") unless service_hash["Expires"]
51
+ service_hash["SignatureVersion"] = signature
52
+ service_hash
53
+ end
54
+
55
+ # Signature Version 0
56
+ # A deprecated guy (should work till septemper 2009)
57
+ def self.sign_request_v0(aws_secret_access_key, service_hash)
58
+ fix_service_params(service_hash, '0')
59
+ string_to_sign = "#{service_hash['Action']}#{service_hash['Timestamp'] || service_hash['Expires']}"
60
+ service_hash['Signature'] = AwsUtils::sign(aws_secret_access_key, string_to_sign)
61
+ service_hash.to_a.collect{|key,val| "#{amz_escape(key)}=#{amz_escape(val.to_s)}" }.join("&")
62
+ end
63
+
64
+ # Signature Version 1
65
+ # Another deprecated guy (should work till septemper 2009)
66
+ def self.sign_request_v1(aws_secret_access_key, service_hash)
67
+ fix_service_params(service_hash, '1')
68
+ string_to_sign = service_hash.sort{|a,b| (a[0].to_s.downcase)<=>(b[0].to_s.downcase)}.to_s
69
+ service_hash['Signature'] = AwsUtils::sign(aws_secret_access_key, string_to_sign)
70
+ service_hash.to_a.collect{|key,val| "#{amz_escape(key)}=#{amz_escape(val.to_s)}" }.join("&")
71
+ end
72
+
73
+ # Signature Version 2
74
+ # EC2, SQS and SDB requests must be signed by this guy.
75
+ # See: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
76
+ # http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1928
77
+ def self.sign_request_v2(aws_secret_access_key, service_hash, http_verb, host, uri)
78
+ fix_service_params(service_hash, '2')
79
+ # select a signing method (make an old openssl working with sha1)
80
+ # make 'HmacSHA256' to be a default one
81
+ service_hash['SignatureMethod'] = 'HmacSHA256' unless ['HmacSHA256', 'HmacSHA1'].include?(service_hash['SignatureMethod'])
82
+ service_hash['SignatureMethod'] = 'HmacSHA1' unless @@digest256
83
+ # select a digest
84
+ digest = (service_hash['SignatureMethod'] == 'HmacSHA256' ? @@digest256 : @@digest1)
85
+ # form string to sign
86
+ canonical_string = service_hash.keys.sort.map do |key|
87
+ "#{amz_escape(key)}=#{amz_escape(service_hash[key])}"
88
+ end.join('&')
89
+ string_to_sign = "#{http_verb.to_s.upcase}\n#{host.downcase}\n#{uri}\n#{canonical_string}"
90
+ # sign the string
91
+ signature = amz_escape(Base64.encode64(OpenSSL::HMAC.digest(digest, aws_secret_access_key, string_to_sign)).strip)
92
+ "#{canonical_string}&Signature=#{signature}"
93
+ end
94
+
95
+ # From Amazon's SQS Dev Guide, a brief description of how to escape:
96
+ # "URL encode the computed signature and other query parameters as specified in
97
+ # RFC1738, section 2.2. In addition, because the + character is interpreted as a blank space
98
+ # by Sun Java classes that perform URL decoding, make sure to encode the + character
99
+ # although it is not required by RFC1738."
100
+ # Avoid using CGI::escape to escape URIs.
101
+ # CGI::escape will escape characters in the protocol, host, and port
102
+ # sections of the URI. Only target chars in the query
103
+ # string should be escaped.
104
+ def self.URLencode(raw)
105
+ e = URI.escape(raw)
106
+ e.gsub(/\+/, "%2b")
107
+ end
108
+
109
+ def self.allow_only(allowed_keys, params)
110
+ bogus_args = []
111
+ params.keys.each {|p| bogus_args.push(p) unless allowed_keys.include?(p) }
112
+ raise AwsError.new("The following arguments were given but are not legal for the function call #{caller_method}: #{bogus_args.inspect}") if bogus_args.length > 0
113
+ end
114
+
115
+ def self.mandatory_arguments(required_args, params)
116
+ rargs = required_args.dup
117
+ params.keys.each {|p| rargs.delete(p)}
118
+ raise AwsError.new("The following mandatory arguments were not provided to #{caller_method}: #{rargs.inspect}") if rargs.length > 0
119
+ end
120
+
121
+ def self.caller_method
122
+ caller[1]=~/`(.*?)'/
123
+ $1
124
+ end
125
+
126
+ def self.split_items_and_params(array)
127
+ items = array.to_a.flatten.compact
128
+ params = items.last.kind_of?(Hash) ? items.pop : {}
129
+ [items, params]
130
+ end
131
+ end
132
+
133
+ class AwsBenchmarkingBlock #:nodoc:
134
+ attr_accessor :xml, :service
135
+ def initialize
136
+ # Benchmark::Tms instance for service (Ec2, S3, or SQS) access benchmarking.
137
+ @service = Benchmark::Tms.new()
138
+ # Benchmark::Tms instance for XML parsing benchmarking.
139
+ @xml = Benchmark::Tms.new()
140
+ end
141
+ end
142
+
143
+ class AwsNoChange < RuntimeError
144
+ end
145
+
146
+ class RightAwsBase
147
+
148
+ # Amazon HTTP Error handling
149
+
150
+ # Text, if found in an error message returned by AWS, indicates that this may be a transient
151
+ # error. Transient errors are automatically retried with exponential back-off.
152
+ AMAZON_PROBLEMS = [ 'internal service error',
153
+ 'is currently unavailable',
154
+ 'no response from',
155
+ 'Please try again',
156
+ 'InternalError',
157
+ 'Internal Server Error',
158
+ 'ServiceUnavailable', #from SQS docs
159
+ 'Unavailable',
160
+ 'This application is not currently available',
161
+ 'InsufficientInstanceCapacity'
162
+ ]
163
+ @@amazon_problems = AMAZON_PROBLEMS
164
+ # Returns a list of Amazon service responses which are known to be transient problems.
165
+ # We have to re-request if we get any of them, because the problem will probably disappear.
166
+ # By default this method returns the same value as the AMAZON_PROBLEMS const.
167
+ def self.amazon_problems
168
+ @@amazon_problems
169
+ end
170
+
171
+ # Sets the list of Amazon side problems. Use in conjunction with the
172
+ # getter to append problems.
173
+ def self.amazon_problems=(problems_list)
174
+ @@amazon_problems = problems_list
175
+ end
176
+
177
+ end
178
+
179
+ module RightAwsBaseInterface
180
+ DEFAULT_SIGNATURE_VERSION = '2'
181
+
182
+ @@caching = false
183
+ def self.caching
184
+ @@caching
185
+ end
186
+ def self.caching=(caching)
187
+ @@caching = caching
188
+ end
189
+
190
+ # Current aws_access_key_id
191
+ attr_reader :aws_access_key_id
192
+ # Last HTTP request object
193
+ attr_reader :last_request
194
+ # Last HTTP response object
195
+ attr_reader :last_response
196
+ # Last AWS errors list (used by AWSErrorHandler)
197
+ attr_accessor :last_errors
198
+ # Last AWS request id (used by AWSErrorHandler)
199
+ attr_accessor :last_request_id
200
+ # Logger object
201
+ attr_accessor :logger
202
+ # Initial params hash
203
+ attr_accessor :params
204
+ # RightHttpConnection instance
205
+ attr_reader :connection
206
+ # Cache
207
+ attr_reader :cache
208
+ # Signature version (all services except s3)
209
+ attr_reader :signature_version
210
+
211
+ def init(service_info, aws_access_key_id, aws_secret_access_key, params={}) #:nodoc:
212
+ @params = params
213
+ # If one defines EC2_URL he may forget to use a single slash as an "empty service" path.
214
+ # Amazon does not like this therefore add this bad boy if he is missing...
215
+ service_info[:default_service] = '/' if service_info[:default_service].blank?
216
+ raise AwsError.new("AWS access keys are required to operate on #{service_info[:name]}") \
217
+ if aws_access_key_id.blank? || aws_secret_access_key.blank?
218
+ @aws_access_key_id = aws_access_key_id
219
+ @aws_secret_access_key = aws_secret_access_key
220
+ # if the endpoint was explicitly defined - then use it
221
+ if @params[:endpoint_url]
222
+ @params[:server] = URI.parse(@params[:endpoint_url]).host
223
+ @params[:port] = URI.parse(@params[:endpoint_url]).port
224
+ @params[:service] = URI.parse(@params[:endpoint_url]).path
225
+ # make sure the 'service' path is not empty
226
+ @params[:service] = service_info[:default_service] if @params[:service].blank?
227
+ @params[:protocol] = URI.parse(@params[:endpoint_url]).scheme
228
+ @params[:region] = nil
229
+ else
230
+ @params[:server] ||= service_info[:default_host]
231
+ @params[:server] = "#{@params[:region]}.#{@params[:server]}" if @params[:region]
232
+ @params[:port] ||= service_info[:default_port]
233
+ @params[:service] ||= service_info[:default_service]
234
+ @params[:protocol] ||= service_info[:default_protocol]
235
+ end
236
+ # @params[:multi_thread] ||= defined?(AWS_DAEMON)
237
+ @params[:connections] ||= :shared # || :dedicated
238
+ @params[:max_connections] ||= 10
239
+ @params[:connection_lifetime] ||= 20*60
240
+ @params[:api_version] ||= service_info[:default_api_version]
241
+ @logger = @params[:logger]
242
+ @logger = RAILS_DEFAULT_LOGGER if !@logger && defined?(RAILS_DEFAULT_LOGGER)
243
+ @logger = Logger.new(STDOUT) if !@logger
244
+ @logger.info "New #{self.class.name} using #{@params[:connections]} connections mode"
245
+ @error_handler = nil
246
+ @cache = {}
247
+ @signature_version = (params[:signature_version] || DEFAULT_SIGNATURE_VERSION).to_s
248
+ end
249
+
250
+ def signed_service_params(aws_secret_access_key, service_hash, http_verb=nil, host=nil, service=nil )
251
+ case signature_version.to_s
252
+ when '0' then AwsUtils::sign_request_v0(aws_secret_access_key, service_hash)
253
+ when '1' then AwsUtils::sign_request_v1(aws_secret_access_key, service_hash)
254
+ when '2' then AwsUtils::sign_request_v2(aws_secret_access_key, service_hash, http_verb, host, service)
255
+ else raise AwsError.new("Unknown signature version (#{signature_version.to_s}) requested")
256
+ end
257
+ end
258
+
259
+ # Returns +true+ if the describe_xxx responses are being cached
260
+ def caching?
261
+ @params.key?(:cache) ? @params[:cache] : @@caching
262
+ end
263
+
264
+ # Check if the aws function response hits the cache or not.
265
+ # If the cache hits:
266
+ # - raises an +AwsNoChange+ exception if +do_raise+ == +:raise+.
267
+ # - returnes parsed response from the cache if it exists or +true+ otherwise.
268
+ # If the cache miss or the caching is off then returns +false+.
269
+ def cache_hits?(function, response, do_raise=:raise)
270
+ result = false
271
+ if caching?
272
+ function = function.to_sym
273
+ # get rid of requestId (this bad boy was added for API 2008-08-08+ and it is uniq for every response)
274
+ # feb 04, 2009 (load balancer uses 'RequestId' hence use 'i' modifier to hit it also)
275
+ response = response.sub(%r{<requestId>.+?</requestId>}i, '')
276
+ response_md5 = MD5.md5(response).to_s
277
+ # check for changes
278
+ unless @cache[function] && @cache[function][:response_md5] == response_md5
279
+ # well, the response is new, reset cache data
280
+ update_cache(function, {:response_md5 => response_md5,
281
+ :timestamp => Time.now,
282
+ :hits => 0,
283
+ :parsed => nil})
284
+ else
285
+ # aha, cache hits, update the data and throw an exception if needed
286
+ @cache[function][:hits] += 1
287
+ if do_raise == :raise
288
+ raise(AwsNoChange, "Cache hit: #{function} response has not changed since "+
289
+ "#{@cache[function][:timestamp].strftime('%Y-%m-%d %H:%M:%S')}, "+
290
+ "hits: #{@cache[function][:hits]}.")
291
+ else
292
+ result = @cache[function][:parsed] || true
293
+ end
294
+ end
295
+ end
296
+ result
297
+ end
298
+
299
+ def update_cache(function, hash)
300
+ (@cache[function.to_sym] ||= {}).merge!(hash) if caching?
301
+ end
302
+
303
+ def on_exception(options={:raise=>true, :log=>true}) # :nodoc:
304
+ raise if $!.is_a?(AwsNoChange)
305
+ AwsError::on_aws_exception(self, options)
306
+ end
307
+
308
+ # # Return +true+ if this instance works in multi_thread mode and +false+ otherwise.
309
+ # def multi_thread
310
+ # @params[:multi_thread]
311
+ # end
312
+
313
+ # ACF, AMS, EC2, LBS and SDB uses this guy
314
+ # SQS and S3 use their own methods
315
+ def generate_request_impl(verb, action, options={}) #:nodoc:
316
+ # Form a valid http verb: 'GET' or 'POST' (all the other are not supported now)
317
+ http_verb = verb.to_s.upcase
318
+ # remove empty keys from request options
319
+ options.delete_if { |key, value| value.nil? }
320
+ # prepare service data
321
+ service_hash = {"Action" => action,
322
+ "AWSAccessKeyId" => @aws_access_key_id,
323
+ "Version" => @params[:api_version] }
324
+ service_hash.merge!(options)
325
+ # Sign request options
326
+ service_params = signed_service_params(@aws_secret_access_key, service_hash, http_verb, @params[:server], @params[:service])
327
+ # Use POST if the length of the query string is too large
328
+ # see http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/MakingRESTRequests.html
329
+ if http_verb != 'POST' && service_params.size > 2000
330
+ http_verb = 'POST'
331
+ if signature_version == '2'
332
+ service_params = signed_service_params(@aws_secret_access_key, service_hash, http_verb, @params[:server], @params[:service])
333
+ end
334
+ end
335
+ # create a request
336
+ case http_verb
337
+ when 'GET'
338
+ request = Net::HTTP::Get.new("#{@params[:service]}?#{service_params}")
339
+ when 'POST'
340
+ request = Net::HTTP::Post.new(@params[:service])
341
+ request.body = service_params
342
+ request['Content-Type'] = 'application/x-www-form-urlencoded'
343
+ else
344
+ raise "Unsupported HTTP verb #{verb.inspect}!"
345
+ end
346
+ # prepare output hash
347
+ { :request => request,
348
+ :server => @params[:server],
349
+ :port => @params[:port],
350
+ :protocol => @params[:protocol] }
351
+ end
352
+
353
+ def get_connection(aws_service, request) #:nodoc
354
+ server_url = "#{request[:protocol]}://#{request[:server]}:#{request[:port]}}"
355
+ #
356
+ case @params[:connections].to_s
357
+ when 'dedicated'
358
+ @connections_storage ||= {}
359
+ else # 'dedicated'
360
+ @connections_storage = (Thread.current[aws_service] ||= {})
361
+ end
362
+ #
363
+ @connections_storage[server_url] ||= {}
364
+ @connections_storage[server_url][:last_used_at] = Time.now
365
+ @connections_storage[server_url][:connection] ||= Rightscale::HttpConnection.new(:exception => RightAws::AwsError, :logger => @logger)
366
+ # keep X most recent connections (but were used not far than Y minutes ago)
367
+ connections = 0
368
+ @connections_storage.to_a.sort{|i1, i2| i2[1][:last_used_at] <=> i1[1][:last_used_at]}.to_a.each do |i|
369
+ if i[0] != server_url && (@params[:max_connections] <= connections || i[1][:last_used_at] < Time.now - @params[:connection_lifetime])
370
+ # delete the connection from the list
371
+ @connections_storage.delete(i[0])
372
+ # then finish it
373
+ i[1][:connection].finish((@params[:max_connections] <= connections) ? "out-of-limit" : "out-of-date") rescue nil
374
+ else
375
+ connections += 1
376
+ end
377
+ end
378
+ @connections_storage[server_url][:connection]
379
+ end
380
+
381
+ # All services uses this guy.
382
+ def request_info_impl(aws_service, benchblock, request, parser, &block) #:nodoc:
383
+ @connection = get_connection(aws_service, request)
384
+ @last_request = request[:request]
385
+ @last_response = nil
386
+ response = nil
387
+ blockexception = nil
388
+
389
+ if(block != nil)
390
+ # TRB 9/17/07 Careful - because we are passing in blocks, we get a situation where
391
+ # an exception may get thrown in the block body (which is high-level
392
+ # code either here or in the application) but gets caught in the
393
+ # low-level code of HttpConnection. The solution is not to let any
394
+ # exception escape the block that we pass to HttpConnection::request.
395
+ # Exceptions can originate from code directly in the block, or from user
396
+ # code called in the other block which is passed to response.read_body.
397
+ benchblock.service.add! do
398
+ responsehdr = @connection.request(request) do |response|
399
+ #########
400
+ begin
401
+ @last_response = response
402
+ if response.is_a?(Net::HTTPSuccess)
403
+ @error_handler = nil
404
+ response.read_body(&block)
405
+ else
406
+ @error_handler = AWSErrorHandler.new(self, parser, :errors_list => self.class.amazon_problems) unless @error_handler
407
+ check_result = @error_handler.check(request)
408
+ if check_result
409
+ @error_handler = nil
410
+ return check_result
411
+ end
412
+ raise AwsError.new(@last_errors, @last_response.code, @last_request_id)
413
+ end
414
+ rescue Exception => e
415
+ blockexception = e
416
+ end
417
+ end
418
+ #########
419
+
420
+ #OK, now we are out of the block passed to the lower level
421
+ if(blockexception)
422
+ raise blockexception
423
+ end
424
+ benchblock.xml.add! do
425
+ parser.parse(responsehdr)
426
+ end
427
+ return parser.result
428
+ end
429
+ else
430
+ benchblock.service.add!{ response = @connection.request(request) }
431
+ # check response for errors...
432
+ @last_response = response
433
+ if response.is_a?(Net::HTTPSuccess)
434
+ @error_handler = nil
435
+ benchblock.xml.add! { parser.parse(response) }
436
+ return parser.result
437
+ else
438
+ @error_handler = AWSErrorHandler.new(self, parser, :errors_list => self.class.amazon_problems) unless @error_handler
439
+ check_result = @error_handler.check(request)
440
+ if check_result
441
+ @error_handler = nil
442
+ return check_result
443
+ end
444
+ raise AwsError.new(@last_errors, @last_response.code, @last_request_id)
445
+ end
446
+ end
447
+ rescue
448
+ @error_handler = nil
449
+ raise
450
+ end
451
+
452
+ def request_cache_or_info(method, link, parser_class, benchblock, use_cache=true) #:nodoc:
453
+ # We do not want to break the logic of parsing hence will use a dummy parser to process all the standard
454
+ # steps (errors checking etc). The dummy parser does nothig - just returns back the params it received.
455
+ # If the caching is enabled and hit then throw AwsNoChange.
456
+ # P.S. caching works for the whole images list only! (when the list param is blank)
457
+ # check cache
458
+ response, params = request_info(link, RightDummyParser.new)
459
+ cache_hits?(method.to_sym, response.body) if use_cache
460
+ parser = parser_class.new(:logger => @logger)
461
+ benchblock.xml.add!{ parser.parse(response, params) }
462
+ result = block_given? ? yield(parser) : parser.result
463
+ # update parsed data
464
+ update_cache(method.to_sym, :parsed => result) if use_cache
465
+ result
466
+ end
467
+
468
+ # Returns Amazons request ID for the latest request
469
+ def last_request_id
470
+ @last_response && @last_response.body.to_s[%r{<requestId>(.+?)</requestId>}i] && $1
471
+ end
472
+
473
+ # Format array of items into Amazons handy hash ('?' is a place holder):
474
+ #
475
+ # amazonize_list('Item', ['a', 'b', 'c']) =>
476
+ # { 'Item.1' => 'a', 'Item.2' => 'b', 'Item.3' => 'c' }
477
+ #
478
+ # amazonize_list('Item.?.instance', ['a', 'c']) #=>
479
+ # { 'Item.1.instance' => 'a', 'Item.2.instance' => 'c' }
480
+ #
481
+ # amazonize_list(['Item.?.Name', 'Item.?.Value'], {'A' => 'a', 'B' => 'b'}) #=>
482
+ # { 'Item.1.Name' => 'A', 'Item.1.Value' => 'a',
483
+ # 'Item.2.Name' => 'B', 'Item.2.Value' => 'b' }
484
+ #
485
+ # amazonize_list(['Item.?.Name', 'Item.?.Value'], [['A','a'], ['B','b']]) #=>
486
+ # { 'Item.1.Name' => 'A', 'Item.1.Value' => 'a',
487
+ # 'Item.2.Name' => 'B', 'Item.2.Value' => 'b' }
488
+ #
489
+ # amazonize_list(['Filter.?.Key', 'Filter.?.Value.?'], {'A' => ['aa','ab'], 'B' => ['ba','bb']}) #=>
490
+ # amazonize_list(['Filter.?.Key', 'Filter.?.Value.?'], [['A',['aa','ab']], ['B',['ba','bb']]]) #=>
491
+ # {"Filter.1.Key"=>"A",
492
+ # "Filter.1.Value.1"=>"aa",
493
+ # "Filter.1.Value.2"=>"ab",
494
+ # "Filter.2.Key"=>"B",
495
+ # "Filter.2.Value.1"=>"ba",
496
+ # "Filter.2.Value.2"=>"bb"}
497
+ def amazonize_list(masks, list) #:nodoc:
498
+ groups = {}
499
+ list.to_a.each_with_index do |list_item, i|
500
+ masks.to_a.each_with_index do |mask, mask_idx|
501
+ key = mask[/\?/] ? mask.dup : mask.dup + '.?'
502
+ key.sub!('?', (i+1).to_s)
503
+ value = list_item.to_a[mask_idx]
504
+ if value.is_a?(Array)
505
+ groups.merge!(amazonize_list(key, value))
506
+ else
507
+ groups[key] = value
508
+ end
509
+ end
510
+ end
511
+ groups
512
+ end
513
+ end
514
+
515
+
516
+ # Exception class to signal any Amazon errors. All errors occuring during calls to Amazon's
517
+ # web services raise this type of error.
518
+ # Attribute inherited by RuntimeError:
519
+ # message - the text of the error, generally as returned by AWS in its XML response.
520
+ class AwsError < RuntimeError
521
+
522
+ # either an array of errors where each item is itself an array of [code, message]),
523
+ # or an error string if the error was raised manually, as in <tt>AwsError.new('err_text')</tt>
524
+ attr_reader :errors
525
+
526
+ # Request id (if exists)
527
+ attr_reader :request_id
528
+
529
+ # Response HTTP error code
530
+ attr_reader :http_code
531
+
532
+ def initialize(errors=nil, http_code=nil, request_id=nil)
533
+ @errors = errors
534
+ @request_id = request_id
535
+ @http_code = http_code
536
+ super(@errors.is_a?(Array) ? @errors.map{|code, msg| "#{code}: #{msg}"}.join("; ") : @errors.to_s)
537
+ end
538
+
539
+ # Does any of the error messages include the regexp +pattern+?
540
+ # Used to determine whether to retry request.
541
+ def include?(pattern)
542
+ if @errors.is_a?(Array)
543
+ @errors.each{ |code, msg| return true if code =~ pattern }
544
+ else
545
+ return true if @errors_str =~ pattern
546
+ end
547
+ false
548
+ end
549
+
550
+ # Generic handler for AwsErrors. +aws+ is the RightAws::S3, RightAws::EC2, or RightAws::SQS
551
+ # object that caused the exception (it must provide last_request and last_response). Supported
552
+ # boolean options are:
553
+ # * <tt>:log</tt> print a message into the log using aws.logger to access the Logger
554
+ # * <tt>:puts</tt> do a "puts" of the error
555
+ # * <tt>:raise</tt> re-raise the error after logging
556
+ def self.on_aws_exception(aws, options={:raise=>true, :log=>true})
557
+ # Only log & notify if not user error
558
+ if !options[:raise] || system_error?($!)
559
+ error_text = "#{$!.inspect}\n#{$@}.join('\n')}"
560
+ puts error_text if options[:puts]
561
+ # Log the error
562
+ if options[:log]
563
+ request = aws.last_request ? aws.last_request.path : '-none-'
564
+ response = aws.last_response ? "#{aws.last_response.code} -- #{aws.last_response.message} -- #{aws.last_response.body}" : '-none-'
565
+ aws.logger.error error_text
566
+ aws.logger.error "Request was: #{request}"
567
+ aws.logger.error "Response was: #{response}"
568
+ end
569
+ end
570
+ raise if options[:raise] # re-raise an exception
571
+ return nil
572
+ end
573
+
574
+ # True if e is an AWS system error, i.e. something that is for sure not the caller's fault.
575
+ # Used to force logging.
576
+ def self.system_error?(e)
577
+ !e.is_a?(self) || e.message =~ /InternalError|InsufficientInstanceCapacity|Unavailable/
578
+ end
579
+
580
+ end
581
+
582
+
583
+ class AWSErrorHandler
584
+ # 0-100 (%)
585
+ DEFAULT_CLOSE_ON_4XX_PROBABILITY = 10
586
+
587
+ @@reiteration_start_delay = 0.2
588
+ def self.reiteration_start_delay
589
+ @@reiteration_start_delay
590
+ end
591
+ def self.reiteration_start_delay=(reiteration_start_delay)
592
+ @@reiteration_start_delay = reiteration_start_delay
593
+ end
594
+
595
+ @@reiteration_time = 5
596
+ def self.reiteration_time
597
+ @@reiteration_time
598
+ end
599
+ def self.reiteration_time=(reiteration_time)
600
+ @@reiteration_time = reiteration_time
601
+ end
602
+
603
+ @@close_on_error = true
604
+ def self.close_on_error
605
+ @@close_on_error
606
+ end
607
+ def self.close_on_error=(close_on_error)
608
+ @@close_on_error = close_on_error
609
+ end
610
+
611
+ @@close_on_4xx_probability = DEFAULT_CLOSE_ON_4XX_PROBABILITY
612
+ def self.close_on_4xx_probability
613
+ @@close_on_4xx_probability
614
+ end
615
+ def self.close_on_4xx_probability=(close_on_4xx_probability)
616
+ @@close_on_4xx_probability = close_on_4xx_probability
617
+ end
618
+
619
+ # params:
620
+ # :reiteration_time
621
+ # :errors_list
622
+ # :close_on_error = true | false
623
+ # :close_on_4xx_probability = 1-100
624
+ def initialize(aws, parser, params={}) #:nodoc:
625
+ @aws = aws # Link to RightEc2 | RightSqs | RightS3 instance
626
+ @parser = parser # parser to parse Amazon response
627
+ @started_at = Time.now
628
+ @stop_at = @started_at + (params[:reiteration_time] || @@reiteration_time)
629
+ @errors_list = params[:errors_list] || []
630
+ @reiteration_delay = @@reiteration_start_delay
631
+ @retries = 0
632
+ # close current HTTP(S) connection on 5xx, errors from list and 4xx errors
633
+ @close_on_error = params[:close_on_error].nil? ? @@close_on_error : params[:close_on_error]
634
+ @close_on_4xx_probability = params[:close_on_4xx_probability] || @@close_on_4xx_probability
635
+ end
636
+
637
+ # Returns false if
638
+ def check(request) #:nodoc:
639
+ result = false
640
+ error_found = false
641
+ redirect_detected= false
642
+ error_match = nil
643
+ last_errors_text = ''
644
+ response = @aws.last_response
645
+ # log error
646
+ request_text_data = "#{request[:protocol]}://#{request[:server]}:#{request[:port]}#{request[:request].path}"
647
+ # is this a redirect?
648
+ # yes!
649
+ if response.is_a?(Net::HTTPRedirection)
650
+ redirect_detected = true
651
+ else
652
+ # no, it's an error ...
653
+ @aws.logger.warn("##### #{@aws.class.name} returned an error: #{response.code} #{response.message}\n#{response.body} #####")
654
+ @aws.logger.warn("##### #{@aws.class.name} request: #{request_text_data} ####")
655
+ end
656
+
657
+ # Extract error/redirection message from the response body
658
+ # Amazon claims that a redirection must have a body but somethimes it is nil....
659
+ if response.body && response.body[/^(<\?xml|<ErrorResponse)/]
660
+ error_parser = RightErrorResponseParser.new
661
+ @aws.class.bench_xml.add! do
662
+ error_parser.parse(response.body)
663
+ end
664
+ @aws.last_errors = error_parser.errors
665
+ @aws.last_request_id = error_parser.requestID
666
+ last_errors_text = @aws.last_errors.flatten.join("\n")
667
+ else
668
+ @aws.last_errors = [[response.code, "#{response.message} (#{request_text_data})"]]
669
+ @aws.last_request_id = '-undefined-'
670
+ last_errors_text = response.message
671
+ end
672
+
673
+ # Ok, it is a redirect, find the new destination location
674
+ if redirect_detected
675
+ location = response['location']
676
+ # ... log information and ...
677
+ @aws.logger.info("##### #{@aws.class.name} redirect requested: #{response.code} #{response.message} #####")
678
+ @aws.logger.info(" Old location: #{request_text_data}")
679
+ @aws.logger.info(" New location: #{location}")
680
+ # ... fix the connection data
681
+ request[:server] = URI.parse(location).host
682
+ request[:protocol] = URI.parse(location).scheme
683
+ request[:port] = URI.parse(location).port
684
+ else
685
+ # Not a redirect but an error: try to find the error in our list
686
+ @errors_list.each do |error_to_find|
687
+ if last_errors_text[/#{error_to_find}/i]
688
+ error_found = true
689
+ error_match = error_to_find
690
+ @aws.logger.warn("##### Retry is needed, error pattern match: #{error_to_find} #####")
691
+ break
692
+ end
693
+ end
694
+ end
695
+
696
+ # check the time has gone from the first error come
697
+ if redirect_detected || error_found
698
+ # Close the connection to the server and recreate a new one.
699
+ # It may have a chance that one server is a semi-down and reconnection
700
+ # will help us to connect to the other server
701
+ if !redirect_detected && @close_on_error
702
+ @aws.connection.finish "#{self.class.name}: error match to pattern '#{error_match}'"
703
+ end
704
+
705
+ if (Time.now < @stop_at)
706
+ @retries += 1
707
+ unless redirect_detected
708
+ @aws.logger.warn("##### Retry ##{@retries} is being performed. Sleeping for #{@reiteration_delay} sec. Whole time: #{Time.now-@started_at} sec ####")
709
+ sleep @reiteration_delay
710
+ @reiteration_delay *= 2
711
+
712
+ # Always make sure that the fp is set to point to the beginning(?)
713
+ # of the File/IO. TODO: it assumes that offset is 0, which is bad.
714
+ if(request[:request].body_stream && request[:request].body_stream.respond_to?(:pos))
715
+ begin
716
+ request[:request].body_stream.pos = 0
717
+ rescue Exception => e
718
+ @logger.warn("Retry may fail due to unable to reset the file pointer" +
719
+ " -- #{self.class.name} : #{e.inspect}")
720
+ end
721
+ end
722
+ else
723
+ @aws.logger.info("##### Retry ##{@retries} is being performed due to a redirect. ####")
724
+ end
725
+ result = @aws.request_info(request, @parser)
726
+ else
727
+ @aws.logger.warn("##### Ooops, time is over... ####")
728
+ end
729
+ # aha, this is unhandled error:
730
+ elsif @close_on_error
731
+ # Is this a 5xx error ?
732
+ if @aws.last_response.code.to_s[/^5\d\d$/]
733
+ @aws.connection.finish "#{self.class.name}: code: #{@aws.last_response.code}: '#{@aws.last_response.message}'"
734
+ # Is this a 4xx error ?
735
+ elsif @aws.last_response.code.to_s[/^4\d\d$/] && @close_on_4xx_probability > rand(100)
736
+ @aws.connection.finish "#{self.class.name}: code: #{@aws.last_response.code}: '#{@aws.last_response.message}', " +
737
+ "probability: #{@close_on_4xx_probability}%"
738
+ end
739
+ end
740
+ result
741
+ end
742
+
743
+ end
744
+
745
+
746
+ #-----------------------------------------------------------------
747
+
748
+ class RightSaxParserCallback #:nodoc:
749
+ def self.include_callback
750
+ include XML::SaxParser::Callbacks
751
+ end
752
+ def initialize(right_aws_parser)
753
+ @right_aws_parser = right_aws_parser
754
+ end
755
+ def on_start_element(name, attr_hash)
756
+ @right_aws_parser.tag_start(name, attr_hash)
757
+ end
758
+ def on_characters(chars)
759
+ @right_aws_parser.text(chars)
760
+ end
761
+ def on_end_element(name)
762
+ @right_aws_parser.tag_end(name)
763
+ end
764
+ def on_start_document; end
765
+ def on_comment(msg); end
766
+ def on_processing_instruction(target, data); end
767
+ def on_cdata_block(cdata); end
768
+ def on_end_document; end
769
+ end
770
+
771
+ class RightAWSParser #:nodoc:
772
+ # default parsing library
773
+ DEFAULT_XML_LIBRARY = 'rexml'
774
+ # a list of supported parsers
775
+ @@supported_xml_libs = [DEFAULT_XML_LIBRARY, 'libxml']
776
+
777
+ @@xml_lib = DEFAULT_XML_LIBRARY # xml library name: 'rexml' | 'libxml'
778
+ def self.xml_lib
779
+ @@xml_lib
780
+ end
781
+ def self.xml_lib=(new_lib_name)
782
+ @@xml_lib = new_lib_name
783
+ end
784
+
785
+ attr_accessor :result
786
+ attr_reader :xmlpath
787
+ attr_accessor :xml_lib
788
+ attr_reader :full_tag_name
789
+ attr_reader :tag
790
+
791
+ def initialize(params={})
792
+ @xmlpath = ''
793
+ @full_tag_name = ''
794
+ @result = false
795
+ @text = ''
796
+ @tag = ''
797
+ @xml_lib = params[:xml_lib] || @@xml_lib
798
+ @logger = params[:logger]
799
+ reset
800
+ end
801
+ def tag_start(name, attributes)
802
+ @text = ''
803
+ @tag = name
804
+ @full_tag_name += @full_tag_name.empty? ? name : "/#{name}"
805
+ tagstart(name, attributes)
806
+ @xmlpath = @full_tag_name
807
+ end
808
+ def tag_end(name)
809
+ @xmlpath = @full_tag_name[/^(.*?)\/?#{name}$/] && $1
810
+ tagend(name)
811
+ @full_tag_name = @xmlpath
812
+ end
813
+ def text(text)
814
+ @text += text
815
+ tagtext(text)
816
+ end
817
+ # Parser method.
818
+ # Params:
819
+ # xml_text - xml message text(String) or Net:HTTPxxx instance (response)
820
+ # params[:xml_lib] - library name: 'rexml' | 'libxml'
821
+ def parse(xml_text, params={})
822
+ # Get response body
823
+ xml_text = xml_text.body unless xml_text.is_a?(String)
824
+ @xml_lib = params[:xml_lib] || @xml_lib
825
+ # check that we had no problems with this library otherwise use default
826
+ @xml_lib = DEFAULT_XML_LIBRARY unless @@supported_xml_libs.include?(@xml_lib)
827
+ # load xml library
828
+ if @xml_lib=='libxml' && !defined?(XML::SaxParser)
829
+ begin
830
+ require 'xml/libxml'
831
+ # is it new ? - Setup SaxParserCallback
832
+ if XML::Parser::VERSION >= '0.5.1.0'
833
+ RightSaxParserCallback.include_callback
834
+ end
835
+ rescue LoadError => e
836
+ @@supported_xml_libs.delete(@xml_lib)
837
+ @xml_lib = DEFAULT_XML_LIBRARY
838
+ if @logger
839
+ @logger.error e.inspect
840
+ @logger.error e.backtrace
841
+ @logger.info "Can not load 'libxml' library. '#{DEFAULT_XML_LIBRARY}' is used for parsing."
842
+ end
843
+ end
844
+ end
845
+ # Parse the xml text
846
+ case @xml_lib
847
+ when 'libxml'
848
+ xml = XML::SaxParser.new
849
+ xml.string = xml_text
850
+ # check libxml-ruby version
851
+ if XML::Parser::VERSION >= '0.5.1.0'
852
+ xml.callbacks = RightSaxParserCallback.new(self)
853
+ else
854
+ xml.on_start_element{|name, attr_hash| self.tag_start(name, attr_hash)}
855
+ xml.on_characters{ |text| self.text(text)}
856
+ xml.on_end_element{ |name| self.tag_end(name)}
857
+ end
858
+ xml.parse
859
+ else
860
+ REXML::Document.parse_stream(xml_text, self)
861
+ end
862
+ end
863
+ # Parser must have a lots of methods
864
+ # (see /usr/lib/ruby/1.8/rexml/parsers/streamparser.rb)
865
+ # We dont need most of them in RightAWSParser and method_missing helps us
866
+ # to skip their definition
867
+ def method_missing(method, *params)
868
+ # if the method is one of known - just skip it ...
869
+ return if [:comment, :attlistdecl, :notationdecl, :elementdecl,
870
+ :entitydecl, :cdata, :xmldecl, :attlistdecl, :instruction,
871
+ :doctype].include?(method)
872
+ # ... else - call super to raise an exception
873
+ super(method, params)
874
+ end
875
+ # the functions to be overriden by children (if nessesery)
876
+ def reset ; end
877
+ def tagstart(name, attributes); end
878
+ def tagend(name) ; end
879
+ def tagtext(text) ; end
880
+ end
881
+
882
+ #-----------------------------------------------------------------
883
+ # PARSERS: Errors
884
+ #-----------------------------------------------------------------
885
+
886
+ #<Error>
887
+ # <Code>TemporaryRedirect</Code>
888
+ # <Message>Please re-send this request to the specified temporary endpoint. Continue to use the original request endpoint for future requests.</Message>
889
+ # <RequestId>FD8D5026D1C5ABA3</RequestId>
890
+ # <Endpoint>bucket-for-k.s3-external-3.amazonaws.com</Endpoint>
891
+ # <HostId>ItJy8xPFPli1fq/JR3DzQd3iDvFCRqi1LTRmunEdM1Uf6ZtW2r2kfGPWhRE1vtaU</HostId>
892
+ # <Bucket>bucket-for-k</Bucket>
893
+ #</Error>
894
+
895
+ class RightErrorResponseParser < RightAWSParser #:nodoc:
896
+ attr_accessor :errors # array of hashes: error/message
897
+ attr_accessor :requestID
898
+ # attr_accessor :endpoint, :host_id, :bucket
899
+ def tagend(name)
900
+ case name
901
+ when 'RequestID' ; @requestID = @text
902
+ when 'Code' ; @code = @text
903
+ when 'Message' ; @message = @text
904
+ # when 'Endpoint' ; @endpoint = @text
905
+ # when 'HostId' ; @host_id = @text
906
+ # when 'Bucket' ; @bucket = @text
907
+ when 'Error' ; @errors << [ @code, @message ]
908
+ end
909
+ end
910
+ def reset
911
+ @errors = []
912
+ end
913
+ end
914
+
915
+ # Dummy parser - does nothing
916
+ # Returns the original params back
917
+ class RightDummyParser # :nodoc:
918
+ attr_accessor :result
919
+ def parse(response, params={})
920
+ @result = [response, params]
921
+ end
922
+ end
923
+
924
+ class RightHttp2xxParser < RightAWSParser # :nodoc:
925
+ def parse(response)
926
+ @result = response.is_a?(Net::HTTPSuccess)
927
+ end
928
+ end
929
+
930
+ end
931
+