right_cloud_api_base 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/HISTORY +2 -0
- data/LICENSE +19 -0
- data/README.md +14 -0
- data/Rakefile +37 -0
- data/lib/base/api_manager.rb +707 -0
- data/lib/base/helpers/cloud_api_logger.rb +214 -0
- data/lib/base/helpers/http_headers.rb +239 -0
- data/lib/base/helpers/http_parent.rb +103 -0
- data/lib/base/helpers/http_request.rb +173 -0
- data/lib/base/helpers/http_response.rb +122 -0
- data/lib/base/helpers/net_http_patch.rb +31 -0
- data/lib/base/helpers/query_api_patterns.rb +862 -0
- data/lib/base/helpers/support.rb +270 -0
- data/lib/base/helpers/support.xml.rb +306 -0
- data/lib/base/helpers/utils.rb +380 -0
- data/lib/base/manager.rb +122 -0
- data/lib/base/parsers/json.rb +38 -0
- data/lib/base/parsers/plain.rb +36 -0
- data/lib/base/parsers/rexml.rb +83 -0
- data/lib/base/parsers/sax.rb +200 -0
- data/lib/base/routines/cache_validator.rb +184 -0
- data/lib/base/routines/connection_proxies/net_http_persistent_proxy.rb +194 -0
- data/lib/base/routines/connection_proxies/right_http_connection_proxy.rb +224 -0
- data/lib/base/routines/connection_proxy.rb +66 -0
- data/lib/base/routines/request_analyzer.rb +122 -0
- data/lib/base/routines/request_generator.rb +48 -0
- data/lib/base/routines/request_initializer.rb +52 -0
- data/lib/base/routines/response_analyzer.rb +152 -0
- data/lib/base/routines/response_parser.rb +79 -0
- data/lib/base/routines/result_wrapper.rb +75 -0
- data/lib/base/routines/retry_manager.rb +106 -0
- data/lib/base/routines/routine.rb +98 -0
- data/lib/right_cloud_api_base.rb +72 -0
- data/lib/right_cloud_api_base_version.rb +37 -0
- data/right_cloud_api_base.gemspec +63 -0
- data/spec/helpers/query_api_pattern_spec.rb +312 -0
- data/spec/helpers/support_spec.rb +211 -0
- data/spec/helpers/support_xml_spec.rb +207 -0
- data/spec/helpers/utils_spec.rb +179 -0
- data/spec/routines/connection_proxies/test_net_http_persistent_proxy_spec.rb +143 -0
- data/spec/routines/test_cache_validator_spec.rb +152 -0
- data/spec/routines/test_connection_proxy_spec.rb +44 -0
- data/spec/routines/test_request_analyzer_spec.rb +106 -0
- data/spec/routines/test_response_analyzer_spec.rb +132 -0
- data/spec/routines/test_response_parser_spec.rb +228 -0
- data/spec/routines/test_result_wrapper_spec.rb +63 -0
- data/spec/routines/test_retry_manager_spec.rb +84 -0
- data/spec/spec_helper.rb +15 -0
- metadata +215 -0
@@ -0,0 +1,707 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2013 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 NONINFRINGEMENT.
|
18
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
module RightScale
|
25
|
+
module CloudApi
|
26
|
+
|
27
|
+
# The class is the parent class for all the cloud based thread-non-safe managers
|
28
|
+
#
|
29
|
+
# @api public
|
30
|
+
#
|
31
|
+
# It implements all the +generic+ functionalities the cloud specific managers share.
|
32
|
+
#
|
33
|
+
class ApiManager
|
34
|
+
|
35
|
+
# Log filters set by default
|
36
|
+
DEFAULT_LOG_FILTERS = [
|
37
|
+
:connection_proxy,
|
38
|
+
:request_generator,
|
39
|
+
:request_generator_body,
|
40
|
+
:response_analyzer,
|
41
|
+
:response_analyzer_body_error,
|
42
|
+
:response_parser,
|
43
|
+
]
|
44
|
+
|
45
|
+
|
46
|
+
# Default Error
|
47
|
+
class Error < CloudApi::Error
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
# Options reader
|
52
|
+
#
|
53
|
+
# @return [Hash] The list of class level set options.
|
54
|
+
# @example
|
55
|
+
# # no example
|
56
|
+
#
|
57
|
+
def self.options
|
58
|
+
@options ||= {}
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
# Options setter
|
63
|
+
#
|
64
|
+
# @param [Hash] options
|
65
|
+
# @return [void]
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# module RightScale
|
69
|
+
# module CloudApi
|
70
|
+
# module AWS
|
71
|
+
# class ApiManager < CloudApi::ApiManager
|
72
|
+
# set :response_error_parser => Parser::AWS::ResponseErrorV1
|
73
|
+
# set :cache => true
|
74
|
+
# ...
|
75
|
+
# end
|
76
|
+
# end
|
77
|
+
# end
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
def self.set(opts)
|
81
|
+
opts.each { |key, value| self.options[key] = value }
|
82
|
+
nil
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
# Returns a list of routines the manager invokes while processing API request
|
87
|
+
#
|
88
|
+
# @return [Array] An array of RightScale::CloudApi::Routine.
|
89
|
+
# @example
|
90
|
+
# # no example
|
91
|
+
#
|
92
|
+
def self.routines
|
93
|
+
@routines ||= []
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
# Add routine of the given tipe into the list of API processing routines
|
98
|
+
#
|
99
|
+
# @param [RightScale::CloudApi::Routine] routines A set of routines.
|
100
|
+
# @return [Array] The current set of routines.
|
101
|
+
# @example
|
102
|
+
# # no example
|
103
|
+
#
|
104
|
+
def self.set_routine(*new_routines)
|
105
|
+
new_routines.flatten.each do |routine|
|
106
|
+
self.routines << routine
|
107
|
+
# If a routine has ClassMethods module defined then extend the current class with those methods.
|
108
|
+
# We use this to add class level helper methods like: error_pattern or cache_pattern
|
109
|
+
self.extend routine::ClassMethods if defined?(routine::ClassMethods)
|
110
|
+
end
|
111
|
+
self.routines
|
112
|
+
end
|
113
|
+
|
114
|
+
# Return a set of system vars (ignore this attribute)
|
115
|
+
#
|
116
|
+
# @return [Hash]
|
117
|
+
# @example
|
118
|
+
# # no example
|
119
|
+
#
|
120
|
+
attr_reader :data
|
121
|
+
|
122
|
+
# Return a set of system routines (ignore this attribute)
|
123
|
+
#
|
124
|
+
# @return [Array]
|
125
|
+
# @example
|
126
|
+
# # no example
|
127
|
+
#
|
128
|
+
attr_reader :routines
|
129
|
+
|
130
|
+
|
131
|
+
# Constructor
|
132
|
+
#
|
133
|
+
# @param [Hash] credentials Cloud credentials.
|
134
|
+
# @param [String] endpoint API endpoint.
|
135
|
+
# @param [Hash] options A set of options (see below).
|
136
|
+
#
|
137
|
+
# @option options [Boolean] :allow_endpoint_params
|
138
|
+
# When the given endpoint has any set of URL params they will not be ignored but will
|
139
|
+
# be added to every API request.
|
140
|
+
#
|
141
|
+
# @option options [Boolean] :abort_on_timeout
|
142
|
+
# When set to +true+ the gem does not perform a retry call when there is a connection
|
143
|
+
# timeout.
|
144
|
+
#
|
145
|
+
# @option options [String] :api_version
|
146
|
+
# The required cloud API version if it is different from the default one.
|
147
|
+
#
|
148
|
+
# @option options [Class] :api_wrapper
|
149
|
+
# The Query-like API wrapper module that provides a set of handy methods to drive
|
150
|
+
# REST APIs (see {RightScale::CloudApi::Mixin::QueryApiPatterns::ClassMethods})
|
151
|
+
#
|
152
|
+
# @option options [Boolean] :cache
|
153
|
+
# Cache cloud responses when possible so that we don't parse them again if cloud
|
154
|
+
# response does not change (see cloud specific ApiManager definition).
|
155
|
+
#
|
156
|
+
# @option options [Hash] :cloud
|
157
|
+
# A set of cloud specific options. See custom cloud specific ApiManagers for better
|
158
|
+
# explanation.
|
159
|
+
#
|
160
|
+
# @option options [String] :connection_ca_file
|
161
|
+
# CA certificate for SSL connection.
|
162
|
+
#
|
163
|
+
# @option options [Integer] :connection_open_timeout
|
164
|
+
# Connection open timeout (in seconds).
|
165
|
+
#
|
166
|
+
# @option options [String] :connection_proxy
|
167
|
+
# Connection proxy class (when it need to be different from the default one).
|
168
|
+
# Only RightScale::CloudApi::ConnectionProxy::NetHttpPersistentProxy (default) and
|
169
|
+
# RightScale::CloudApi::ConnectionProxy::RightHttpConnectionProxy are supported.
|
170
|
+
# The last one requires 'right_http_connection' gem to be manually installed, and it is
|
171
|
+
# not recommended to use because it monkey patches Net::HTTP.
|
172
|
+
#
|
173
|
+
# @option options [Integer] :connection_read_timeout
|
174
|
+
# Connection read timeout (in seconds).
|
175
|
+
#
|
176
|
+
# @option options [Integer] :connection_retry_count
|
177
|
+
# Max number of retries to when unable to establish a connection to API server
|
178
|
+
#
|
179
|
+
# @option options [Integer] :connection_retry_delay
|
180
|
+
# Defines how long we wait on a low level connection error (in seconds)
|
181
|
+
#
|
182
|
+
# @option options [Hash] :creds
|
183
|
+
# A set of optional extra creds a cloud may require
|
184
|
+
# (see right_cloud_stack_api gem which supports :tenant_name and :tenant_id)
|
185
|
+
#
|
186
|
+
# @option options [Hash] :headers
|
187
|
+
# A set of request headers to be added to every API call.
|
188
|
+
#
|
189
|
+
# @option options [Logger] :logger
|
190
|
+
# Current logger. If is not provided then it logs to STDOUT. When if nil is given it
|
191
|
+
# logs to '/dev/nul'.
|
192
|
+
#
|
193
|
+
# @option options [Symbol] :log_filter_patterns
|
194
|
+
# A set of log filters that define what to log (see {RightScale::CloudApi::CloudApiLogger}).
|
195
|
+
#
|
196
|
+
# @option options [Hash] :params
|
197
|
+
# A set of URL params to be sent with the API request.
|
198
|
+
#
|
199
|
+
# @option options [Boolean,String] :random_token
|
200
|
+
# Some clouds API cache their responses when they receive the same request again
|
201
|
+
# and again, even when we are sure that cloud response mush have changed. To deal
|
202
|
+
# with this we can add a random parameter to an API call to trick the remote API.
|
203
|
+
# When :random_token is set to +true+ it adds an extra param with name 'rsrcarandomtoken'
|
204
|
+
# and a random value to every single API request. When :random_token is a String then
|
205
|
+
# the gem uses it as the random param name.
|
206
|
+
#
|
207
|
+
# @option options [Boolean] :raw_response
|
208
|
+
# By default the gem parses all XML and JSON responses and returns them as ruby Hashes.
|
209
|
+
# Sometimes it is not what one would want (Amazon S3 GetObject for example).
|
210
|
+
# Setting this option to +true+ forces the gem to return a not parsed response.
|
211
|
+
#
|
212
|
+
# @option options [Class] :response_error_parser
|
213
|
+
# API response parser in case of error (when it needs to be different from the default one).
|
214
|
+
#
|
215
|
+
# @option options [Symbol] :xml_parser
|
216
|
+
# XML parser (:sax | :rexml are supported).
|
217
|
+
#
|
218
|
+
# @option options [Proc] :before_process_api_request_callback
|
219
|
+
# The callback is called before every API request (may be helpful when debugging things).
|
220
|
+
#
|
221
|
+
# @option options [Proc] :before_routine_callback
|
222
|
+
# The callback is called before each routine is executed.
|
223
|
+
#
|
224
|
+
# @option options [Proc] :after_routine_callback
|
225
|
+
# The callback is called after each routine is executed.
|
226
|
+
#
|
227
|
+
# @option options [Proc] :after_process_api_request_callback
|
228
|
+
# The callback is called after the API request completion.
|
229
|
+
#
|
230
|
+
# @option options [Proc] :before_retry_callback
|
231
|
+
# The callback is called if a retry attempt is required.
|
232
|
+
#
|
233
|
+
# @option options [Proc] :before_redirect_callback
|
234
|
+
# The callback is called when a redirect is detected.
|
235
|
+
#
|
236
|
+
# @option options [Proc] :stat_data_callback
|
237
|
+
# The callback is called when stat data for the current request is ready.
|
238
|
+
#
|
239
|
+
# @raise [Rightscale::CloudApi::ApiManager::Error]
|
240
|
+
# If no credentials have been set or the endpoint is blank.
|
241
|
+
#
|
242
|
+
# @example
|
243
|
+
# # See cloud specific gems for use case.
|
244
|
+
#
|
245
|
+
# @see Manager
|
246
|
+
#
|
247
|
+
def initialize(credentials, endpoint, options={})
|
248
|
+
@endpoint = endpoint
|
249
|
+
@credentials = credentials.merge!(options[:creds] || {})
|
250
|
+
@credentials.each do |key, value|
|
251
|
+
fail(Error, "Credential #{key.inspect} cannot be empty") unless value
|
252
|
+
end
|
253
|
+
@options = options
|
254
|
+
@options[:cloud] ||= {}
|
255
|
+
@with_options = []
|
256
|
+
@with_headers = {}
|
257
|
+
@routines = []
|
258
|
+
@storage = {}
|
259
|
+
@options[:cloud_api_logger] = RightScale::CloudApi::CloudApiLogger.new(@options , DEFAULT_LOG_FILTERS)
|
260
|
+
# Try to set an API version when possible
|
261
|
+
@options[:api_version] ||= "#{self.class.name}::DEFAULT_API_VERSION"._constantize rescue nil
|
262
|
+
# Load routines
|
263
|
+
routine_classes = (Utils.inheritance_chain(self.class, :routines).select{|rc| !rc._blank?}.last || [])
|
264
|
+
@routines = routine_classes.map{ |routine_class| routine_class.new }
|
265
|
+
# fail Error::new("Credentials must be set") if @credentials._blank?
|
266
|
+
fail Error::new("Endpoint must be set") if @endpoint._blank?
|
267
|
+
# Try to wrap this manager with the handy API methods if possible using [:api_wrapper, :api_version, 'default']
|
268
|
+
# (but do nothing if one explicitly passed :api_wrapper => nil )
|
269
|
+
unless @options.has_key?(:api_wrapper) && @options[:api_wrapper].nil?
|
270
|
+
# And then wrap with the most recent or user's wrapper
|
271
|
+
[ @options[:api_wrapper], @options[:api_version], 'default'].uniq.each do |api_wrapper|
|
272
|
+
break if wrap_api_with(api_wrapper, false)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
|
278
|
+
# Main API request entry point
|
279
|
+
#
|
280
|
+
# @api private
|
281
|
+
#
|
282
|
+
# @param [String,Symbol] verb HTTP verb: :get, :post, :put, :delete, etc.
|
283
|
+
# @param [String] relative_path Relative URI path.
|
284
|
+
# @param [Hash] opts A set of extra options.
|
285
|
+
#
|
286
|
+
# @option options [Hash] :params
|
287
|
+
# A set of URL parameters.
|
288
|
+
#
|
289
|
+
# @option options [Hash] :headers
|
290
|
+
# A set of HTTP headers.
|
291
|
+
#
|
292
|
+
# @option options [Hash] :options
|
293
|
+
# A set of extra options: see {#initialize} method for them.
|
294
|
+
#
|
295
|
+
# @option options [Hash,String] :body
|
296
|
+
# The request body. If Hash is passed then it will convert it into String accordingly to
|
297
|
+
# 'content-type' header.
|
298
|
+
#
|
299
|
+
# @option options [String] :endpoint
|
300
|
+
# An endpoint if it is different from the default one.
|
301
|
+
#
|
302
|
+
# @return [Object]
|
303
|
+
#
|
304
|
+
# @example
|
305
|
+
# # The method should not be used directly: use *api* method instead.
|
306
|
+
#
|
307
|
+
# @yield [String] If a block is given it will call it on every chunk of data received from a socket.
|
308
|
+
#
|
309
|
+
def process_api_request(verb, relative_path, opts={}, &block)
|
310
|
+
# Add a unique-per-request log prefix to every logged line.
|
311
|
+
cloud_api_logger.set_unique_prefix
|
312
|
+
# Initialize @data variable and get a final set of API request options.
|
313
|
+
options = initialize_api_request_options(verb, relative_path, opts, &block)
|
314
|
+
# Before_process_api_request_callback.
|
315
|
+
invoke_callback_method(options[:before_process_api_request_callback], :manager => self)
|
316
|
+
# Main loop
|
317
|
+
loop do
|
318
|
+
# Start a new stat session.
|
319
|
+
stat = {}
|
320
|
+
@data[:stat][:data] << stat
|
321
|
+
# Reset retry attempt flag.
|
322
|
+
retry_attempt = false
|
323
|
+
# Loop through all the required routes.
|
324
|
+
routines.each do |routine|
|
325
|
+
# Start a new stat record for current routine.
|
326
|
+
routine_name = routine.class.name
|
327
|
+
stat[routine_name] = {}
|
328
|
+
stat[routine_name][:started_at] = Time.now.utc
|
329
|
+
begin
|
330
|
+
# Set routine data
|
331
|
+
routine.reset(data)
|
332
|
+
# Before_routine_callback.
|
333
|
+
invoke_callback_method(options[:before_routine_callback],
|
334
|
+
:routine => routine,
|
335
|
+
:manager => self)
|
336
|
+
# Process current routine.
|
337
|
+
routine.process
|
338
|
+
# After_routine_callback.
|
339
|
+
invoke_callback_method(options[:after_routine_callback],
|
340
|
+
:routine => routine,
|
341
|
+
:manager => self)
|
342
|
+
# If current routine reported the API request is done we should stop and skip all the
|
343
|
+
# rest routines
|
344
|
+
break if data[:vars][:system][:done]
|
345
|
+
rescue RetryAttempt
|
346
|
+
invoke_callback_method(options[:after_routine_callback],
|
347
|
+
:routine => routine,
|
348
|
+
:manager => self,
|
349
|
+
:retry => true)
|
350
|
+
# Set a flag that would notify the exterlan main loop there is a retry request received.
|
351
|
+
retry_attempt = true
|
352
|
+
# Break the routines loop and exit into the main one.
|
353
|
+
break
|
354
|
+
ensure
|
355
|
+
# Complete current stat session
|
356
|
+
stat[routine_name][:time_taken] = Time.now.utc - stat[routine_name][:started_at]
|
357
|
+
end
|
358
|
+
end
|
359
|
+
# Make another attempt from the scratch or...
|
360
|
+
redo if retry_attempt
|
361
|
+
# ...stop and report the result.
|
362
|
+
break
|
363
|
+
end
|
364
|
+
# After_process_api_request_callback.
|
365
|
+
invoke_callback_method(options[:after_process_api_request_callback], :manager => self)
|
366
|
+
data[:result]
|
367
|
+
rescue => error
|
368
|
+
# Invoke :after error callback
|
369
|
+
invoke_callback_method(options[:after_error_callback], :manager => self, :error => error)
|
370
|
+
fail error
|
371
|
+
ensure
|
372
|
+
# Remove the unique-per-request log prefix.
|
373
|
+
cloud_api_logger.reset_unique_prefix
|
374
|
+
# Complete stat data and invoke its callback.
|
375
|
+
@data[:stat][:time_taken] = Time.now.utc - @data[:stat][:started_at] if @data[:stat]
|
376
|
+
invoke_callback_method(options[:stat_data_callback], :manager => self, :stat => self.stat, :error => error)
|
377
|
+
end
|
378
|
+
private :process_api_request
|
379
|
+
|
380
|
+
|
381
|
+
# Initializes the @data variable and builds the request options
|
382
|
+
#
|
383
|
+
# @api private
|
384
|
+
#
|
385
|
+
# @param [String,Symbol] verb HTTP verb: :get, :post, :put, :delete, etc.
|
386
|
+
# @param [String] relative_path Relative URI path.
|
387
|
+
# @param [Hash] opts A set of extra options.
|
388
|
+
#
|
389
|
+
# @option options [Hash] :params A set of URL parameters.
|
390
|
+
# @option options [Hash] :headers A set of HTTP headers.
|
391
|
+
# @option options [Hash] :options A set of extra options: see {#initialize} method for them.
|
392
|
+
# @option options [Hash,String] :body The request body. If Hash is passed then it will
|
393
|
+
# convert it into String accordingly to 'content-type' header.
|
394
|
+
#
|
395
|
+
# @yield [String] If a block is given it will call it on every chunk of data received from a socket.
|
396
|
+
#
|
397
|
+
# @return [Any] The result of the request (usually a Hash or a String instance).
|
398
|
+
#
|
399
|
+
def initialize_api_request_options(verb, relative_path, opts, &block)
|
400
|
+
options = {}
|
401
|
+
options_chain = Utils.inheritance_chain(self.class, :options, @options, *(@with_options + [opts[:options]]))
|
402
|
+
options_chain.each{ |o| options.merge!(o || {}) }
|
403
|
+
# Endpoint
|
404
|
+
endpoint = options[:endpoint] || @endpoint
|
405
|
+
# Params
|
406
|
+
params = {}
|
407
|
+
params.merge!(Utils::extract_url_params(endpoint))._stringify_keys if options[:allow_endpoint_params]
|
408
|
+
params.merge!(options[:params] || {})._stringify_keys
|
409
|
+
params.merge!(opts[:params] || {})._stringify_keys
|
410
|
+
# Headers
|
411
|
+
headers = (options[:headers] || {})._stringify_keys
|
412
|
+
headers.merge!(@with_headers._stringify_keys)
|
413
|
+
headers.merge!( opts[:headers] || {})._stringify_keys
|
414
|
+
# Make sure the endpoint's schema is valid.
|
415
|
+
parsed_endpoint = ::URI::parse(endpoint)
|
416
|
+
unless [nil, 'http', 'https'].include? parsed_endpoint.scheme
|
417
|
+
fail Error.new('Endpoint parse failed - invalid scheme')
|
418
|
+
end
|
419
|
+
# Options: Build the initial data hash
|
420
|
+
@data = {
|
421
|
+
:options => options.dup,
|
422
|
+
:credentials => @credentials.dup,
|
423
|
+
:connection => { :uri => parsed_endpoint },
|
424
|
+
:request => { :verb => verb.to_s.downcase.to_sym,
|
425
|
+
:relative_path => relative_path,
|
426
|
+
:headers => HTTPHeaders::new(headers),
|
427
|
+
:body => opts[:body],
|
428
|
+
:orig_body => opts[:body], # keep here a copy of original body (Routines may change the real one, when it is a Hash)
|
429
|
+
:params => params,
|
430
|
+
:orig_params => params.dup }, # original params without any signatures etc
|
431
|
+
:vars => { :system => { :started_at => Time::now.utc,
|
432
|
+
:storage => @storage,
|
433
|
+
:block => block }
|
434
|
+
},
|
435
|
+
:callbacks => { },
|
436
|
+
:stat => {
|
437
|
+
:started_at => Time::now.utc,
|
438
|
+
:data => [ ],
|
439
|
+
},
|
440
|
+
}
|
441
|
+
options
|
442
|
+
end
|
443
|
+
private :initialize_api_request_options
|
444
|
+
|
445
|
+
|
446
|
+
# A helper method for invoking callbacks
|
447
|
+
#
|
448
|
+
# @api private
|
449
|
+
#
|
450
|
+
# The method checks if the given Proc exists and invokes it with the given set of arguments.
|
451
|
+
# In the case when proc==nil the method does nothing.
|
452
|
+
#
|
453
|
+
# @param [Proc] proc The callback.
|
454
|
+
# @param [Any] args A set of callback method arguments.
|
455
|
+
#
|
456
|
+
# @return [void]
|
457
|
+
#
|
458
|
+
def invoke_callback_method(proc, *args) # :nodoc:
|
459
|
+
proc.call(*args) if proc.is_a?(Proc)
|
460
|
+
end
|
461
|
+
private :invoke_callback_method
|
462
|
+
|
463
|
+
|
464
|
+
# Returns the current logger
|
465
|
+
#
|
466
|
+
# @return [RightScale::CloudApi::CloudApiLogger]
|
467
|
+
# @example
|
468
|
+
# # no example
|
469
|
+
#
|
470
|
+
def cloud_api_logger
|
471
|
+
@options[:cloud_api_logger]
|
472
|
+
end
|
473
|
+
|
474
|
+
|
475
|
+
# Returns current statistic
|
476
|
+
#
|
477
|
+
# @return [Hash]
|
478
|
+
#
|
479
|
+
# @example
|
480
|
+
# # Simple case:
|
481
|
+
# amazon.DescribeVolumes #=> [...]
|
482
|
+
# amazon.stat #=>
|
483
|
+
# {:started_at=>2014-01-03 19:09:13 UTC,
|
484
|
+
# :time_taken=>2.040465903,
|
485
|
+
# :data=>
|
486
|
+
# [{"RightScale::CloudApi::RetryManager"=>
|
487
|
+
# {:started_at=>2014-01-03 19:09:13 UTC, :time_taken=>1.7136e-05},
|
488
|
+
# "RightScale::CloudApi::RequestInitializer"=>
|
489
|
+
# {:started_at=>2014-01-03 19:09:13 UTC, :time_taken=>7.405e-06},
|
490
|
+
# "RightScale::CloudApi::AWS::RequestSigner"=>
|
491
|
+
# {:started_at=>2014-01-03 19:09:13 UTC, :time_taken=>0.000140031},
|
492
|
+
# "RightScale::CloudApi::RequestGenerator"=>
|
493
|
+
# {:started_at=>2014-01-03 19:09:13 UTC, :time_taken=>4.7781e-05},
|
494
|
+
# "RightScale::CloudApi::RequestAnalyzer"=>
|
495
|
+
# {:started_at=>2014-01-03 19:09:13 UTC, :time_taken=>3.1789e-05},
|
496
|
+
# "RightScale::CloudApi::ConnectionProxy"=>
|
497
|
+
# {:started_at=>2014-01-03 19:09:13 UTC, :time_taken=>2.025818663},
|
498
|
+
# "RightScale::CloudApi::ResponseAnalyzer"=>
|
499
|
+
# {:started_at=>2014-01-03 19:09:15 UTC, :time_taken=>0.000116668},
|
500
|
+
# "RightScale::CloudApi::CacheValidator"=>
|
501
|
+
# {:started_at=>2014-01-03 19:09:15 UTC, :time_taken=>1.9225e-05},
|
502
|
+
# "RightScale::CloudApi::ResponseParser"=>
|
503
|
+
# {:started_at=>2014-01-03 19:09:15 UTC, :time_taken=>0.014059933},
|
504
|
+
# "RightScale::CloudApi::ResultWrapper"=>
|
505
|
+
# {:started_at=>2014-01-03 19:09:15 UTC, :time_taken=>4.4907e-05}}]}
|
506
|
+
#
|
507
|
+
# @example
|
508
|
+
# # Using callback:
|
509
|
+
# STAT_DATA_CALBACK = lambda do |args|
|
510
|
+
# puts "Error: #{args[:error].class.name}" if args[:error]
|
511
|
+
# pp args[:stat]
|
512
|
+
# end
|
513
|
+
#
|
514
|
+
# amazon = RightScale::CloudApi::AWS::EC2::Manager::new(
|
515
|
+
# ENV['AWS_ACCESS_KEY_ID'],
|
516
|
+
# ENV['AWS_SECRET_ACCESS_KEY'],
|
517
|
+
# endpoint || ENV['EC2_URL'],
|
518
|
+
# :stat_data_callback => STAT_DATA_CALBACK)
|
519
|
+
#
|
520
|
+
# amazon.DescribeVolumes #=> [...]
|
521
|
+
#
|
522
|
+
# # >> Stat data callback's output <<:
|
523
|
+
# {:started_at=>2014-01-03 19:09:13 UTC,
|
524
|
+
# :time_taken=>2.040465903,
|
525
|
+
# :data=>
|
526
|
+
# [{"RightScale::CloudApi::RetryManager"=>
|
527
|
+
# {:started_at=>2014-01-03 19:09:13 UTC, :time_taken=>1.7136e-05},
|
528
|
+
# ...
|
529
|
+
# "RightScale::CloudApi::ResultWrapper"=>
|
530
|
+
# {:started_at=>2014-01-03 19:09:15 UTC, :time_taken=>4.4907e-05}}]}
|
531
|
+
#
|
532
|
+
def stat
|
533
|
+
@data && @data[:stat]
|
534
|
+
end
|
535
|
+
|
536
|
+
|
537
|
+
# Returns the last request object
|
538
|
+
#
|
539
|
+
# @return [RightScale::CloudApi::HTTPRequest]
|
540
|
+
# @example
|
541
|
+
# # no example
|
542
|
+
#
|
543
|
+
def request
|
544
|
+
@data && @data[:request] && @data[:request][:instance]
|
545
|
+
end
|
546
|
+
|
547
|
+
|
548
|
+
# Returns the last response object
|
549
|
+
#
|
550
|
+
# @return [RightScale::CloudApi::HTTPResponse]
|
551
|
+
# @example
|
552
|
+
# # no example
|
553
|
+
#
|
554
|
+
def response
|
555
|
+
@data && @data[:response] && @data[:response][:instance]
|
556
|
+
end
|
557
|
+
|
558
|
+
|
559
|
+
# The method is just a wrapper around process_api_request
|
560
|
+
#
|
561
|
+
# But this behavour can be overriden by sub-classes.
|
562
|
+
#
|
563
|
+
# @param [Any] args See *process_api_request* for the current ApiManager.
|
564
|
+
#
|
565
|
+
# @yield [String] See *process_api_request* for the current ApiManager.
|
566
|
+
#
|
567
|
+
# @return [Any] See *process_api_request* for the current ApiManager.
|
568
|
+
#
|
569
|
+
# @example
|
570
|
+
# # see cloud specific gems
|
571
|
+
#
|
572
|
+
def api(*args, &block)
|
573
|
+
process_api_request(*args, &block)
|
574
|
+
end
|
575
|
+
|
576
|
+
|
577
|
+
# Defines a set of *get*, *post*, *put*, *head*, *delete*, *patch* helper methods.
|
578
|
+
# All the methods are very simple wrappers aroung the *api* method. Whatever you would feed to
|
579
|
+
# *api* method you can feed to these ones except for the very first parameter :verb which
|
580
|
+
# is not required.
|
581
|
+
#
|
582
|
+
# @example
|
583
|
+
# s3.api(:get, 'my_bucket')
|
584
|
+
# # is equivalent to
|
585
|
+
# s3.get('my_bucket')
|
586
|
+
#
|
587
|
+
HTTP_VERBS = [ :get, :post, :put, :head, :delete, :patch ]
|
588
|
+
HTTP_VERBS.each do |http_verb|
|
589
|
+
eval <<-EOM
|
590
|
+
def #{http_verb}(*args, &block)
|
591
|
+
api(__method__.to_sym, *args, &block)
|
592
|
+
end
|
593
|
+
EOM
|
594
|
+
end
|
595
|
+
|
596
|
+
|
597
|
+
# Sets temporary set of options
|
598
|
+
#
|
599
|
+
# The method takes a block and all the API calls made inside it will have the given set of
|
600
|
+
# extra options. The method supports nesting.
|
601
|
+
#
|
602
|
+
# @param [Hash] options The set of options. See {#initialize} methos for the possible options.
|
603
|
+
# @return [void]
|
604
|
+
# @yield [] All the API call made in the block will have the provided options.
|
605
|
+
#
|
606
|
+
# @example
|
607
|
+
# # The example does not make too much sense - it just shows the idea.
|
608
|
+
# ec2 = RightScale::CloudApi::AWS::EC2.new(key, secret_key, :api_version => '2009-01-01')
|
609
|
+
# # Describe all the instances against API '2009-01-01'.
|
610
|
+
# ec2.DescribeInstances
|
611
|
+
# ec2.with_options(:api_version => '2012-01-01') do
|
612
|
+
# # Describe all the instances against API '2012-01-01'.
|
613
|
+
# ec2.DescribeInstances
|
614
|
+
# # Describe and stop only 2 instances.
|
615
|
+
# ec2.with_options(:params => { 'InstanceId' => ['i-01234567', 'i-76543210'] }) do
|
616
|
+
# ec2.DescribeInstances
|
617
|
+
# ec2.StopInstances
|
618
|
+
# end
|
619
|
+
# end
|
620
|
+
#
|
621
|
+
def with_options(options={}, &block)
|
622
|
+
@with_options << (options || {})
|
623
|
+
block.call
|
624
|
+
ensure
|
625
|
+
@with_options.pop
|
626
|
+
end
|
627
|
+
|
628
|
+
|
629
|
+
# Sets temporary sets of HTTP headers
|
630
|
+
#
|
631
|
+
# The method takes a block and all the API calls made inside it will have the given set of
|
632
|
+
# headers.
|
633
|
+
#
|
634
|
+
# @param [Hash] headers The set oh temporary headers.
|
635
|
+
# @option options [option_type] option_name option_description
|
636
|
+
#
|
637
|
+
# @return [void]
|
638
|
+
#
|
639
|
+
# @yield [] All the API call made in the block will have the provided headers.
|
640
|
+
#
|
641
|
+
# @example
|
642
|
+
# # The example does not make too much sense - it just shows the idea.
|
643
|
+
# ec2 = RightScale::CloudApi::AWS::EC2.new(key, secret_key, :api_version => '2009-01-01')
|
644
|
+
# ec2.with_header('agent' => 'mozzzzzillllla') do
|
645
|
+
# # the header is added to every request below
|
646
|
+
# ec2.DescribeInstances
|
647
|
+
# ec2.DescribeImaneg
|
648
|
+
# ec2.DescribeVolumes
|
649
|
+
# end
|
650
|
+
#
|
651
|
+
def with_headers(headers={}, &block)
|
652
|
+
@with_headers = headers || {}
|
653
|
+
block.call
|
654
|
+
ensure
|
655
|
+
@with_headers = {}
|
656
|
+
end
|
657
|
+
|
658
|
+
|
659
|
+
# Wraps the Manager with handy API helper methods
|
660
|
+
#
|
661
|
+
# The wrappers are not necessary but may be very helpful for REST API related clouds such
|
662
|
+
# as Amazon S3, OpenStack/Rackspace or Windows Azure.
|
663
|
+
#
|
664
|
+
# @param [Module,String] api_wrapper The wrapper module or a string that would help to
|
665
|
+
# identify it.
|
666
|
+
#
|
667
|
+
# @return [void]
|
668
|
+
#
|
669
|
+
# @raise [RightScale::CloudApi::ApiManager::Error] If an unexpected parameter is passed.
|
670
|
+
# @raise [RightScale::CloudApi::ApiManager::Error] If the requested wrapper does not exist.
|
671
|
+
#
|
672
|
+
# If string is passed:
|
673
|
+
#
|
674
|
+
# OpenStack: 'v1.0' #=> 'RightScale::CloudApi::OpenStack::Wrapper::V1_0'
|
675
|
+
# EC2: '2011-05-08' #=> 'RightScale::CloudApi::AWS::EC2::Wrapper::V2011_05_08'
|
676
|
+
#
|
677
|
+
# @example
|
678
|
+
# # ignore the method
|
679
|
+
#
|
680
|
+
def wrap_api_with(api_wrapper=nil, raise_if_not_exist=true) # :nodoc:
|
681
|
+
return if api_wrapper._blank?
|
682
|
+
# Complain if something unexpected was passed.
|
683
|
+
fail Error.new("Unsupported wrapper: #{api_wrapper.inspect}") unless api_wrapper.is_a?(Module) || api_wrapper.is_a?(String)
|
684
|
+
# If it is not a module - make it be the module
|
685
|
+
unless api_wrapper.is_a?(Module)
|
686
|
+
# If the String starts with a digit the prefix it with 'v'.
|
687
|
+
api_wrapper = "v" + api_wrapper if api_wrapper.to_s[/^\d/]
|
688
|
+
# Build the module name including the parent namespaces.
|
689
|
+
api_wrapper = "#{self.class.name.sub(/::ApiManager$/, '')}::Wrapper::#{api_wrapper.to_s.upcase.gsub(/[^A-Z0-9]/,'_')}"
|
690
|
+
# Try constantizing it.
|
691
|
+
_module = api_wrapper._constantize rescue nil
|
692
|
+
# Complain if the requested wrapper was not found.
|
693
|
+
fail Error.new("Wrapper not found #{api_wrapper}") if !_module && raise_if_not_exist
|
694
|
+
else
|
695
|
+
_module = api_wrapper
|
696
|
+
end
|
697
|
+
# Exit if there is no wrapper or it is already in use
|
698
|
+
return false if !_module || _extended?(_module)
|
699
|
+
# Use the wrapper
|
700
|
+
extend(_module)
|
701
|
+
cloud_api_logger.log("Wrapper: wrapped: #{_module.inspect}.", :wrapper)
|
702
|
+
true
|
703
|
+
end
|
704
|
+
|
705
|
+
end
|
706
|
+
end
|
707
|
+
end
|