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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY +2 -0
  3. data/LICENSE +19 -0
  4. data/README.md +14 -0
  5. data/Rakefile +37 -0
  6. data/lib/base/api_manager.rb +707 -0
  7. data/lib/base/helpers/cloud_api_logger.rb +214 -0
  8. data/lib/base/helpers/http_headers.rb +239 -0
  9. data/lib/base/helpers/http_parent.rb +103 -0
  10. data/lib/base/helpers/http_request.rb +173 -0
  11. data/lib/base/helpers/http_response.rb +122 -0
  12. data/lib/base/helpers/net_http_patch.rb +31 -0
  13. data/lib/base/helpers/query_api_patterns.rb +862 -0
  14. data/lib/base/helpers/support.rb +270 -0
  15. data/lib/base/helpers/support.xml.rb +306 -0
  16. data/lib/base/helpers/utils.rb +380 -0
  17. data/lib/base/manager.rb +122 -0
  18. data/lib/base/parsers/json.rb +38 -0
  19. data/lib/base/parsers/plain.rb +36 -0
  20. data/lib/base/parsers/rexml.rb +83 -0
  21. data/lib/base/parsers/sax.rb +200 -0
  22. data/lib/base/routines/cache_validator.rb +184 -0
  23. data/lib/base/routines/connection_proxies/net_http_persistent_proxy.rb +194 -0
  24. data/lib/base/routines/connection_proxies/right_http_connection_proxy.rb +224 -0
  25. data/lib/base/routines/connection_proxy.rb +66 -0
  26. data/lib/base/routines/request_analyzer.rb +122 -0
  27. data/lib/base/routines/request_generator.rb +48 -0
  28. data/lib/base/routines/request_initializer.rb +52 -0
  29. data/lib/base/routines/response_analyzer.rb +152 -0
  30. data/lib/base/routines/response_parser.rb +79 -0
  31. data/lib/base/routines/result_wrapper.rb +75 -0
  32. data/lib/base/routines/retry_manager.rb +106 -0
  33. data/lib/base/routines/routine.rb +98 -0
  34. data/lib/right_cloud_api_base.rb +72 -0
  35. data/lib/right_cloud_api_base_version.rb +37 -0
  36. data/right_cloud_api_base.gemspec +63 -0
  37. data/spec/helpers/query_api_pattern_spec.rb +312 -0
  38. data/spec/helpers/support_spec.rb +211 -0
  39. data/spec/helpers/support_xml_spec.rb +207 -0
  40. data/spec/helpers/utils_spec.rb +179 -0
  41. data/spec/routines/connection_proxies/test_net_http_persistent_proxy_spec.rb +143 -0
  42. data/spec/routines/test_cache_validator_spec.rb +152 -0
  43. data/spec/routines/test_connection_proxy_spec.rb +44 -0
  44. data/spec/routines/test_request_analyzer_spec.rb +106 -0
  45. data/spec/routines/test_response_analyzer_spec.rb +132 -0
  46. data/spec/routines/test_response_parser_spec.rb +228 -0
  47. data/spec/routines/test_result_wrapper_spec.rb +63 -0
  48. data/spec/routines/test_retry_manager_spec.rb +84 -0
  49. data/spec/spec_helper.rb +15 -0
  50. 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