right_agent 1.0.1 → 2.0.7

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 (67) hide show
  1. data/README.rdoc +10 -8
  2. data/Rakefile +31 -5
  3. data/lib/right_agent.rb +6 -1
  4. data/lib/right_agent/actor.rb +4 -20
  5. data/lib/right_agent/actors/agent_manager.rb +1 -1
  6. data/lib/right_agent/agent.rb +357 -144
  7. data/lib/right_agent/agent_config.rb +7 -6
  8. data/lib/right_agent/agent_identity.rb +13 -11
  9. data/lib/right_agent/agent_tag_manager.rb +60 -64
  10. data/{spec/results_mock.rb → lib/right_agent/clients.rb} +10 -24
  11. data/lib/right_agent/clients/api_client.rb +383 -0
  12. data/lib/right_agent/clients/auth_client.rb +247 -0
  13. data/lib/right_agent/clients/balanced_http_client.rb +369 -0
  14. data/lib/right_agent/clients/base_retry_client.rb +495 -0
  15. data/lib/right_agent/clients/right_http_client.rb +279 -0
  16. data/lib/right_agent/clients/router_client.rb +493 -0
  17. data/lib/right_agent/command/command_io.rb +4 -4
  18. data/lib/right_agent/command/command_parser.rb +2 -2
  19. data/lib/right_agent/command/command_runner.rb +1 -1
  20. data/lib/right_agent/connectivity_checker.rb +179 -0
  21. data/lib/right_agent/core_payload_types/secure_document_location.rb +2 -2
  22. data/lib/right_agent/dispatcher.rb +12 -10
  23. data/lib/right_agent/enrollment_result.rb +16 -12
  24. data/lib/right_agent/exceptions.rb +34 -20
  25. data/lib/right_agent/history.rb +10 -5
  26. data/lib/right_agent/log.rb +5 -5
  27. data/lib/right_agent/minimal.rb +1 -0
  28. data/lib/right_agent/multiplexer.rb +1 -1
  29. data/lib/right_agent/offline_handler.rb +270 -0
  30. data/lib/right_agent/packets.rb +7 -7
  31. data/lib/right_agent/payload_formatter.rb +1 -1
  32. data/lib/right_agent/pending_requests.rb +128 -0
  33. data/lib/right_agent/platform.rb +1 -1
  34. data/lib/right_agent/protocol_version_mixin.rb +69 -0
  35. data/lib/right_agent/{idempotent_request.rb → retryable_request.rb} +7 -7
  36. data/lib/right_agent/scripts/agent_controller.rb +28 -26
  37. data/lib/right_agent/scripts/agent_deployer.rb +37 -22
  38. data/lib/right_agent/scripts/common_parser.rb +10 -3
  39. data/lib/right_agent/secure_identity.rb +1 -1
  40. data/lib/right_agent/sender.rb +299 -785
  41. data/lib/right_agent/serialize/secure_serializer.rb +3 -1
  42. data/lib/right_agent/serialize/secure_serializer_initializer.rb +2 -2
  43. data/lib/right_agent/serialize/serializable.rb +8 -3
  44. data/right_agent.gemspec +49 -18
  45. data/spec/agent_config_spec.rb +7 -7
  46. data/spec/agent_identity_spec.rb +7 -4
  47. data/spec/agent_spec.rb +43 -7
  48. data/spec/agent_tag_manager_spec.rb +72 -83
  49. data/spec/clients/api_client_spec.rb +423 -0
  50. data/spec/clients/auth_client_spec.rb +272 -0
  51. data/spec/clients/balanced_http_client_spec.rb +576 -0
  52. data/spec/clients/base_retry_client_spec.rb +635 -0
  53. data/spec/clients/router_client_spec.rb +594 -0
  54. data/spec/clients/spec_helper.rb +111 -0
  55. data/spec/command/command_io_spec.rb +1 -1
  56. data/spec/command/command_parser_spec.rb +1 -1
  57. data/spec/connectivity_checker_spec.rb +83 -0
  58. data/spec/dispatcher_spec.rb +3 -2
  59. data/spec/enrollment_result_spec.rb +2 -2
  60. data/spec/history_spec.rb +51 -39
  61. data/spec/offline_handler_spec.rb +340 -0
  62. data/spec/pending_requests_spec.rb +136 -0
  63. data/spec/{idempotent_request_spec.rb → retryable_request_spec.rb} +73 -73
  64. data/spec/sender_spec.rb +835 -1052
  65. data/spec/serialize/secure_serializer_spec.rb +3 -2
  66. data/spec/spec_helper.rb +54 -1
  67. metadata +71 -12
