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.
- data/README.rdoc +10 -8
- data/Rakefile +31 -5
- data/lib/right_agent.rb +6 -1
- data/lib/right_agent/actor.rb +4 -20
- data/lib/right_agent/actors/agent_manager.rb +1 -1
- data/lib/right_agent/agent.rb +357 -144
- data/lib/right_agent/agent_config.rb +7 -6
- data/lib/right_agent/agent_identity.rb +13 -11
- data/lib/right_agent/agent_tag_manager.rb +60 -64
- data/{spec/results_mock.rb → lib/right_agent/clients.rb} +10 -24
- data/lib/right_agent/clients/api_client.rb +383 -0
- data/lib/right_agent/clients/auth_client.rb +247 -0
- data/lib/right_agent/clients/balanced_http_client.rb +369 -0
- data/lib/right_agent/clients/base_retry_client.rb +495 -0
- data/lib/right_agent/clients/right_http_client.rb +279 -0
- data/lib/right_agent/clients/router_client.rb +493 -0
- data/lib/right_agent/command/command_io.rb +4 -4
- data/lib/right_agent/command/command_parser.rb +2 -2
- data/lib/right_agent/command/command_runner.rb +1 -1
- data/lib/right_agent/connectivity_checker.rb +179 -0
- data/lib/right_agent/core_payload_types/secure_document_location.rb +2 -2
- data/lib/right_agent/dispatcher.rb +12 -10
- data/lib/right_agent/enrollment_result.rb +16 -12
- data/lib/right_agent/exceptions.rb +34 -20
- data/lib/right_agent/history.rb +10 -5
- data/lib/right_agent/log.rb +5 -5
- data/lib/right_agent/minimal.rb +1 -0
- data/lib/right_agent/multiplexer.rb +1 -1
- data/lib/right_agent/offline_handler.rb +270 -0
- data/lib/right_agent/packets.rb +7 -7
- data/lib/right_agent/payload_formatter.rb +1 -1
- data/lib/right_agent/pending_requests.rb +128 -0
- data/lib/right_agent/platform.rb +1 -1
- data/lib/right_agent/protocol_version_mixin.rb +69 -0
- data/lib/right_agent/{idempotent_request.rb → retryable_request.rb} +7 -7
- data/lib/right_agent/scripts/agent_controller.rb +28 -26
- data/lib/right_agent/scripts/agent_deployer.rb +37 -22
- data/lib/right_agent/scripts/common_parser.rb +10 -3
- data/lib/right_agent/secure_identity.rb +1 -1
- data/lib/right_agent/sender.rb +299 -785
- data/lib/right_agent/serialize/secure_serializer.rb +3 -1
- data/lib/right_agent/serialize/secure_serializer_initializer.rb +2 -2
- data/lib/right_agent/serialize/serializable.rb +8 -3
- data/right_agent.gemspec +49 -18
- data/spec/agent_config_spec.rb +7 -7
- data/spec/agent_identity_spec.rb +7 -4
- data/spec/agent_spec.rb +43 -7
- data/spec/agent_tag_manager_spec.rb +72 -83
- data/spec/clients/api_client_spec.rb +423 -0
- data/spec/clients/auth_client_spec.rb +272 -0
- data/spec/clients/balanced_http_client_spec.rb +576 -0
- data/spec/clients/base_retry_client_spec.rb +635 -0
- data/spec/clients/router_client_spec.rb +594 -0
- data/spec/clients/spec_helper.rb +111 -0
- data/spec/command/command_io_spec.rb +1 -1
- data/spec/command/command_parser_spec.rb +1 -1
- data/spec/connectivity_checker_spec.rb +83 -0
- data/spec/dispatcher_spec.rb +3 -2
- data/spec/enrollment_result_spec.rb +2 -2
- data/spec/history_spec.rb +51 -39
- data/spec/offline_handler_spec.rb +340 -0
- data/spec/pending_requests_spec.rb +136 -0
- data/spec/{idempotent_request_spec.rb → retryable_request_spec.rb} +73 -73
- data/spec/sender_spec.rb +835 -1052
- data/spec/serialize/secure_serializer_spec.rb +3 -2
- data/spec/spec_helper.rb +54 -1
- 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
|