right_rackspace 0.0.0.1
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.
- 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
|