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,224 @@
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
+ class ConnectionProxy
27
+
28
+ class RightHttpConnectionProxy
29
+ @@storage = {}
30
+
31
+ def self.storage
32
+ @@storage
33
+ end
34
+
35
+ # Remove dead threads/fibers from the storage
36
+ def self.clean_storage
37
+ Utils::remove_dead_fibers_and_threads_from_storage(storage)
38
+ end
39
+
40
+ class Error < CloudApi::Error
41
+ end
42
+
43
+ # Performs an HTTP request.
44
+ #
45
+ # @param [Hash] data The API request +data+ storage.
46
+ # See {RightScale::CloudApi::ApiManager.initialize_api_request_options} code for its explanation.
47
+ #
48
+ def request(data)
49
+ require "right_http_connection"
50
+
51
+ @data = data
52
+ @data[:response] = {}
53
+ # Create a connection
54
+ @uri = @data[:connection][:uri].dup
55
+
56
+ # Create/Get RightHttpConnection instance
57
+ remote_endpoint = current_endpoint
58
+ right_http_connection = current_connection
59
+
60
+ # Register a callback to close current connection
61
+ @data[:callbacks][:close_current_connection] = Proc::new{|reason| close_connection(remote_endpoint, reason); log "Current connection closed: #{reason}" }
62
+
63
+ # Create a real HTTP request
64
+ fake = @data[:request][:instance]
65
+ http_request = "Net::HTTP::#{fake.verb._camelize}"._constantize::new(fake.path)
66
+ if fake.is_io?
67
+ http_request.body_stream = fake.body
68
+ else
69
+ http_request.body = fake.body
70
+ end
71
+ fake.headers.each{|header, value| http_request[header] = value }
72
+ fake.raw = http_request
73
+
74
+ # Set all the options are suported by RightHttpConnection (if they are)
75
+ http_connection_data = {
76
+ :server => @uri.host,
77
+ :port => @uri.port,
78
+ :protocol => @uri.scheme,
79
+ :request => http_request,
80
+ :exception => ConnectionError
81
+ }
82
+
83
+ # Set all required options
84
+ http_connection_data[:logger] = @data[:options][:cloud_api_logger].logger
85
+ http_connection_data[:user_agent] = @data[:options][:connection_user_agent] if @data[:options].has_key?(:connection_user_agent)
86
+ http_connection_data[:ca_file] = @data[:options][:connection_ca_file] if @data[:options].has_key?(:connection_ca_file)
87
+ http_connection_data[:http_connection_retry_count] = @data[:options][:connection_retry_count] if @data[:options].has_key?(:connection_retry_count)
88
+ http_connection_data[:http_connection_read_timeout] = @data[:options][:connection_read_timeout] if @data[:options].has_key?(:connection_read_timeout)
89
+ http_connection_data[:http_connection_open_timeout] = @data[:options][:connection_open_timeout] if @data[:options].has_key?(:connection_open_timeout)
90
+ http_connection_data[:http_connection_retry_delay] = @data[:options][:connection_retry_delay] if @data[:options].has_key?(:connection_retry_delay)
91
+ http_connection_data[:raise_on_timeout] = @data[:options][:abort_on_timeout] if @data[:options][:abort_on_timeout]
92
+ http_connection_data[:cert] = @data[:credentials][:cert] if @data[:credentials].has_key?(:cert)
93
+ http_connection_data[:key] = @data[:credentials][:key] if @data[:credentials].has_key?(:key)
94
+
95
+ #log "HttpConnection request: #{http_connection_data.inspect}"
96
+
97
+ # Make a request:
98
+ block = @data[:vars][:system][:block]
99
+ if block
100
+ # If block is given - pass there all the chunks of a response and stop
101
+ # (dont do any parsing, analysing etc)
102
+ #
103
+ # TRB 9/17/07 Careful - because we are passing in blocks, we get a situation where
104
+ # an exception may get thrown in the block body (which is high-level
105
+ # code either here or in the application) but gets caught in the
106
+ # low-level code of HttpConnection. The solution is not to let any
107
+ # exception escape the block that we pass to HttpConnection::request.
108
+ # Exceptions can originate from code directly in the block, or from user
109
+ # code called in the other block which is passed to response.read_body.
110
+ #
111
+ # TODO: the suggested fix for RightHttpConnection if to catch
112
+ # Interrupt and SystemCallError instead of Exception in line 402
113
+ response = nil
114
+ begin
115
+ block_exception = nil
116
+ # Response.body will be a Net::ReadAdapter instance here - it cant be read as a string.
117
+ # WEB: On its own, Net::HTTP causes response.body to be a Net::ReadAdapter when you make a request with a block
118
+ # that calls read_body on the response.
119
+ response = right_http_connection.request(http_connection_data) do |res|
120
+ begin
121
+ # Update temp response
122
+ @data[:response][:instance] = HTTPResponse::new( res.code,
123
+ nil,
124
+ res.to_hash,
125
+ res )
126
+ res.read_body(&block) if res.is_a?(Net::HTTPSuccess)
127
+ rescue Exception => e
128
+ block_exception = e
129
+ break
130
+ end
131
+ end
132
+ raise block_exception if block_exception
133
+ rescue Exception => e
134
+ right_http_connection.finish(e.message)
135
+ raise e
136
+ end
137
+ else
138
+ # Things are simple if there is no any block
139
+ response = right_http_connection.request(http_connection_data)
140
+ end
141
+
142
+ @data[:response][:instance] = HTTPResponse::new( response.code,
143
+ response.body,
144
+ response.to_hash,
145
+ response )
146
+
147
+ # # HACK: KD
148
+ # #
149
+ # # When one uploads a file with pos > 0 and 'content-length' != File.size - pos
150
+ # # then the next request through this connection fails with 400 or 505...
151
+ # # It seems that it expects the file to be read until EOF.
152
+ # #
153
+ # # KIlling the current connection seems to help but it is not good...
154
+ # #
155
+ # if @data[:request][:instance].body_stream #&& !@data[:request][:instance].body_stream.eof
156
+ # pp @data[:request][:instance].body_stream.pos
157
+ # log "closing current connection because of an issue when an IO object is not read until EOF"
158
+ # @connection.finish
159
+ # end
160
+ end
161
+
162
+ def log(message)
163
+ @data[:options][:cloud_api_logger].log(message, :connection_proxy)
164
+ end
165
+
166
+ #----------------------------
167
+ # HTTP Connections handling
168
+ #----------------------------
169
+
170
+ def storage # :nodoc:
171
+ @@storage[Utils::current_thread_and_fiber] ||= {}
172
+ end
173
+
174
+ def current_endpoint # :nodoc:
175
+ "#{@uri.scheme}://#{@uri.host}:#{@uri.port}"
176
+ end
177
+
178
+ def close_connection(endpoint, reason='') # :nodoc:
179
+ return nil unless storage[endpoint]
180
+
181
+ log "Destroying RightHttpConnection to #{endpoint}, reason: #{reason}"
182
+ storage[endpoint][:connection].finish(reason)
183
+ rescue => e
184
+ log "Exception in close_connection: #{e.class.name}: #{e.message}"
185
+ ensure
186
+ storage.delete(endpoint) if endpoint
187
+ end
188
+
189
+ INACTIVE_LIFETIME_LIMIT = 900 # seconds
190
+
191
+ # Delete out-of-dated connections for current Thread/Fiber
192
+ def clean_outdated_connections
193
+ life_time_scratch = Time::now - INACTIVE_LIFETIME_LIMIT
194
+ storage.each do |endpoint, connection|
195
+ if connection[:last_used_at] < life_time_scratch
196
+ close_connection(endpoint, 'out-of-date')
197
+ end
198
+ end
199
+ end
200
+
201
+ # Expire the connection if it has expired.
202
+ def current_connection # :nodoc:
203
+ # Remove dead threads/fibers from the storage
204
+ self.class::clean_storage
205
+ # Delete out-of-dated connections
206
+ clean_outdated_connections
207
+ # Get current_connection
208
+ endpoint = current_endpoint
209
+ unless storage[endpoint]
210
+ storage[endpoint] = {}
211
+ storage[endpoint][:connection] = Rightscale::HttpConnection.new( :exception => CloudError,
212
+ :logger => @data[:options][:cloud_api_logger].logger )
213
+ log "Creating RightHttpConection to #{endpoint.inspect}"
214
+ else
215
+ log "Reusing RightHttpConection to #{endpoint.inspect}"
216
+ end
217
+ storage[endpoint][:last_used_at] = Time::now
218
+ storage[endpoint][:connection]
219
+ end
220
+ end
221
+
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,66 @@
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
+ # This routine generifies all HTTP requests so that the main code does not need to worry about
28
+ # the underlaying libraries (right_http_connection or persistent_connection).
29
+ #
30
+ class ConnectionProxy < Routine
31
+ class Error < CloudApi::Error
32
+ end
33
+
34
+ # Main entry point.
35
+ #
36
+ # Performs an HTTP request.
37
+ #
38
+ def process
39
+ unless @connection_proxy
40
+ # Try to use a user defined connection proxy. The options are:
41
+ # - RightScale::CloudApi::ConnectionProxy::RightHttpConnectionProxy,
42
+ # - RightScale::CloudApi::ConnectionProxy::NetHttpPersistentProxy
43
+ connection_proxy_class = data[:options][:connection_proxy]
44
+ unless connection_proxy_class
45
+ # If it is not defined then load right_http_connection gem and use it.
46
+ # connection_proxy_class = ConnectionProxy::RightHttpConnectionProxy
47
+ connection_proxy_class = RightScale::CloudApi::ConnectionProxy::NetHttpPersistentProxy
48
+ end
49
+ @connection_proxy = connection_proxy_class.new
50
+ end
51
+
52
+ # Register a call back to close current connection
53
+ data[:callbacks][:close_current_connection] = Proc::new do |reason|
54
+ @connection_proxy.close_connection(nil, reason)
55
+ cloud_api_logger.log("Current connection closed: #{reason}", :connection_proxy)
56
+ end
57
+
58
+ # Make a request.
59
+ with_timer('HTTP request', :connection_proxy) do
60
+ @connection_proxy.request(data)
61
+ end
62
+ end
63
+
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,122 @@
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 routine provides *error_pattern* method that is used to define request and response patterns.
28
+ #
29
+ # The patterns allows one to control the request processing flow: you can enable retries for
30
+ # certain API calls or can re-open a connection on HTTP failure.
31
+ #
32
+ # The supported actions are:
33
+ #
34
+ # Request option:
35
+ # - :abort_on_timeout - If there was a low level timeout then it should not retry a request
36
+ # but should fail. Lets say one made a call to launch some instance and the remote cloud
37
+ # launched them but timed out to respond back. The default behavior for the gem is to make
38
+ # a retry if a timeout received but in this particular case it will launch the instances
39
+ # again. So the solution here is to make the system to fail after the first unsuccessfull
40
+ # request.
41
+ #
42
+ # Response options:
43
+ # - :retry - Make a retry if the failed response matches to the pattern.
44
+ # - :abort - Do not make a retry and fail if the response matches to the pattern (opposite to :retry).
45
+ # - :disconnect_and_abort - Close a connection and fail.
46
+ # - :reconnect_and_retry - Reestablish a connection and make a retry.
47
+ #
48
+ class RequestAnalyzer < Routine
49
+
50
+ class Error < CloudApi::Error
51
+ end
52
+
53
+ REQUEST_ACTIONS = [ :abort_on_timeout ]
54
+ REQUEST_KEYS = [ :verb, :verb!, :path, :path!, :request, :request!, :if ]
55
+
56
+ RESPONSE_ACTIONS = [ :disconnect_and_abort, :abort, :reconnect_and_retry, :retry ]
57
+ RESPONSE_KEYS = [ :verb, :verb!, :path, :path!, :request, :request!, :code, :code!, :response, :response!, :if ]
58
+
59
+ ALL_ACTIONS = REQUEST_ACTIONS + RESPONSE_ACTIONS
60
+
61
+ module ClassMethods
62
+
63
+ def self.extended(base)
64
+ unless base.respond_to?(:options) && base.options.is_a?(Hash)
65
+ fail Error::new("RequestAnalyzer routine assumes class being extended responds to :options and returns a hash")
66
+ end
67
+ end
68
+
69
+ # Adds a new error pattern.
70
+ # Patterns are analyzed in order of their definition. If one pattern hits the rest are not analyzed.
71
+ #
72
+ # @param [Symbol] action The requested action.
73
+ # @param [Hash] error_pattern The requested pattern (see {file:lib/base/helper/utils.rb self.pattern_matches?}).
74
+ #
75
+ # @xample:
76
+ # error_pattern :abort_on_timeout, :path => /Action=(Run|Create)/
77
+ # error_pattern :retry, :response => /InternalError|Internal Server Error|internal service error/i
78
+ # error_pattern :disconnect_and_abort, :code => /5..|403|408/
79
+ # error_pattern :disconnect_and_abort, :code => /4../, :if => Proc.new{ |opts| rand(100) < 10 }
80
+ #
81
+ # @raise [RightScale::CloudApi::RequestAnalyzer::Error] If error_pattern is not a Hash instance.
82
+ # @raise [RightScale::CloudApi::RequestAnalyzer::Error] If action is not supported.
83
+ # @raise [RightScale::CloudApi::RequestAnalyzer::Error] If pattern keys are weird.
84
+ #
85
+ def error_pattern(action, error_pattern)
86
+ action = action.to_sym
87
+ fail Error::new("Patterns are not set for action #{action.inspect}") if !error_pattern.is_a?(Hash) || error_pattern._blank?
88
+ fail Error::new("Unsupported action #{action.inspect} for error pattern #{error_pattern.inspect}") unless ALL_ACTIONS.include?(action)
89
+ unsupported_keys = REQUEST_ACTIONS.include?(action) ? error_pattern.keys - REQUEST_KEYS : error_pattern.keys - RESPONSE_KEYS
90
+ fail Error::new("Unsupported keys #{unsupported_keys.inspect} for #{action.inspect} in error pattern #{error_pattern.inspect}") unless unsupported_keys._blank?
91
+ (options[:error_patterns] ||= []) << error_pattern.merge(:action => action)
92
+ end
93
+ end
94
+
95
+ # The main entry point.
96
+ #
97
+ def process
98
+ # Get a list of accessible error patterns
99
+ error_patterns = data[:options][:error_patterns] || []
100
+ opts = { :request => data[:request][:instance],
101
+ :response => nil,
102
+ :verb => data[:request][:verb],
103
+ :params => data[:request][:orig_params].dup}
104
+ # Walk through all the error patterns and find the first that matches.
105
+ # RequestAnalyser accepts only REQUEST_ACTIONS (actually "abort_on_timeout" only)
106
+ request_error_patterns = error_patterns.select{|e| REQUEST_ACTIONS.include?(e[:action])}
107
+ request_error_patterns.each do |pattern|
108
+ # If we see any pattern that matches our current state
109
+ if Utils::pattern_matches?(pattern, opts)
110
+ # then set a flag to disable retries
111
+ data[:options][:abort_on_timeout] = true
112
+ cloud_api_logger.log("Request matches to error pattern: #{pattern.inspect}" , :request_analyzer)
113
+ break
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ end
120
+ end
121
+
122
+
@@ -0,0 +1,48 @@
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 routine generates a new HTTP request.
28
+ #
29
+ class RequestGenerator < Routine
30
+
31
+ # Generates an HTTP request instance.
32
+ #
33
+ # The request instance must be compatible to what ConnectionProxy is being used expects.
34
+ #
35
+ def process
36
+ request = HTTPRequest::new( data[:request][:verb],
37
+ data[:request][:path],
38
+ data[:request][:body],
39
+ data[:request][:headers] )
40
+ cloud_api_logger.log("Request generated: #{request.to_s}" , :request_generator)
41
+ cloud_api_logger.log("Request headers: #{request.headers_info}" , :request_generator)
42
+ cloud_api_logger.log("Request body: #{request.body_info}\n", :request_generator_body) unless (request.body.to_s.size == 0)
43
+ data[:request][:instance] = request
44
+ end
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,52 @@
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 routine adds a random token to all the GET requests if the corresponding option is enabled.
28
+ #
29
+ class RequestInitializer < Routine
30
+
31
+ # Initializes things we may need to initialize.
32
+ #
33
+ # Sometimes we may need to add a random token for every request so that remote cloud. This may
34
+ # be needed when the cloud caches responses for similar requests. Lets say you listed instances
35
+ # then created one and then listed them again. S-me clouds (rackspace) may start to report the
36
+ # new seconds after it was created because of the caching they do.
37
+ #
38
+ # But if we mix something random onto every request then 2 consecutive list instances calls will
39
+ # look like they are different and the cloud wont return the cached data.
40
+ #
41
+ def process
42
+ # Add a random thing to every get request
43
+ if data[:request][:verb] == :get && !data[:options][:random_token]._blank?
44
+ random_token_name = 'rsrcarandomtoken'
45
+ random_token_name = data[:options][:random_token].to_s if [String, Symbol].include?(data[:options][:random_token].class)
46
+ data[:request][:params][random_token_name] = Utils::generate_token
47
+ end
48
+ end
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,152 @@
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 routine analyzes HTTP responses and in the case of HTTP error it takes actions defined
28
+ # through *error_pattern* definitions.
29
+ #
30
+ class ResponseAnalyzer < Routine
31
+
32
+ class Error < CloudApi::Error
33
+ end
34
+
35
+ # Analyzes an HTTP response.
36
+ #
37
+ # In the case of 4xx, 5xx HTTP errors the method parses the response body to get the
38
+ # error message. Then it tries to find an error pattern that would match to the response.
39
+ # If the pattern found it takes the action (:retry, :abort, :disconnect_and_abort or
40
+ # :reconnect_and_retry) acordingly to the error patern. If the pattern not fount it just
41
+ # fails with RightScale::CloudApi::CloudError.
42
+ #
43
+ # In the case of 2xx code the method does nothing.
44
+ #
45
+ # In the case of any other unexpected HTTP code it fails with RightScale::CloudApi::CloudError.
46
+ #
47
+ # @example
48
+ # error_pattern :abort_on_timeout, :path => /Action=(Run|Create)/
49
+ # error_pattern :retry, :response => /InternalError|Internal Server Error|internal service error/i
50
+ # error_pattern :disconnect_and_abort, :code => /5..|403|408/
51
+ # error_pattern :reconnect_and_retry, :code => /4../, :if => Proc.new{ |opts| rand(100) < 10 }
52
+ #
53
+ def process
54
+ # Extract the current response and log it.
55
+ response = data[:response][:instance]
56
+ unless response.nil?
57
+ cloud_api_logger.log("Response received: #{response.to_s}", :response_analyzer)
58
+ cloud_api_logger.log("Response headers: #{response.headers_info}", :response_analyzer)
59
+ log_method = (response.is_error? || response.is_redirect?) ? :response_analyzer_body_error : :response_analyzer_body
60
+ cloud_api_logger.log("Response body: #{response.body_info}", log_method)
61
+ end
62
+
63
+ code = data[:response][:instance].code
64
+ body = data[:response][:instance].body
65
+ close_current_connection_proc = data[:callbacks][:close_current_connection]
66
+
67
+ # Analyze the response code.
68
+ case code
69
+ when /^(5..|4..)/
70
+ # Try to parse the received error message.
71
+ error_message = if data[:options][:response_error_parser]
72
+ parser = data[:options][:response_error_parser]
73
+ with_timer("Error parsing with #{parser}") do
74
+ parser::parse(data[:response][:instance], data[:options])
75
+ end
76
+ else
77
+ "#{code}: #{body.to_s}"
78
+ end
79
+ # Get the list of patterns.
80
+ error_patterns = data[:options][:error_patterns] || []
81
+ opts = { :request => data[:request][:instance],
82
+ :response => data[:response][:instance],
83
+ :verb => data[:request][:verb],
84
+ :params => data[:request][:orig_params].dup }
85
+ # Walk through all the patterns and find the first that matches.
86
+ error_patterns.each do |pattern|
87
+ if Utils::pattern_matches?(pattern, opts)
88
+ cloud_api_logger.log("Error code: #{code}, pattern match: #{pattern.inspect}", :response_analyzer, :warn)
89
+ # Take the required action.
90
+ case pattern[:action]
91
+ when :disconnect_and_abort
92
+ close_current_connection_proc && close_current_connection_proc.call("Error code: #{code}")
93
+ fail(HttpError::new(code, error_message))
94
+ when :reconnect_and_retry
95
+ close_current_connection_proc && close_current_connection_proc.call("Error code: #{code}")
96
+ @data[:vars][:retry][:http] = { :code => code, :message => error_message }
97
+ fail(RetryAttempt::new)
98
+ when :abort
99
+ fail(HttpError::new(code, error_message))
100
+ when :retry
101
+ invoke_callback_method(data[:options][:before_retry_callback],
102
+ :routine => self,
103
+ :pattern => pattern,
104
+ :opts => opts)
105
+ @data[:vars][:retry][:http] = { :code => code, :message => error_message }
106
+ fail(RetryAttempt::new)
107
+ end
108
+ end
109
+ end
110
+ # The default behavior: this guy hits when there is no any matching pattern
111
+ fail(HttpError::new(code, error_message))
112
+ when /^3..$/
113
+ # In the case of redirect: update a request URI and retry
114
+ location = Array(data[:response][:instance].headers['location']).first
115
+ # ----- AMAZON HACK BEGIN ----------------------------------------------------------
116
+ # Amazon sometimes hide a location host into a response body.
117
+ if location._blank? && body && body[/<Endpoint>(.*?)<\/Endpoint>/] && $1
118
+ data[:connection][:uri].host = $1
119
+ location = data[:connection][:uri].to_s
120
+ end
121
+ # ----- AMAZON HACK END ------------------------------------------------------------
122
+ # Replace URI and retry if the location was successfully set
123
+ unless location._blank?
124
+ data[:connection][:uri] = ::URI.parse(location)
125
+ old_request = data[:request].delete(:instance)
126
+ data[:request].delete(:path)
127
+ cloud_api_logger.log("Redirect detected: #{location.inspect}", :response_analyzer)
128
+ invoke_callback_method(data[:options][:before_redirect_callback],
129
+ :routine => self,
130
+ :old_request => old_request,
131
+ :location => location)
132
+ raise(RetryAttempt::new)
133
+ else
134
+ # ----- OPENSTACK BEGIN ----------------------------------------------------------
135
+ # some OS services like Glance returns a list of supported api versions with status 300
136
+ # if there is at least one href in the body we need to further analize it in the OS manager
137
+ return true if body && body[/href/]
138
+ # ----- OPENSTACK END ----------------------------------------------------------
139
+ raise HttpError::new(code, "Cannot parse a redirect location")
140
+ end
141
+ when /^2../
142
+ # There is nothing to do on 2xx code
143
+ return true
144
+ else
145
+ fail(Error::new("Unexpected response code: #{code.inspect}"))
146
+ end
147
+ end
148
+ end
149
+
150
+ end
151
+ end
152
+