redaranj-right_aws 1.10.3

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