right_rackspace 0.0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +6 -0
- data/Manifest.txt +11 -0
- data/README.txt +46 -0
- data/Rakefile +24 -0
- data/lib/benchmark_fix.rb +39 -0
- data/lib/rackspace.rb +744 -0
- data/lib/rackspace_base.rb +528 -0
- data/lib/right_rackspace.rb +46 -0
- data/lib/support.rb +124 -0
- data/test/_test_credentials.rb +41 -0
- data/test/test_right_rackspace.rb +301 -0
- metadata +100 -0
@@ -0,0 +1,528 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2009 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 Rightscale
|
25
|
+
module Rackspace
|
26
|
+
|
27
|
+
class BenchmarkingBlock #:nodoc:
|
28
|
+
attr_accessor :parser, :service
|
29
|
+
def initialize
|
30
|
+
# Benchmark::Tms instance for service access benchmarking.
|
31
|
+
@service = Benchmark::Tms.new()
|
32
|
+
# Benchmark::Tms instance for parsing benchmarking.
|
33
|
+
@parser = Benchmark::Tms.new()
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Interface
|
38
|
+
DEFAULT_AUTH_ENDPOINT = "https://auth.api.rackspacecloud.com/v1.0"
|
39
|
+
DEFAULT_LIMIT = 1000
|
40
|
+
|
41
|
+
@@rackspace_problems = []
|
42
|
+
# A Regexps of errors say the Rackspace has issues and
|
43
|
+
# a request is likely to be repeated
|
44
|
+
def self.rackspace_problems
|
45
|
+
@@rackspace_problems
|
46
|
+
end
|
47
|
+
|
48
|
+
@@bench = Rightscale::Rackspace::BenchmarkingBlock.new
|
49
|
+
def self.bench
|
50
|
+
@@bench
|
51
|
+
end
|
52
|
+
|
53
|
+
@@params = {}
|
54
|
+
def self.params
|
55
|
+
@@params
|
56
|
+
end
|
57
|
+
|
58
|
+
def params
|
59
|
+
@params
|
60
|
+
end
|
61
|
+
|
62
|
+
def merged_params #:nodoc:
|
63
|
+
@@params.merge(@params)
|
64
|
+
end
|
65
|
+
|
66
|
+
@@caching = false
|
67
|
+
|
68
|
+
attr_accessor :username
|
69
|
+
attr_accessor :auth_key
|
70
|
+
attr_reader :logged_in
|
71
|
+
attr_reader :auth_headers
|
72
|
+
attr_reader :auth_token
|
73
|
+
attr_accessor :auth_endpoint
|
74
|
+
attr_accessor :service_endpoint
|
75
|
+
attr_accessor :last_request
|
76
|
+
attr_accessor :last_response
|
77
|
+
attr_accessor :last_error
|
78
|
+
attr_reader :logger
|
79
|
+
attr_reader :cache
|
80
|
+
|
81
|
+
# Parses an endpoint and returns a hash of data
|
82
|
+
def endpoint_to_host_data(endpoint)# :nodoc:
|
83
|
+
service = URI.parse(endpoint).path
|
84
|
+
service.chop! if service[/\/$/] # remove a trailing '/'
|
85
|
+
{ :server => URI.parse(endpoint).host,
|
86
|
+
:service => service,
|
87
|
+
:protocol => URI.parse(endpoint).scheme,
|
88
|
+
:port => URI.parse(endpoint).port }
|
89
|
+
end
|
90
|
+
|
91
|
+
# Create new Rackspace interface handle.
|
92
|
+
#
|
93
|
+
# Params:
|
94
|
+
# :logger - a logger object
|
95
|
+
# :caching - enabled/disables RightRackspace caching on level (only for list_xxx calls)
|
96
|
+
# :verbose_errors - verbose error messages
|
97
|
+
# :auth_endpoint - an auth endpoint URL ()
|
98
|
+
# :service_endpoint - a service endpoint URL
|
99
|
+
# callbacks:
|
100
|
+
# :on_request(self, request_hash) - every time before the request
|
101
|
+
# :on_response(self) - every time the response comes
|
102
|
+
# :on_error(self, error_message) - every time the response is not 2xx | 304
|
103
|
+
# :on_success(self) - once after the successfull response
|
104
|
+
# :on_login(self) - before login
|
105
|
+
# :on_login_success(self) - when login is successfull
|
106
|
+
# :on_login_failure(self) - when login fails
|
107
|
+
# :on_failure(self) - once after the unsuccessfull response
|
108
|
+
#
|
109
|
+
# Handle creation:
|
110
|
+
#
|
111
|
+
# # Just pass your username and youe key
|
112
|
+
# rackspace = Rightscale::Rackspace::Interface::new('uw1...cct', '99b0...047d')
|
113
|
+
#
|
114
|
+
# # The username and the key are in ENV vars: RACKSPACE_USERNAME & RACKSPACE_AUTH_KEY
|
115
|
+
# rackspace = Rightscale::Rackspace::Interface::new
|
116
|
+
#
|
117
|
+
# # Specify the auth endpoint and the service endpoint explicitly. Make the error
|
118
|
+
# # messages as verbose as possible.
|
119
|
+
# rackspace = Rightscale::Rackspace::Interface::new('uw1...cct', '99b0...047d',
|
120
|
+
# :auth_endpoint => 'https://api.mosso.com/auth',
|
121
|
+
# :service_point => 'https://servers.api.rackspacecloud.com/v1.0/413609',
|
122
|
+
# :verbose_errors => true )
|
123
|
+
#
|
124
|
+
# # Fix params after the handle creation:
|
125
|
+
# rackspace = Rightscale::Rackspace::Interface::new('uw1...cct', '99b0...047d')
|
126
|
+
# rackspace.params[:verbose_errors] = true
|
127
|
+
#
|
128
|
+
# Calbacks handling:
|
129
|
+
#
|
130
|
+
# # On response calback
|
131
|
+
# on_response = Proc.new do |handle|
|
132
|
+
# puts ">> response headers: #{handle.last_response.to_hash.inspect}"
|
133
|
+
# end
|
134
|
+
#
|
135
|
+
# # On error calback
|
136
|
+
# on_error = Proc.new do |handle, error_message|
|
137
|
+
# puts ">> Error: #{error_message}"
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# # Create a handle
|
141
|
+
# rackspace = Rightscale::Rackspace::Interface::new('uw1...cct', '99b0...047d',
|
142
|
+
# :on_response => on_response,
|
143
|
+
# :on_error => on_error)
|
144
|
+
#
|
145
|
+
def initialize(username=nil, auth_key=nil, params={})
|
146
|
+
@params = params
|
147
|
+
# Auth data
|
148
|
+
@username = username || ENV['RACKSPACE_USERNAME']
|
149
|
+
@auth_key = auth_key || ENV['RACKSPACE_AUTH_KEY']
|
150
|
+
@logged_in = false
|
151
|
+
# Auth host
|
152
|
+
@auth_headers = {} # a set of headers is returned on authentification coplete
|
153
|
+
@auth_endpoint = ENV['RACKSPACE_AUTH_ENDPOINT'] || params[:auth_endpoint] || DEFAULT_AUTH_ENDPOINT
|
154
|
+
@auth_endpoint_data = endpoint_to_host_data(@auth_endpoint)
|
155
|
+
# Logger
|
156
|
+
@logger = @params[:logger] || (defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER) || Logger.new(STDOUT)
|
157
|
+
# Request and response
|
158
|
+
@last_request = nil
|
159
|
+
@last_response = nil
|
160
|
+
# cache
|
161
|
+
@cache = {}
|
162
|
+
end
|
163
|
+
|
164
|
+
# Generate a request.
|
165
|
+
#
|
166
|
+
# opts:
|
167
|
+
# :body - String
|
168
|
+
# :endpoint_data - Hash
|
169
|
+
# :no_service_path - bool
|
170
|
+
# :headers - a hash or an array of HTTP headers
|
171
|
+
# :vars - a hash or an array of URL variables
|
172
|
+
#
|
173
|
+
def generate_request(verb, path='', opts={}) #:nodoc:
|
174
|
+
# Form a valid http verb: 'Get', 'Post', 'Put', 'Delete'
|
175
|
+
verb = verb.to_s.capitalize
|
176
|
+
raise "Unsupported HTTP verb #{verb.inspect}!" unless verb[/^(Get|Post|Put|Delete)$/]
|
177
|
+
# Select an endpoint
|
178
|
+
endpoint_data = (opts[:endpoint_data] || @service_endpoint_data).dup
|
179
|
+
# Fix a path
|
180
|
+
path = "/#{path}" if !path.blank? && !path[/^\//]
|
181
|
+
# Request variables
|
182
|
+
request_params = opts[:vars].to_a.map do |key, value|
|
183
|
+
key = key.to_s.downcase
|
184
|
+
# Make sure we do not pass a Time object instead of integer for 'changes-since'
|
185
|
+
value = value.to_i if key == 'changes-since'
|
186
|
+
"#{URI.escape(key)}=#{URI.escape(value.to_s)}"
|
187
|
+
end.join('&')
|
188
|
+
# Build a request final path
|
189
|
+
service = opts[:no_service_path] ? '' : endpoint_data[:service]
|
190
|
+
request_path = "#{service}#{path}"
|
191
|
+
request_path = '/' if request_path.blank?
|
192
|
+
request_path += "?#{request_params}" unless request_params.blank?
|
193
|
+
# Create a request
|
194
|
+
request = eval("Net::HTTP::#{verb}").new(request_path)
|
195
|
+
request.body = opts[:body] if opts[:body]
|
196
|
+
# Set headers
|
197
|
+
opts[:headers].to_a.each do |key, value|
|
198
|
+
key = key.to_s.downcase
|
199
|
+
# make sure 'if-modified-since' is always httpdated
|
200
|
+
if key == 'if-modified-since'
|
201
|
+
value = Time.at(value) if value.is_a?(Fixnum)
|
202
|
+
value = value.utc.httpdate if value.is_a?(Time)
|
203
|
+
end
|
204
|
+
request[key] = value.to_s
|
205
|
+
end
|
206
|
+
request['content-type'] ||= 'application/json'
|
207
|
+
request['accept'] = 'application/json'
|
208
|
+
# prepare output hash
|
209
|
+
endpoint_data.merge(:request => request)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Just requests a remote end
|
213
|
+
def internal_request_info(request_hash) #:nodoc:
|
214
|
+
on_event(:on_request, request_hash)
|
215
|
+
@connection ||= Rightscale::HttpConnection.new(:exception => Error, :logger => @logger)
|
216
|
+
@last_request = request_hash[:request]
|
217
|
+
@@bench.service.add!{ @last_response = @connection.request(request_hash) }
|
218
|
+
on_event(:on_response)
|
219
|
+
end
|
220
|
+
|
221
|
+
# Request a remote end and process any errors is found
|
222
|
+
def request_info(request_hash) #:nodoc:
|
223
|
+
internal_request_info(request_hash)
|
224
|
+
result = nil
|
225
|
+
# check response for success...
|
226
|
+
case @last_response.code
|
227
|
+
when /^2..|304/ # SUCCESS
|
228
|
+
@error_handler = nil
|
229
|
+
on_event(:on_success)
|
230
|
+
|
231
|
+
# Cache hit: Cached
|
232
|
+
case @last_response.code
|
233
|
+
when '304' # 'changes-since' param
|
234
|
+
raise NoChange.new("NotModified: '#{simple_path(@last_request.path)}' has not changed since the requested time.")
|
235
|
+
when '203' # 'if-modified-since' header
|
236
|
+
# TODO: Mhhh... It seems Rackspace updates 'last-modified' header every 60 seconds or something even if nothing has changed
|
237
|
+
if @last_response.body.blank?
|
238
|
+
cached_path = cached_path(@last_request.path)
|
239
|
+
last_modified = @last_response['last-modified'].first
|
240
|
+
message_header = merged_params[:caching] &&
|
241
|
+
@cache[cached_path] &&
|
242
|
+
@cache[cached_path][:last_modified_at] == last_modified ? 'Cached' : 'NotModified'
|
243
|
+
# 203 + an empty response body means we asked whether the value did not change and it hit
|
244
|
+
raise NoChange.new("#{message_header}: '#{cached_path}' has not changed since #{last_modified}.")
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Parse a response body. If the body is empty the return +true+
|
249
|
+
@@bench.parser.add! do
|
250
|
+
result = if @last_response.body.blank? then true
|
251
|
+
else
|
252
|
+
case @last_response['content-type'].first
|
253
|
+
when 'application/json' then JSON::parse(@last_response.body)
|
254
|
+
else @last_response.body
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
else # ERROR
|
259
|
+
@last_error = HttpErrorHandler::extract_error_description(@last_response, merged_params[:verbose_errors])
|
260
|
+
on_event(:on_error, @last_error)
|
261
|
+
@error_handler ||= HttpErrorHandler.new(self, :errors_list => self.class.rackspace_problems)
|
262
|
+
result = @error_handler.check(request_hash)
|
263
|
+
@error_handler = nil
|
264
|
+
if result.nil?
|
265
|
+
on_event(:on_failure)
|
266
|
+
raise Error.new(@last_error)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
result
|
270
|
+
rescue
|
271
|
+
@error_handler = nil
|
272
|
+
raise
|
273
|
+
end
|
274
|
+
|
275
|
+
# simple_path('/v1.0/123456/images/detail?var1=var2') #=> '/images/detail?var1=var2'
|
276
|
+
def simple_path(path) # :nodoc:
|
277
|
+
(path[/^#{@service_endpoint_data[:service]}(.*)/] && $1) || path
|
278
|
+
end
|
279
|
+
|
280
|
+
# simple_path('/v1.0/123456/images/detail?var1=var2') #=> '/images/detail'
|
281
|
+
def cached_path(path) # :nodoc:
|
282
|
+
simple_path(path)[/([^?]*)/] && $1
|
283
|
+
end
|
284
|
+
|
285
|
+
# detailed_path('/images', true) #=> '/images/detail'
|
286
|
+
def detailed_path(path, options) # :nodoc:
|
287
|
+
"#{path}#{options[:detail] ? '/detail' : ''}"
|
288
|
+
end
|
289
|
+
|
290
|
+
# Authenticate a user.
|
291
|
+
# Params: +soft+ is used for auto-authentication when auth_token expires. Soft auth
|
292
|
+
# do not overrides @last_request and @last_response attributes (are needed for a proper
|
293
|
+
# error handling) on success.
|
294
|
+
def authenticate(opts={}) # :nodoc:
|
295
|
+
@logged_in = false
|
296
|
+
@auth_headers = {}
|
297
|
+
opts = opts.dup
|
298
|
+
opts[:endpoint_data] = @auth_endpoint_data
|
299
|
+
(opts[:headers] ||= {}).merge!({ 'x-auth-user' => @username, 'x-auth-key' => @auth_key })
|
300
|
+
request_data = generate_request( :get, '', opts )
|
301
|
+
on_event(:on_login)
|
302
|
+
internal_request_info(request_data)
|
303
|
+
unless @last_response.is_a?(Net::HTTPSuccess)
|
304
|
+
@error_handler = nil
|
305
|
+
@last_error = HttpErrorHandler::extract_error_description(@last_response, merged_params[:verbose_errors])
|
306
|
+
on_event(:on_error, @last_error)
|
307
|
+
on_event(:on_login_failure)
|
308
|
+
on_event(:on_failure)
|
309
|
+
raise Error.new(@last_error)
|
310
|
+
end
|
311
|
+
# Store all auth response headers
|
312
|
+
@auth_headers = @last_response.to_hash
|
313
|
+
@auth_token = @auth_headers['x-auth-token'].first
|
314
|
+
# Service endpoint
|
315
|
+
@service_endpoint = merged_params[:service_endpoint] || @auth_headers['x-server-management-url'].first
|
316
|
+
@service_endpoint_data = endpoint_to_host_data(@service_endpoint)
|
317
|
+
@logged_in = true
|
318
|
+
on_event(:on_login_success)
|
319
|
+
true
|
320
|
+
end
|
321
|
+
|
322
|
+
# Incrementally lists something.
|
323
|
+
def incrementally_list_resources(verb, path, offset=nil, limit=nil, opts={}, &block) # :nodoc:
|
324
|
+
opts = opts.dup
|
325
|
+
opts[:vars] ||= {}
|
326
|
+
opts[:vars]['offset'] = offset || 0
|
327
|
+
opts[:vars]['limit'] = limit || DEFAULT_LIMIT
|
328
|
+
# Get a resource name by path:
|
329
|
+
# '/images' -> 'images'
|
330
|
+
# '/shared_ip_groups/detail' -> 'sharedIpGroups'
|
331
|
+
resource_name = ''
|
332
|
+
(path[%r{^/([^/]*)}] && $1).split('_').each_with_index do |w, i|
|
333
|
+
resource_name += (i==0 ? w.downcase : w.capitalize)
|
334
|
+
end
|
335
|
+
result = { resource_name => []}
|
336
|
+
loop do
|
337
|
+
# begin
|
338
|
+
response = api(verb, path, opts)
|
339
|
+
result[resource_name] += response[resource_name]
|
340
|
+
# rescue Rightscale::Rackspace::Error => e
|
341
|
+
# raise e unless e.message[/itemNotFound/]
|
342
|
+
# response = nil
|
343
|
+
# end
|
344
|
+
break if response.blank? ||
|
345
|
+
(response[resource_name].blank?) ||
|
346
|
+
(block && !block.call(response)) ||
|
347
|
+
(response[resource_name].size < opts[:vars]['limit'])
|
348
|
+
opts[:vars]['offset'] += opts[:vars]['limit']
|
349
|
+
end
|
350
|
+
result
|
351
|
+
end
|
352
|
+
|
353
|
+
# Call Rackspace. Caching is not used.
|
354
|
+
def api(verb, path='', options={}) # :nodoc:
|
355
|
+
login unless @logged_in
|
356
|
+
options[:headers] ||= {}
|
357
|
+
options[:headers]['x-auth-token'] = @auth_token
|
358
|
+
request_info(generate_request(verb, path, options))
|
359
|
+
end
|
360
|
+
|
361
|
+
# Call Rackspace. Use cache if possible
|
362
|
+
# opts:
|
363
|
+
# :incrementally - use incrementally list to get the whole list of items
|
364
|
+
# otherwise it will get max DEFAULT_LIMIT items (PS API call must support pagination)
|
365
|
+
#
|
366
|
+
def api_or_cache(verb, path, options={}) # :nodoc:
|
367
|
+
use_caching = merged_params[:caching] && options[:vars].blank?
|
368
|
+
cache_record = use_caching && @cache[path]
|
369
|
+
# Create a proc object to avoid a code duplication
|
370
|
+
proc = Proc.new do
|
371
|
+
if options[:incrementally]
|
372
|
+
incrementally_list_resources(verb, path, nil, nil, options)
|
373
|
+
else api(verb, path, options)
|
374
|
+
end
|
375
|
+
end
|
376
|
+
# The cache is not used or record is not found
|
377
|
+
unless cache_record
|
378
|
+
response = proc.call
|
379
|
+
if use_caching
|
380
|
+
last_modified_at = @last_response.to_hash['last-modified'].first
|
381
|
+
update_cache(path, last_modified_at, response)
|
382
|
+
end
|
383
|
+
response
|
384
|
+
else
|
385
|
+
# Record found - ask Rackspace whether it changed or not since last update
|
386
|
+
options = options.dup
|
387
|
+
options[:headers] ||= {}
|
388
|
+
options[:headers]['if-modified-since'] = cache_record[:last_modified_at]
|
389
|
+
proc.call
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
def update_cache(path, last_modified_at, data) #:nodoc:
|
394
|
+
@cache[path] ||= {}
|
395
|
+
@cache[path][:last_modified_at] = last_modified_at
|
396
|
+
@cache[path][:data] = data
|
397
|
+
end
|
398
|
+
|
399
|
+
# Events (callbacks) for logging and debugging features.
|
400
|
+
# These callbacks do not catch low level connection errors that are handled by RightHttpConnection but
|
401
|
+
# only HTTP errors.
|
402
|
+
def on_event(event, *params) #:nodoc:
|
403
|
+
self.merged_params[event].call(self, *params) if self.merged_params[event].kind_of?(Proc)
|
404
|
+
end
|
405
|
+
|
406
|
+
end
|
407
|
+
|
408
|
+
#------------------------------------------------------------
|
409
|
+
# Error handling
|
410
|
+
#------------------------------------------------------------
|
411
|
+
|
412
|
+
class NoChange < RuntimeError
|
413
|
+
end
|
414
|
+
|
415
|
+
class Error < RuntimeError
|
416
|
+
end
|
417
|
+
|
418
|
+
class HttpErrorHandler # :nodoc:
|
419
|
+
|
420
|
+
# Receiving these codes we have to reauthenticate at Rackspace
|
421
|
+
REAUTHENTICATE_ON = ['401']
|
422
|
+
# Some error are too ennoing to be logged: '404' comes very often when one calls
|
423
|
+
# incrementally_list_something
|
424
|
+
# SKIP_LOGGING_ON = ['404']
|
425
|
+
SKIP_LOGGING_ON = []
|
426
|
+
|
427
|
+
@@reiteration_start_delay = 0.2
|
428
|
+
def self.reiteration_start_delay
|
429
|
+
@@reiteration_start_delay
|
430
|
+
end
|
431
|
+
def self.reiteration_start_delay=(reiteration_start_delay)
|
432
|
+
@@reiteration_start_delay = reiteration_start_delay
|
433
|
+
end
|
434
|
+
@@reiteration_time = 5
|
435
|
+
def self.reiteration_time
|
436
|
+
@@reiteration_time
|
437
|
+
end
|
438
|
+
def self.reiteration_time=(reiteration_time)
|
439
|
+
@@reiteration_time = reiteration_time
|
440
|
+
end
|
441
|
+
|
442
|
+
# Format a response error message.
|
443
|
+
def self.extract_error_description(response, verbose=false) #:nodoc:
|
444
|
+
message = nil
|
445
|
+
Interface::bench.parser.add! do
|
446
|
+
message = begin
|
447
|
+
if response.body[/^<!DOCTYPE HTML PUBLIC/] then response.message
|
448
|
+
else
|
449
|
+
JSON::parse(response.body).to_a.map do |k,v|
|
450
|
+
"#{k}: #{v['message']}" + (verbose ? "\n#{v['details']}" : "")
|
451
|
+
end.join("\n")
|
452
|
+
end
|
453
|
+
rescue
|
454
|
+
response.message
|
455
|
+
end
|
456
|
+
end
|
457
|
+
"#{response.code}: #{message}"
|
458
|
+
end
|
459
|
+
|
460
|
+
# params:
|
461
|
+
# :reiteration_time
|
462
|
+
# :errors_list
|
463
|
+
def initialize(handle, params={}) #:nodoc:
|
464
|
+
@handle = handle # Link to RightEc2 | RightSqs | RightS3 instance
|
465
|
+
@started_at = Time.now
|
466
|
+
@stop_at = @started_at + (params[:reiteration_time] || @@reiteration_time)
|
467
|
+
@errors_list = params[:errors_list] || []
|
468
|
+
@reiteration_delay = @@reiteration_start_delay
|
469
|
+
@retries = 0
|
470
|
+
end
|
471
|
+
|
472
|
+
# Process errored response
|
473
|
+
def check(request_hash) #:nodoc:
|
474
|
+
result = nil
|
475
|
+
error_found = false
|
476
|
+
response = @handle.last_response
|
477
|
+
error_message = @handle.last_error
|
478
|
+
# Log the error
|
479
|
+
logger = @handle.logger
|
480
|
+
unless SKIP_LOGGING_ON.include?(response.code)
|
481
|
+
logger.warn("##### #{@handle.class.name} returned an error: #{error_message} #####")
|
482
|
+
logger.warn("##### #{@handle.class.name} request: #{request_hash[:server]}:#{request_hash[:port]}#{request_hash[:request].path} ####")
|
483
|
+
end
|
484
|
+
# now - check the error
|
485
|
+
@errors_list.each do |error_to_find|
|
486
|
+
if error_message[/#{error_to_find}/i]
|
487
|
+
error_found = error_to_find
|
488
|
+
logger.warn("##### Retry is needed, error pattern match: #{error_to_find} #####")
|
489
|
+
break
|
490
|
+
end
|
491
|
+
end
|
492
|
+
# yep, we know this error and have to do a retry when it comes
|
493
|
+
if error_found || REAUTHENTICATE_ON.include?(@handle.last_response.code)
|
494
|
+
# check the time has gone from the first error come
|
495
|
+
# Close the connection to the server and recreate a new one.
|
496
|
+
# It may have a chance that one server is a semi-down and reconnection
|
497
|
+
# will help us to connect to the other server
|
498
|
+
if (Time.now < @stop_at)
|
499
|
+
@retries += 1
|
500
|
+
@handle.logger.warn("##### Retry ##{@retries} is being performed. Sleeping for #{@reiteration_delay} sec. Whole time: #{Time.now-@started_at} sec ####")
|
501
|
+
sleep @reiteration_delay
|
502
|
+
@reiteration_delay *= 2
|
503
|
+
# Always make sure that the fp is set to point to the beginning(?)
|
504
|
+
# of the File/IO. TODO: it assumes that offset is 0, which is bad.
|
505
|
+
if request_hash[:request].body_stream && request_hash[:request].body_stream.respond_to?(:pos)
|
506
|
+
begin
|
507
|
+
request_hash[:request].body_stream.pos = 0
|
508
|
+
rescue Exception => e
|
509
|
+
logger.warn("Retry may fail due to unable to reset the file pointer -- #{self.class.name} : #{e.inspect}")
|
510
|
+
end
|
511
|
+
end
|
512
|
+
# Oops it seems we have been asked about reauthentication..
|
513
|
+
if REAUTHENTICATE_ON.include?(@handle.last_response.code)
|
514
|
+
@handle.authenticate
|
515
|
+
@handle.request_info(request_hash)
|
516
|
+
end
|
517
|
+
# Make another try
|
518
|
+
result = @handle.request_info(request_hash)
|
519
|
+
else
|
520
|
+
logger.warn("##### Ooops, time is over... ####")
|
521
|
+
end
|
522
|
+
end
|
523
|
+
result
|
524
|
+
end
|
525
|
+
|
526
|
+
end
|
527
|
+
end
|
528
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2009 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
|
+
require 'benchmark'
|
25
|
+
require 'net/https'
|
26
|
+
require 'uri'
|
27
|
+
require "base64"
|
28
|
+
require 'rubygems'
|
29
|
+
require 'json'
|
30
|
+
require 'right_http_connection'
|
31
|
+
|
32
|
+
$:.unshift(File.dirname(__FILE__))
|
33
|
+
require 'benchmark_fix'
|
34
|
+
require 'support'
|
35
|
+
require 'rackspace_base'
|
36
|
+
require 'rackspace'
|
37
|
+
|
38
|
+
module RightRackspace #:nodoc:
|
39
|
+
module VERSION #:nodoc:
|
40
|
+
MAJOR = 0
|
41
|
+
MINOR = 0
|
42
|
+
TINY = 0
|
43
|
+
|
44
|
+
STRING = [MAJOR, MINOR, TINY].join('.')
|
45
|
+
end
|
46
|
+
end
|
data/lib/support.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
# If ActiveSupport is loaded, then great - use it. But we don't
|
2
|
+
# want a dependency on it, so if it's not present, define the few
|
3
|
+
# extensions that we want to use...
|
4
|
+
unless defined? ActiveSupport::CoreExtensions
|
5
|
+
# These are ActiveSupport-;like extensions to do a few handy things in the gems
|
6
|
+
# Derived from ActiveSupport, so the AS copyright notice applies:
|
7
|
+
#
|
8
|
+
#
|
9
|
+
#
|
10
|
+
# Copyright (c) 2005 David Heinemeier Hansson
|
11
|
+
#
|
12
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
13
|
+
# a copy of this software and associated documentation files (the
|
14
|
+
# "Software"), to deal in the Software without restriction, including
|
15
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
16
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
17
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
18
|
+
# the following conditions:
|
19
|
+
#
|
20
|
+
# The above copyright notice and this permission notice shall be
|
21
|
+
# included in all copies or substantial portions of the Software.
|
22
|
+
#
|
23
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
24
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
25
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
26
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
27
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
28
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
29
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
30
|
+
#++
|
31
|
+
#
|
32
|
+
#
|
33
|
+
class String #:nodoc:
|
34
|
+
|
35
|
+
# Constantize tries to find a declared constant with the name specified
|
36
|
+
# in the string. It raises a NameError when the name is not in CamelCase
|
37
|
+
# or is not initialized.
|
38
|
+
#
|
39
|
+
# Examples
|
40
|
+
# "Module".constantize #=> Module
|
41
|
+
# "Class".constantize #=> Class
|
42
|
+
def constantize()
|
43
|
+
unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ self
|
44
|
+
raise NameError, "#{self.inspect} is not a valid constant name!"
|
45
|
+
end
|
46
|
+
|
47
|
+
Object.module_eval("::#{$1}", __FILE__, __LINE__)
|
48
|
+
end
|
49
|
+
|
50
|
+
def camelize()
|
51
|
+
self.dup.split(/_/).map{ |word| word.capitalize }.join('')
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
class Object #:nodoc:
|
58
|
+
# "", " ", nil, [], and {} are blank
|
59
|
+
def blank?
|
60
|
+
if respond_to?(:empty?) && respond_to?(:strip)
|
61
|
+
empty? or strip.empty?
|
62
|
+
elsif respond_to?(:empty?)
|
63
|
+
empty?
|
64
|
+
else
|
65
|
+
!self
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class NilClass #:nodoc:
|
71
|
+
def blank?
|
72
|
+
true
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class FalseClass #:nodoc:
|
77
|
+
def blank?
|
78
|
+
true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class TrueClass #:nodoc:
|
83
|
+
def blank?
|
84
|
+
false
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class Array #:nodoc:
|
89
|
+
alias_method :blank?, :empty?
|
90
|
+
end
|
91
|
+
|
92
|
+
class Hash #:nodoc:
|
93
|
+
alias_method :blank?, :empty?
|
94
|
+
|
95
|
+
# Return a new hash with all keys converted to symbols.
|
96
|
+
def symbolize_keys
|
97
|
+
inject({}) do |options, (key, value)|
|
98
|
+
options[key.to_sym] = value
|
99
|
+
options
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class String #:nodoc:
|
105
|
+
def blank?
|
106
|
+
empty? || strip.empty?
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class Numeric #:nodoc:
|
111
|
+
def blank?
|
112
|
+
false
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# We also use String#first from 1.8.7 that doesn't exist in 1.8.6
|
118
|
+
unless String.instance_methods.include? "first"
|
119
|
+
class String
|
120
|
+
def first
|
121
|
+
self
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|