@@ -0,0 +1,247 @@
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
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ module RightScale
25
+
26
+ # Abstract base class for authorization client
27
+ class AuthClient
28
+
29
+ # State of authorization: :pending, :authorized, :unauthorized, :expired, :failed, :closed
30
+ attr_reader :state
31
+
32
+ PERMITTED_STATE_TRANSITIONS = {
33
+ :pending => [:pending, :authorized, :unauthorized, :failed, :closed],
34
+ :authorized => [:authorized, :unauthorized, :expired, :failed, :closed],
35
+ :unauthorized => [:authorized, :unauthorized, :failed, :closed],
36
+ :expired => [:authorized, :unauthorized, :expired, :failed, :closed],
37
+ :failed => [:failed, :closed],
38
+ :closed => [:closed] }
39
+
40
+ # Initialize client
41
+ # Derived classes need to call reset_stats
42
+ def initialize(options = {})
43
+ raise NotImplementedError, "#{self.class.name} is an abstract class"
44
+ end
45
+
46
+ # Identity of agent using this client
47
+ #
48
+ # @return [String] identity
49
+ def identity
50
+ @identity
51
+ end
52
+
53
+ # Headers to be added to HTTP request
54
+ # Include authorization header by default
55
+ #
56
+ # @return [Hash] headers to be added to request header
57
+ #
58
+ # @raise [Exceptions::Unauthorized] not authorized
59
+ # @raise [Exceptions::RetryableError] authorization expired, but retry may succeed
60
+ def headers
61
+ check_authorized
62
+ auth_header
63
+ end
64
+
65
+ # Authorization header to be added to HTTP request
66
+ #
67
+ # @return [Hash] authorization header
68
+ #
69
+ # @raise [Exceptions::Unauthorized] not authorized
70
+ # @raise [Exceptions::RetryableError] authorization expired, but retry may succeed
71
+ def auth_header
72
+ check_authorized
73
+ {"Authorization" => "Bearer #{@access_token}"}
74
+ end
75
+
76
+ # Account if any to which agent using this client belongs
77
+ #
78
+ # @return [Integer] account ID
79
+ #
80
+ # @raise [Exceptions::Unauthorized] not authorized
81
+ def account_id
82
+ check_authorized
83
+ @account_id
84
+ end
85
+
86
+ # URL for accessing RightApi including base path
87
+ #
88
+ # @return [String] URL including base path
89
+ #
90
+ # @raise [Exceptions::Unauthorized] not authorized
91
+ # @raise [Exceptions::RetryableError] authorization expired, but retry may succeed
92
+ def api_url
93
+ check_authorized
94
+ @api_url
95
+ end
96
+
97
+ # URL for accessing RightNet router including base path
98
+ #
99
+ # @return [String] URL including base path
100
+ #
101
+ # @raise [Exceptions::Unauthorized] not authorized
102
+ # @raise [Exceptions::RetryableError] authorization expired, but retry may succeed
103
+ def router_url
104
+ check_authorized
105
+ @router_url
106
+ end
107
+
108
+ # RightNet communication mode
109
+ #
110
+ # @return [Symbol] :http or :amqp
111
+ def mode
112
+ @mode
113
+ end
114
+
115
+ # An HTTP request had an authorization expiration error
116
+ # Renew authorization
117
+ #
118
+ # @return [TrueClass] always true
119
+ def expired
120
+ Log.info("Renewing authorization for #{identity} because request failed due to expiration")
121
+ self.state = :expired
122
+ renew_authorization
123
+ true
124
+ end
125
+
126
+ # An HTTP request received a redirect response
127
+ #
128
+ # @param [String] location to which response indicated to redirect
129
+ #
130
+ # @return [TrueClass] always true
131
+ def redirect(location)
132
+ true
133
+ end
134
+
135
+ # Take any actions necessary to quiesce client interaction in preparation
136
+ # for agent termination but allow any active requests to complete
137
+ #
138
+ # @return [TrueClass] always true
139
+ def close
140
+ self.state = :closed
141
+ true
142
+ end
143
+
144
+ # Record callback to be notified of authorization status changes
145
+ # Multiple callbacks are supported
146
+ #
147
+ # @yield [type, status] called when status changes (optional)
148
+ # @yieldparam [Symbol] type of client reporting status change: :auth
149
+ # @yieldparam [Symbol] state of authorization
150
+ #
151
+ # @return [Symbol] current state
152
+ def status(&callback)
153
+ @status_callbacks = (@status_callbacks || []) << callback if callback
154
+ state
155
+ end
156
+
157
+ # Current statistics for this client
158
+ #
159
+ # @param [Boolean] reset the statistics after getting the current ones
160
+ #
161
+ # @return [Hash] current statistics
162
+ # [Hash, NilClass] "state" Activity stats or nil if none
163
+ # [Hash, NilClass] "exceptions" Exceptions stats or nil if none
164
+ def stats(reset = false)
165
+ stats = {}
166
+ @stats.each { |k, v| stats[k] = v.all }
167
+ reset_stats if reset
168
+ stats
169
+ end
170
+
171
+ protected
172
+
173
+ # Reset statistics for this client
174
+ #
175
+ # @return [TrueClass] always true
176
+ def reset_stats
177
+ @stats = {
178
+ "state" => RightSupport::Stats::Activity.new,
179
+ "exceptions" => RightSupport::Stats::Exceptions.new(agent = nil, @exception_callback)}
180
+ true
181
+ end
182
+
183
+ # Check whether authorized
184
+ #
185
+ # @return [TrueClass] always true if don't raise exception
186
+ #
187
+ # @raise [Exceptions::Unauthorized] not authorized
188
+ # @raise [Exceptions::RetryableError] authorization expired, but retry may succeed
189
+ def check_authorized
190
+ if state == :expired
191
+ raise Exceptions::RetryableError, "Authorization expired"
192
+ elsif state != :authorized
193
+ raise Exceptions::Unauthorized, "Not authorized with RightScale" if state != :authorized
194
+ end
195
+ true
196
+ end
197
+
198
+ # Renew authorization
199
+ #
200
+ # @param [Integer] wait time before attempt to renew
201
+ #
202
+ # @return [TrueClass] always true
203
+ def renew_authorization(wait = 0)
204
+ true
205
+ end
206
+
207
+ # Update authorization state
208
+ # If state has changed, make external callbacks to notify of change
209
+ # Do not update state once set to :closed
210
+ #
211
+ # @param [Hash] value for new state
212
+ #
213
+ # @return [Symbol] updated state
214
+ #
215
+ # @raise [ArgumentError] invalid state transition
216
+ def state=(value)
217
+ return if @state == :closed
218
+ unless PERMITTED_STATE_TRANSITIONS[@state].include?(value)
219
+ raise ArgumentError, "Invalid state transition: #{@state.inspect} -> #{value.inspect}"
220
+ end
221
+
222
+ case value
223
+ when :pending, :closed
224
+ @stats["state"].update(value.to_s)
225
+ @state = value
226
+ when :authorized, :unauthorized, :expired, :failed
227
+ if value != @state
228
+ @stats["state"].update(value.to_s)
229
+ @state = value
230
+ (@status_callbacks || []).each do |callback|
231
+ begin
232
+ callback.call(:auth, @state)
233
+ rescue StandardError => e
234
+ Log.error("Failed status callback", e)
235
+ @stats["exceptions"].track("status", e)
236
+ end
237
+ end
238
+ end
239
+ else
240
+ raise ArgumentError, "Unknown state: #{value.inspect}"
241
+ end
242
+ @state
243
+ end
244
+
245
+ end # AuthClient
246
+
247
+ end # RightScale
@@ -0,0 +1,369 @@
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
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require 'restclient'
25
+
26
+ module RightScale
27
+
28
+ # HTTP REST client for request balanced access to RightScale servers
29
+ # It is intended for use by instance agents and by infrastructure servers
30
+ # and therefore supports both session cookie and global session-based authentication
31
+ class BalancedHttpClient
32
+
33
+ # When server not responding and retry is recommended
34
+ class NotResponding < Exceptions::NestedException; end
35
+
36
+ # HTTP status codes for which a retry is warranted, which is limited to when server
37
+ # is not accessible for some reason (502, 503) or server response indicates that
38
+ # the request could not be routed for some retryable reason (504)
39
+ RETRY_STATUS_CODES = [502, 503, 504]
40
+
41
+ # Default time for HTTP connection to open
42
+ DEFAULT_OPEN_TIMEOUT = 2
43
+
44
+ # Default time to wait for health check response
45
+ HEALTH_CHECK_TIMEOUT = 5
46
+
47
+ # Default time to wait for response from request
48
+ DEFAULT_REQUEST_TIMEOUT = 30
49
+
50
+ # Default health check path
51
+ DEFAULT_HEALTH_CHECK_PATH = "/health-check"
52
+
53
+ # Text used for filtered parameter value
54
+ FILTERED_PARAM_VALUE = "<hidden>"
55
+
56
+ # Environment variables to examine for proxy settings, in order
57
+ PROXY_ENVIRONMENT_VARIABLES = ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', 'ALL_PROXY']
58
+
59
+ # Create client for making HTTP REST requests
60
+ #
61
+ # @param [Array, String] urls of server being accessed as array or comma-separated string
62
+ #
63
+ # @option options [String] :api_version for X-API-Version header
64
+ # @option options [String] :server_name of server for use in exceptions; defaults to host name
65
+ # @option options [String] :health_check_path in URI for health check resource;
66
+ # defaults to DEFAULT_HEALTH_CHECK_PATH
67
+ # @option options [Array] :filter_params symbols or strings for names of request parameters
68
+ # whose values are to be hidden when logging; can be augmented on individual requests
69
+ def initialize(urls, options = {})
70
+ @urls = split(urls)
71
+ @api_version = options[:api_version]
72
+ @server_name = options[:server_name]
73
+ @filter_params = (options[:filter_params] || []).map { |p| p.to_s }
74
+
75
+ # Create health check proc for use by request balancer
76
+ # Strip user and password from host name since health-check does not require authorization
77
+ @health_check_proc = Proc.new do |host|
78
+ uri = URI.parse(host)
79
+ uri.user = uri.password = nil
80
+ uri.path = uri.path + (options[:health_check_path] || DEFAULT_HEALTH_CHECK_PATH)
81
+ check_options = {
82
+ :open_timeout => DEFAULT_OPEN_TIMEOUT,
83
+ :timeout => HEALTH_CHECK_TIMEOUT }
84
+ check_options[:headers] = {"X-API-Version" => @api_version} if @api_version
85
+ RightSupport::Net::HTTPClient.new.get(uri.to_s, check_options)
86
+ end
87
+
88
+ # Initialize use of proxy if defined
89
+ if (proxy_var = PROXY_ENVIRONMENT_VARIABLES.detect { |v| ENV.has_key?(v) })
90
+ proxy = ENV[proxy_var].match(/^[[:alpha:]]+:\/\//) ? URI.parse(ENV[proxy_var]) : URI.parse("http://" + ENV[proxy_var])
91
+ RestClient.proxy = proxy.to_s if proxy
92
+ end
93
+
94
+ # Initialize request balancer
95
+ balancer_options = {
96
+ :policy => RightSupport::Net::LB::HealthCheck,
97
+ :health_check => @health_check_proc }
98
+ @balancer = RightSupport::Net::RequestBalancer.new(@urls, balancer_options)
99
+ end
100
+
101
+ def get(*args)
102
+ request(:get, *args)
103
+ end
104
+
105
+ def post(*args)
106
+ request(:post, *args)
107
+ end
108
+
109
+ def put(*args)
110
+ request(:put, *args)
111
+ end
112
+
113
+ def delete(*args)
114
+ request(:delete, *args)
115
+ end
116
+
117
+ def check_health(host = nil)
118
+ begin
119
+ @health_check_proc.call(host || @urls.first)
120
+ rescue StandardError => e
121
+ if e.respond_to?(:http_code) && RETRY_STATUS_CODES.include?(e.http_code)
122
+ raise NotResponding.new("#{@server_name || host} not responding", e)
123
+ else
124
+ raise
125
+ end
126
+ end
127
+ end
128
+
129
+ protected
130
+
131
+ # Make request via request balancer
132
+ # Encode request parameters and response using JSON
133
+ # Apply configured authorization scheme
134
+ # Log request/response with filtered parameters included for failure or debug mode
135
+ #
136
+ # @param [Symbol] verb for HTTP REST request
137
+ # @param [String] path in URI for desired resource
138
+ # @param [Hash] params for HTTP request
139
+ #
140
+ # @option options [Numeric] :open_timeout maximum wait for connection; defaults to DEFAULT_OPEN_TIMEOUT
141
+ # @option options [Numeric] :request_timeout maximum wait for response; defaults to DEFAULT_REQUEST_TIMEOUT
142
+ # @option options [String] :request_uuid uniquely identifying request; defaults to random generated UUID
143
+ # @option options [Array] :filter_params symbols or strings for names of request
144
+ # parameters whose values are to be hidden when logging in addition to the ones
145
+ # provided during object initialization
146
+ # @option options [Hash] :headers to be added to request
147
+ # @option options [Symbol] :log_level to use when logging information about the request other than errors
148
+ #
149
+ # @return [Object] result returned by receiver of request
150
+ #
151
+ # @raise [NotResponding] server not responding, recommend retry
152
+ def request(verb, path, params = {}, options = {})
153
+ result = nil
154
+ host_picked = nil
155
+ started_at = Time.now
156
+ filter = @filter_params + (options[:filter_params] || []).map { |p| p.to_s }
157
+ request_uuid = options[:request_uuid] || RightSupport::Data::UUID.generate
158
+ log_level = options[:log_level] || :info
159
+
160
+ Log.send(log_level, "Requesting #{verb.to_s.upcase} <#{request_uuid}> " + log_text(path, params, filter))
161
+
162
+ begin
163
+ request_options = {
164
+ :open_timeout => options[:open_timeout] || DEFAULT_OPEN_TIMEOUT,
165
+ :timeout => options[:request_timeout] || DEFAULT_REQUEST_TIMEOUT,
166
+ :headers => {
167
+ "X-Request-Lineage-Uuid" => request_uuid,
168
+ :accept => "application/json" } }
169
+ request_options[:headers]["X-API-Version"] = @api_version if @api_version
170
+ request_options[:headers].merge!(options[:headers]) if options[:headers]
171
+ request_options[:headers]["X-DEBUG"] = true if Log.level == :debug
172
+
173
+ if [:get, :delete].include?(verb)
174
+ request_options[:query] = params if params.is_a?(Hash) && params.any?
175
+ else
176
+ request_options[:payload] = JSON.dump(params)
177
+ request_options[:headers][:content_type] = "application/json"
178
+ end
179
+
180
+ response = @balancer.request do |host|
181
+ uri = URI.parse(host)
182
+ uri.user = uri.password = nil
183
+ host_picked = uri.to_s
184
+ RightSupport::Net::HTTPClient.new.send(verb, host + path, request_options)
185
+ end
186
+ rescue RightSupport::Net::NoResult => e
187
+ handle_no_result(e, host_picked) do |e2|
188
+ report_failure(host_picked, path, params, filter, request_uuid, started_at, e2)
189
+ end
190
+ rescue Exception => e
191
+ report_failure(host_picked, path, params, filter, request_uuid, started_at, e)
192
+ raise
193
+ end
194
+
195
+ response(response, host_picked, path, request_uuid, started_at, log_level)
196
+ end
197
+
198
+ # Process HTTP response by extracting result and logging request completion
199
+ # Extract result from location header for 201 response
200
+ # JSON-decode body of other 2xx responses except for 204
201
+ #
202
+ # @param [RestClient::Response, NilClass] response received
203
+ # @param [String] host server URL where request was completed
204
+ # @param [String] path in URI for desired resource
205
+ # @param [String] request_uuid uniquely identifying request
206
+ # @param [Time] started_at time for request
207
+ # @param [Symbol] log_level to use when logging information about the request
208
+ # other than errors
209
+ #
210
+ # @return [Object] JSON-decoded response body
211
+ def response(response, host, path, request_uuid, started_at, log_level)
212
+ result = nil
213
+ code = "nil"
214
+ length = "-"
215
+
216
+ if response
217
+ code = response.code
218
+ body = response.body
219
+ headers = response.headers
220
+ if (200..207).include?(code)
221
+ if code == 201
222
+ result = headers[:location]
223
+ elsif code == 204 || body.nil? || (body.respond_to?(:empty?) && body.empty?)
224
+ result = nil
225
+ else
226
+ result = JSON.load(body)
227
+ result = nil if result.respond_to?(:empty?) && result.empty?
228
+ end
229
+ end
230
+ length = headers[:content_length] || body.size
231
+ end
232
+
233
+ duration = "%.0fms" % ((Time.now - started_at) * 1000)
234
+ completed = "Completed <#{request_uuid}> in #{duration} | #{code} [#{host}#{path}] | #{length} bytes"
235
+ completed << " | #{result.inspect}" if Log.level == :debug
236
+ Log.send(log_level, completed)
237
+
238
+ result
239
+ end
240
+
241
+ # Handle no result from balancer
242
+ # Distinguish the not responding case since it likely warrants a retry by the client
243
+ # Also try to distinguish between the targeted server not responding and that server
244
+ # gatewaying to another server that is not responding, so that the receiver of
245
+ # the resulting exception is clearer as to the source of the problem
246
+ #
247
+ # @param [RightSupport::Net::NoResult] no_result exception raised by request balancer when it
248
+ # could not deliver request
249
+ # @param [String] host server URL where request was attempted
250
+ #
251
+ # @yield [exception] required block called for reporting exception of interest
252
+ # @yieldparam [Exception] exception extracted
253
+ #
254
+ # @return [TrueClass] always true
255
+ #
256
+ # @raise [NotResponding] server not responding, recommend retry
257
+ def handle_no_result(no_result, host)
258
+ server_name = @server_name || host
259
+ e = no_result.details.values.flatten.last
260
+ if no_result.details.empty?
261
+ yield(no_result)
262
+ raise NotResponding.new("#{server_name} not responding", no_result)
263
+ elsif (e.respond_to?(:http_code) && RETRY_STATUS_CODES.include?(e.http_code))
264
+ yield(e)
265
+ if e.http_code == 504 && (e.http_body && !e.http_body.empty?)
266
+ raise NotResponding.new(e.http_body, e)
267
+ else
268
+ raise NotResponding.new("#{server_name} not responding", e)
269
+ end
270
+ else
271
+ yield(e)
272
+ raise e
273
+ end
274
+ true
275
+ end
276
+
277
+ # Report request failure to logs
278
+ # Also report it as audit entry if an instance is targeted
279
+ #
280
+ # @param [String] host server URL where request was attempted if known
281
+ # @param [String] path in URI for desired resource
282
+ # @param [Hash] params for request
283
+ # @param [Array] filter list of parameters whose value is to be hidden
284
+ # @param [String] request_uuid uniquely identifying request
285
+ # @param [Time] started_at time for request
286
+ # @param [Exception, String] exception or message that should be logged
287
+ #
288
+ # @return [TrueClass] Always return true
289
+ def report_failure(host, path, params, filter, request_uuid, started_at, exception)
290
+ status = exception.respond_to?(:http_code) ? exception.http_code : "nil"
291
+ duration = "%.0fms" % ((Time.now - started_at) * 1000)
292
+ Log.error("Failed <#{request_uuid}> in #{duration} | #{status} " + log_text(path, params, filter, host, exception))
293
+ true
294
+ end
295
+
296
+ # Generate log text describing request and failure if any
297
+ #
298
+ # @param [String] path in URI for desired resource
299
+ # @param [Hash] params for HTTP request
300
+ # @param [Array, NilClass] filter augmentation to base filter list
301
+ # @param [String] host server URL where request was attempted if known
302
+ # @param [Exception, String, NilClass] exception or failure message that should be logged
303
+ #
304
+ # @return [String] Log text
305
+ def log_text(path, params, filter, host = nil, exception = nil)
306
+ filtered_params = (exception || Log.level == :debug) ? filter(params, filter).inspect : nil
307
+ text = filtered_params ? "#{path} #{filtered_params}" : path
308
+ text = "[#{host}#{text}]" if host
309
+ text << " | #{self.class.exception_text(exception)}" if exception
310
+ text
311
+ end
312
+
313
+ # Apply parameter hiding filter
314
+ #
315
+ # @param [Hash, Object] params to be filtered
316
+ # @param [Array] filter names of params as strings (not symbols) whose value is to be hidden
317
+ #
318
+ # @return [Hash] filtered parameters
319
+ def filter(params, filter)
320
+ if filter.empty? || !params.is_a?(Hash)
321
+ params
322
+ else
323
+ filtered_params = {}
324
+ params.each { |k, p| filtered_params[k] = filter.include?(k.to_s) ? FILTERED_PARAM_VALUE : p }
325
+ filtered_params
326
+ end
327
+ end
328
+
329
+ # Split string into an array unless nil or already an array
330
+ #
331
+ # @param [String, Array, NilClass] object to be split
332
+ # @param [String, Regex] pattern on which to split; defaults to comma
333
+ #
334
+ # @return [Array] split object
335
+ def split(object, pattern = /,\s*/)
336
+ object ? (object.is_a?(Array) ? object : object.split(pattern)) : []
337
+ end
338
+
339
+ public
340
+
341
+ # Extract text of exception for logging
342
+ # For RestClient exceptions extract useful info from http_body attribute
343
+ #
344
+ # @param [Exception, String, NilClass] exception or failure message
345
+ #
346
+ # @return [String] exception text
347
+ def self.exception_text(exception)
348
+ case exception
349
+ when String
350
+ exception
351
+ when RestClient::Exception
352
+ if exception.http_body.nil? || exception.http_body.empty? || exception.http_body =~ /^<html>| html /
353
+ exception.message
354
+ else
355
+ exception.inspect
356
+ end
357
+ when RightSupport::Net::NoResult, NotResponding
358
+ "#{exception.class}: #{exception.message}"
359
+ when Exception
360
+ backtrace = exception.backtrace ? " in\n" + exception.backtrace.join("\n") : ""
361
+ "#{exception.class}: #{exception.message}" + backtrace
362
+ else
363
+ ""
364
+ end
365
+ end
366
+
367
+ end # BalancedHttpClient
368
+
369
+ end # RightScale