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
@@ -44,10 +44,11 @@ module RightScale
|
|
44
44
|
#
|
45
45
|
# The certs directory contains the x.509 public certificate and keys needed
|
46
46
|
# to sign and encrypt all outgoing messages as well as to check the signature
|
47
|
-
# and decrypt any incoming messages.
|
47
|
+
# and decrypt any incoming messages. If AMQP is being used as the RightNet
|
48
|
+
# protocol, this directory should contain at least:
|
48
49
|
# <agent name>.key - agent's' private key
|
49
50
|
# <agent name>.cert - agent's' public certificate
|
50
|
-
#
|
51
|
+
# router.cert - router's' public certificate
|
51
52
|
#
|
52
53
|
# The scripts directory at a minimum contains the following:
|
53
54
|
# install.sh - script for installing standard and agent specific tools in /usr/bin
|
@@ -63,7 +64,7 @@ module RightScale
|
|
63
64
|
module AgentConfig
|
64
65
|
|
65
66
|
# Current agent protocol version
|
66
|
-
PROTOCOL_VERSION =
|
67
|
+
PROTOCOL_VERSION = 23
|
67
68
|
|
68
69
|
# Current agent protocol version
|
69
70
|
#
|
@@ -138,7 +139,7 @@ module RightScale
|
|
138
139
|
# dir(String|Array):: Directory path or ordered list of directory paths to be searched
|
139
140
|
#
|
140
141
|
# === Return
|
141
|
-
# (
|
142
|
+
# (Array):: Ordered list of directory paths to be searched
|
142
143
|
def self.root_dir=(dir)
|
143
144
|
@root_dirs = array(dir)
|
144
145
|
end
|
@@ -456,7 +457,7 @@ module RightScale
|
|
456
457
|
def self.all_dirs(type)
|
457
458
|
dirs = []
|
458
459
|
root_dirs.each do |d|
|
459
|
-
c = self.
|
460
|
+
c = self.send(type, d)
|
460
461
|
dirs << c if File.directory?(c)
|
461
462
|
end
|
462
463
|
dirs
|
@@ -466,7 +467,7 @@ module RightScale
|
|
466
467
|
def self.first_file(type, name)
|
467
468
|
file = nil
|
468
469
|
root_dirs.each do |d|
|
469
|
-
if File.exist?(f = File.join(self.
|
470
|
+
if File.exist?(f = File.join(self.send(type, d), name))
|
470
471
|
file = f
|
471
472
|
break
|
472
473
|
end
|
@@ -21,10 +21,12 @@
|
|
21
21
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
22
|
|
23
23
|
module RightScale
|
24
|
-
|
24
|
+
|
25
25
|
# Agent identity management
|
26
26
|
class AgentIdentity
|
27
27
|
|
28
|
+
include ProtocolVersionMixin
|
29
|
+
|
28
30
|
# Cutover time at which agents began using new separator
|
29
31
|
SEPARATOR_EPOCH = Time.at(1256702400) unless defined?(SEPARATOR_EPOCH) # Tue Oct 27 21:00:00 -0700 2009
|
30
32
|
|
@@ -47,8 +49,8 @@ module RightScale
|
|
47
49
|
# separator(String):: Character used to separate identity components, defaults to ID_SEPARATOR
|
48
50
|
#
|
49
51
|
# === Raise
|
50
|
-
#
|
51
|
-
def initialize(prefix, agent_type, base_id, token=nil, separator=nil)
|
52
|
+
# ArgumentError:: Invalid argument
|
53
|
+
def initialize(prefix, agent_type, base_id, token = nil, separator = nil)
|
52
54
|
err = "Prefix cannot contain '#{ID_SEPARATOR}'" if prefix && prefix.include?(ID_SEPARATOR)
|
53
55
|
err = "Prefix cannot contain '#{ID_SEPARATOR_OLD}'" if prefix && prefix.include?(ID_SEPARATOR_OLD)
|
54
56
|
err = "Agent type cannot contain '#{ID_SEPARATOR}'" if agent_type.include?(ID_SEPARATOR)
|
@@ -58,7 +60,7 @@ module RightScale
|
|
58
60
|
err = "Base ID must be a positive integer" unless base_id.kind_of?(Integer) && base_id >= 0
|
59
61
|
err = "Token cannot contain '#{ID_SEPARATOR}'" if token && token.include?(ID_SEPARATOR)
|
60
62
|
err = "Token cannot contain '#{ID_SEPARATOR_OLD}'" if token && token.include?(ID_SEPARATOR_OLD)
|
61
|
-
raise
|
63
|
+
raise ArgumentError, err if err
|
62
64
|
|
63
65
|
@separator = separator || ID_SEPARATOR
|
64
66
|
@prefix = prefix
|
@@ -97,12 +99,12 @@ module RightScale
|
|
97
99
|
# (AgentIdentity):: Corresponding agent identity
|
98
100
|
#
|
99
101
|
# === Raise
|
100
|
-
# (
|
102
|
+
# (ArgumentError):: Serialized agent identity is invalid
|
101
103
|
def self.parse(serialized_id)
|
102
104
|
prefix, agent_type, token, bid, separator = parts(self.compatible_serialized(serialized_id))
|
103
|
-
raise
|
105
|
+
raise ArgumentError, "Invalid agent identity: #{serialized_id.inspect}" unless prefix && agent_type && token && bid
|
104
106
|
base_id = bid.to_i
|
105
|
-
raise
|
107
|
+
raise ArgumentError, "Invalid agent identity base ID: #{bid ? bid : bid.inspect}" unless base_id.to_s == bid
|
106
108
|
|
107
109
|
AgentIdentity.new(prefix, agent_type, base_id, token, separator)
|
108
110
|
end
|
@@ -116,11 +118,11 @@ module RightScale
|
|
116
118
|
#
|
117
119
|
# === Return
|
118
120
|
# serialized_id(String):: Compatible serialized agent identity
|
119
|
-
def self.compatible_serialized(serialized_id, version =
|
120
|
-
if version
|
121
|
-
serialized_id =
|
121
|
+
def self.compatible_serialized(serialized_id, version = nil)
|
122
|
+
if version.nil? || ProtocolVersionMixin.can_handle_non_nanite_ids?(version)
|
123
|
+
serialized_id = serialized_id[7..-1] if serialized_id =~ /^nanite-|^mapper-|^router-/
|
122
124
|
else
|
123
|
-
serialized_id = serialized_id
|
125
|
+
serialized_id = "nanite-#{serialized_id}" if self.valid_parts?(serialized_id)
|
124
126
|
end
|
125
127
|
serialized_id
|
126
128
|
end
|
@@ -24,6 +24,7 @@ module RightScale
|
|
24
24
|
|
25
25
|
# Agent tags management
|
26
26
|
class AgentTagManager
|
27
|
+
|
27
28
|
include RightSupport::Ruby::EasySingleton
|
28
29
|
|
29
30
|
# (Agent) Agent being managed
|
@@ -43,7 +44,8 @@ module RightScale
|
|
43
44
|
# === Return
|
44
45
|
# true:: Always return true
|
45
46
|
def tags(options = {})
|
46
|
-
|
47
|
+
# TODO remove use of agent identity when fully drop AMQP
|
48
|
+
do_query(nil, @agent.self_href || @agent.identity, options) do |result|
|
47
49
|
if result.kind_of?(Hash)
|
48
50
|
yield(result.size == 1 ? result.values.first['tags'] : [])
|
49
51
|
else
|
@@ -77,7 +79,7 @@ module RightScale
|
|
77
79
|
#
|
78
80
|
# === Parameters
|
79
81
|
# tags(String, Array):: Tag or tags to query or empty
|
80
|
-
#
|
82
|
+
# hrefs(Array):: hrefs of resources to query with empty or nil meaning all instances in deployment
|
81
83
|
# options(Hash):: Request options
|
82
84
|
# :timeout(Integer):: timeout in seconds before giving up and yielding an error message
|
83
85
|
#
|
@@ -86,10 +88,10 @@ module RightScale
|
|
86
88
|
#
|
87
89
|
# === Return
|
88
90
|
# true:: Always return true
|
89
|
-
def query_tags_raw(tags,
|
91
|
+
def query_tags_raw(tags, hrefs = nil, options = {})
|
90
92
|
tags = ensure_flat_array_value(tags) unless tags.nil? || tags.empty?
|
91
93
|
options = options.merge(:raw => true)
|
92
|
-
do_query(tags,
|
94
|
+
do_query(tags, hrefs, options) { |raw_response| yield raw_response }
|
93
95
|
end
|
94
96
|
|
95
97
|
# Add given tags to agent
|
@@ -105,7 +107,7 @@ module RightScale
|
|
105
107
|
# true always return true
|
106
108
|
def add_tags(new_tags)
|
107
109
|
new_tags = ensure_flat_array_value(new_tags) unless new_tags.nil? || new_tags.empty?
|
108
|
-
|
110
|
+
do_update(new_tags, []) { |raw_response| yield raw_response if block_given? }
|
109
111
|
end
|
110
112
|
|
111
113
|
# Remove given tags from agent
|
@@ -121,42 +123,7 @@ module RightScale
|
|
121
123
|
# true always return true
|
122
124
|
def remove_tags(old_tags)
|
123
125
|
old_tags = ensure_flat_array_value(old_tags) unless old_tags.nil? || old_tags.empty?
|
124
|
-
|
125
|
-
end
|
126
|
-
|
127
|
-
# Runs a tag update with a list of new and old tags.
|
128
|
-
#
|
129
|
-
# === Parameters
|
130
|
-
# new_tags(Array):: new tags to add or empty
|
131
|
-
# old_tags(Array):: old tags to remove or empty
|
132
|
-
# block(Block):: optional callback for update response
|
133
|
-
#
|
134
|
-
# === Block
|
135
|
-
# A block is optional. If provided, should take one argument which will be set with the
|
136
|
-
# raw response
|
137
|
-
#
|
138
|
-
# === Return
|
139
|
-
# true:: Always return true
|
140
|
-
def update_tags(new_tags, old_tags, &block)
|
141
|
-
agent_check
|
142
|
-
tags = @agent.tags
|
143
|
-
tags += (new_tags || [])
|
144
|
-
tags -= (old_tags || [])
|
145
|
-
tags.uniq!
|
146
|
-
|
147
|
-
payload = {:new_tags => new_tags, :obsolete_tags => old_tags}
|
148
|
-
request = RightScale::IdempotentRequest.new("/mapper/update_tags", payload)
|
149
|
-
if block
|
150
|
-
# always yield raw response
|
151
|
-
request.callback do |_|
|
152
|
-
# refresh agent's copy of tags on successful update
|
153
|
-
@agent.tags = tags
|
154
|
-
block.call(request.raw_response)
|
155
|
-
end
|
156
|
-
request.errback { |message| block.call(request.raw_response || message) }
|
157
|
-
end
|
158
|
-
request.run
|
159
|
-
true
|
126
|
+
do_update([], old_tags) { |raw_response| yield raw_response if block_given? }
|
160
127
|
end
|
161
128
|
|
162
129
|
# Clear all agent tags
|
@@ -167,20 +134,20 @@ module RightScale
|
|
167
134
|
# === Return
|
168
135
|
# true::Always return true
|
169
136
|
def clear
|
170
|
-
|
137
|
+
do_update([], @agent.tags) { |raw_response| yield raw_response }
|
171
138
|
end
|
172
139
|
|
173
140
|
private
|
174
141
|
|
175
142
|
def agent_check
|
176
|
-
raise ArgumentError
|
143
|
+
raise ArgumentError.new("Must set agent= before using tag manager") unless @agent
|
177
144
|
end
|
178
145
|
|
179
146
|
# Runs a tag query with an optional list of tags.
|
180
147
|
#
|
181
148
|
# === Parameters
|
182
149
|
# tags(Array):: Tags to query or empty or nil
|
183
|
-
#
|
150
|
+
# hrefs(Array):: hrefs of resources to query with empty or nil meaning all instances in deployment
|
184
151
|
# options(Hash):: Request options
|
185
152
|
# :raw(Boolean):: true to yield raw tag response instead of unserialized tags
|
186
153
|
# :timeout(Integer):: timeout in seconds before giving up and yielding an error message
|
@@ -191,8 +158,8 @@ module RightScale
|
|
191
158
|
#
|
192
159
|
# === Return
|
193
160
|
# true:: Always return true
|
194
|
-
def do_query(tags = nil,
|
195
|
-
raw = options[:raw]
|
161
|
+
def do_query(tags = nil, hrefs = nil, options = {})
|
162
|
+
raw = options[:raw]
|
196
163
|
timeout = options[:timeout]
|
197
164
|
|
198
165
|
request_options = {}
|
@@ -201,21 +168,60 @@ module RightScale
|
|
201
168
|
agent_check
|
202
169
|
payload = {:agent_identity => @agent.identity}
|
203
170
|
payload[:tags] = ensure_flat_array_value(tags) unless tags.nil? || tags.empty?
|
204
|
-
payload[:
|
205
|
-
request = RightScale::
|
206
|
-
payload,
|
207
|
-
request_options)
|
171
|
+
payload[:hrefs] = ensure_flat_array_value(hrefs) unless hrefs.nil? || hrefs.empty?
|
172
|
+
request = RightScale::RetryableRequest.new("/router/query_tags", payload, request_options)
|
208
173
|
request.callback { |result| yield raw ? request.raw_response : result }
|
209
174
|
request.errback do |message|
|
210
|
-
Log.error("Failed to query tags
|
175
|
+
Log.error("Failed to query tags (#{message})")
|
211
176
|
yield((raw ? request.raw_response : nil) || message)
|
212
177
|
end
|
213
178
|
request.run
|
214
179
|
true
|
215
180
|
end
|
216
181
|
|
217
|
-
#
|
218
|
-
#
|
182
|
+
# Runs a tag update with a list of new or old tags
|
183
|
+
#
|
184
|
+
# === Parameters
|
185
|
+
# new_tags(Array):: new tags to add or empty
|
186
|
+
# old_tags(Array):: old tags to remove or empty
|
187
|
+
# block(Block):: optional callback for update response
|
188
|
+
#
|
189
|
+
# === Block
|
190
|
+
# A block is optional. If provided, should take one argument which will be set with the
|
191
|
+
# raw response
|
192
|
+
#
|
193
|
+
# === Return
|
194
|
+
# true:: Always return true
|
195
|
+
def do_update(new_tags, old_tags, &block)
|
196
|
+
agent_check
|
197
|
+
raise ArgumentError.new("Cannot add and remove tags in same update") if new_tags.any? && old_tags.any?
|
198
|
+
tags = @agent.tags
|
199
|
+
tags += new_tags
|
200
|
+
tags -= old_tags
|
201
|
+
tags.uniq!
|
202
|
+
|
203
|
+
if new_tags.any?
|
204
|
+
request = RightScale::RetryableRequest.new("/router/add_tags", {:tags => new_tags})
|
205
|
+
elsif old_tags.any?
|
206
|
+
request = RightScale::RetryableRequest.new("/router/delete_tags", {:tags => old_tags})
|
207
|
+
else
|
208
|
+
return
|
209
|
+
end
|
210
|
+
|
211
|
+
if block
|
212
|
+
# Always yield raw response
|
213
|
+
request.callback do |_|
|
214
|
+
# Refresh agent's copy of tags on successful update
|
215
|
+
@agent.tags = tags
|
216
|
+
block.call(request.raw_response)
|
217
|
+
end
|
218
|
+
request.errback { |message| block.call(request.raw_response || message) }
|
219
|
+
end
|
220
|
+
request.run
|
221
|
+
true
|
222
|
+
end
|
223
|
+
|
224
|
+
# Ensures value is a flat array, making an array from the single value if necessary
|
219
225
|
#
|
220
226
|
# === Parameters
|
221
227
|
# value(Object):: any kind of value
|
@@ -223,19 +229,9 @@ module RightScale
|
|
223
229
|
# === Return
|
224
230
|
# result(Array):: flat array value
|
225
231
|
def ensure_flat_array_value(value)
|
226
|
-
|
227
|
-
value.flatten!
|
228
|
-
else
|
229
|
-
value = [value]
|
230
|
-
end
|
231
|
-
value
|
232
|
+
value = Array(value).flatten.compact
|
232
233
|
end
|
233
234
|
|
234
235
|
end # AgentTagManager
|
235
236
|
|
236
|
-
# This class has been renamed as of RightAgent 0.9.6; provide an alias
|
237
|
-
# to the old typename
|
238
|
-
# TODO remove this alias for RightAgent 1.0
|
239
|
-
AgentTagsManager = AgentTagManager
|
240
|
-
|
241
237
|
end # RightScale
|
@@ -1,5 +1,5 @@
|
|
1
|
-
|
2
|
-
# Copyright (c)
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2013 RightScale Inc
|
3
3
|
#
|
4
4
|
# Permission is hereby granted, free of charge, to any person obtaining
|
5
5
|
# a copy of this software and associated documentation files (the
|
@@ -19,27 +19,13 @@
|
|
19
19
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
20
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
21
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
22
23
|
|
23
|
-
|
24
|
-
module RightScale
|
25
|
-
|
26
|
-
class ResultsMock
|
27
|
-
|
28
|
-
def initialize
|
29
|
-
@agent_id = AgentIdentity.generate
|
30
|
-
end
|
31
|
-
|
32
|
-
# Build a valid request results with given content
|
33
|
-
def success_results(content = nil, reply_to = '*test*1')
|
34
|
-
Result.new(AgentIdentity.generate, reply_to,
|
35
|
-
{ @agent_id => OperationResult.success(content) }, @agent_id)
|
36
|
-
end
|
37
|
-
|
38
|
-
def error_results(content, reply_to = '*test*1')
|
39
|
-
Result.new(AgentIdentity.generate, reply_to,
|
40
|
-
{ @agent_id => OperationResult.error(content) }, @agent_id)
|
41
|
-
end
|
24
|
+
CLIENTS_BASE_DIR = File.join(File.dirname(__FILE__), 'clients')
|
42
25
|
|
43
|
-
|
44
|
-
|
45
|
-
|
26
|
+
require File.normalize_path(File.join(CLIENTS_BASE_DIR, 'balanced_http_client'))
|
27
|
+
require File.normalize_path(File.join(CLIENTS_BASE_DIR, 'base_retry_client'))
|
28
|
+
require File.normalize_path(File.join(CLIENTS_BASE_DIR, 'auth_client'))
|
29
|
+
require File.normalize_path(File.join(CLIENTS_BASE_DIR, 'api_client'))
|
30
|
+
require File.normalize_path(File.join(CLIENTS_BASE_DIR, 'router_client'))
|
31
|
+
require File.normalize_path(File.join(CLIENTS_BASE_DIR, 'right_http_client'))
|
@@ -0,0 +1,383 @@
|
|
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
|
+
# HTTP interface to RightApi for use when mapping actor-based requests to API requests
|
27
|
+
class ApiClient < BaseRetryClient
|
28
|
+
|
29
|
+
# RightApi API version for use in X-API-Version header
|
30
|
+
API_VERSION = "1.5"
|
31
|
+
|
32
|
+
# Maximum length of an audit summary as enforced by RightApi
|
33
|
+
MAX_AUDIT_SUMMARY_LENGTH = 255
|
34
|
+
|
35
|
+
# Default time to wait for HTTP connection to open
|
36
|
+
DEFAULT_OPEN_TIMEOUT = 2
|
37
|
+
|
38
|
+
# Default time to wait for response from request, which is chosen to be 5 seconds greater
|
39
|
+
# than the response timeout inside the RightNet router
|
40
|
+
DEFAULT_REQUEST_TIMEOUT = 35
|
41
|
+
|
42
|
+
# Map from actor-based request paths to RightApi HTTP verb and path; only requests whose type
|
43
|
+
# matches an entry in this hash will be routed to the RightApi; all others will be routed to RightNet
|
44
|
+
API_MAP = {
|
45
|
+
"/auditor/create_entry" => [:post, "/audit_entries"],
|
46
|
+
"/auditor/update_entry" => [:post, "/audit_entries/:id/append"],
|
47
|
+
"/booter/declare" => [:post, "/right_net/booter/declare"],
|
48
|
+
"/booter/get_repositories" => [:get, "/right_net/booter/get_repositories"],
|
49
|
+
"/booter/get_boot_bundle" => [:get, "/right_net/booter/get_boot_bundle"],
|
50
|
+
"/booter/get_decommission_bundle" => [:get, "/right_net/booter/get_decommission_bundle"],
|
51
|
+
"/booter/get_missing_attributes" => [:get, "/right_net/booter/get_missing_attributes"],
|
52
|
+
"/booter/get_login_policy" => [:get, "/right_net/booter/get_login_policy"],
|
53
|
+
"/forwarder/schedule_right_script" => [:post, "/right_net/scheduler/bundle_right_script"],
|
54
|
+
"/forwarder/schedule_recipe" => [:post, "/right_net/scheduler/bundle_recipe"],
|
55
|
+
"/forwarder/shutdown" => [:post, "/right_net/scheduler/shutdown"],
|
56
|
+
"/key_server/retrieve_public_keys" => [:get, "/right_net/key_server/retrieve_public_keys"],
|
57
|
+
"/router/ping" => [:get, "/health-check"],
|
58
|
+
"/router/query_tags" => [:post, "/tags/by_tag"],
|
59
|
+
"/router/add_tags" => [:post, "/tags/multi_add"],
|
60
|
+
"/router/delete_tags" => [:post, "/tags/multi_delete"],
|
61
|
+
"/state_recorder/record" => [:put, "/right_net/state_recorder/record"],
|
62
|
+
"/storage_valet/get_planned_volumes" => [:get, "/right_net/storage_valet/get_planned_volumes"],
|
63
|
+
"/storage_valet/attach_volume" => [:post, "/right_net/storage_valet/attach_volume"],
|
64
|
+
"/storage_valet/detach_volume" => [:post, "/right_net/storage_valet/detach_volume"],
|
65
|
+
"/updater/update_inputs" => [:post, "/right_net/scheduler/update_inputs"],
|
66
|
+
"/vault/read_documents" => [:get, "/right_net/vault/read_documents"] }
|
67
|
+
|
68
|
+
# Symbols for audit request parameters whose values are to be hidden when logging
|
69
|
+
AUDIT_FILTER_PARAMS = ["detail", "text"]
|
70
|
+
|
71
|
+
# Resource href for this agent
|
72
|
+
attr_reader :self_href
|
73
|
+
|
74
|
+
# Create RightApi client of specified type
|
75
|
+
#
|
76
|
+
# @param [AuthClient] auth_client providing authorization session for HTTP requests
|
77
|
+
#
|
78
|
+
# @option options [Numeric] :open_timeout maximum wait for connection; defaults to DEFAULT_OPEN_TIMEOUT
|
79
|
+
# @option options [Numeric] :request_timeout maximum wait for response; defaults to DEFAULT_REQUEST_TIMEOUT
|
80
|
+
# @option options [Numeric] :retry_timeout maximum before stop retrying; defaults to DEFAULT_RETRY_TIMEOUT
|
81
|
+
# @option options [Array] :retry_intervals between successive retries; defaults to DEFAULT_RETRY_INTERVALS
|
82
|
+
# @option options [Boolean] :retry_enabled for requests that fail to connect or that return a retry result
|
83
|
+
# @option options [Numeric] :reconnect_interval for reconnect attempts after lose connectivity
|
84
|
+
# @option options [Proc] :exception_callback for unexpected exceptions
|
85
|
+
#
|
86
|
+
# @raise [ArgumentError] auth client does not support this client type
|
87
|
+
def initialize(auth_client, options)
|
88
|
+
init(:api, auth_client, options.merge(:server_name => "RightApi", :api_version => API_VERSION))
|
89
|
+
end
|
90
|
+
|
91
|
+
# Route a request to a single target or multiple targets with no response expected
|
92
|
+
# Persist the request en route to reduce the chance of it being lost at the expense of some
|
93
|
+
# additional network overhead
|
94
|
+
# Enqueue the request if the target is not currently available
|
95
|
+
# Never automatically retry the request if there is the possibility of it being duplicated
|
96
|
+
# Set time-to-live to be forever
|
97
|
+
#
|
98
|
+
# @param [String] type of request as path specifying actor and action
|
99
|
+
# @param [Hash, NilClass] payload for request
|
100
|
+
# @param [String, Hash, NilClass] target for request, which may be identity of specific
|
101
|
+
# target, hash for selecting potentially multiple targets, or nil if routing solely
|
102
|
+
# using type; hash may contain:
|
103
|
+
# [Array] :tags that must all be associated with a target for it to be selected
|
104
|
+
# [Hash] :scope for restricting routing which may contain:
|
105
|
+
# [Integer] :account id that agents must be associated with to be included
|
106
|
+
# [Integer] :shard id that agents must be in to be included, or if value is
|
107
|
+
# Packet::GLOBAL, ones with no shard id
|
108
|
+
# [Symbol] :selector for picking from qualified targets: :any or :all;
|
109
|
+
# defaults to :any
|
110
|
+
# @param [String, NilClass] token uniquely identifying this request;
|
111
|
+
# defaults to randomly generated ID
|
112
|
+
#
|
113
|
+
# @return [NilClass] always nil since there is no expected response to the request
|
114
|
+
#
|
115
|
+
# @raise [Exceptions::Unauthorized] authorization failed
|
116
|
+
# @raise [Exceptions::ConnectivityFailure] cannot connect to server, lost connection
|
117
|
+
# to it, or it is out of service or too busy to respond
|
118
|
+
# @raise [Exceptions::RetryableError] request failed but if retried may succeed
|
119
|
+
# @raise [Exceptions::Terminating] closing client and terminating service
|
120
|
+
# @raise [Exceptions::InternalServerError] internal error in server being accessed
|
121
|
+
def push(type, payload, target, token = nil)
|
122
|
+
map_request(type, payload, token)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Route a request to a single target with a response expected
|
126
|
+
# Automatically retry the request if a response is not received in a reasonable amount of time
|
127
|
+
# or if there is a non-delivery response indicating the target is not currently available
|
128
|
+
# Timeout the request if a response is not received in time, typically configured to 30 sec
|
129
|
+
# Because of retries there is the possibility of duplicated requests, and these are detected and
|
130
|
+
# discarded automatically for non-idempotent actions
|
131
|
+
# Allow the request to expire per the agent's configured time-to-live, typically 1 minute
|
132
|
+
#
|
133
|
+
# @param [String] type of request as path specifying actor and action
|
134
|
+
# @param [Hash, NilClass] payload for request
|
135
|
+
# @param [String, Hash, NilClass] target for request, which may be identity of specific
|
136
|
+
# target, hash for selecting targets of which one is picked randomly, or nil if routing solely
|
137
|
+
# using type; hash may contain:
|
138
|
+
# [Array] :tags that must all be associated with a target for it to be selected
|
139
|
+
# [Hash] :scope for restricting routing which may contain:
|
140
|
+
# [Integer] :account id that agents must be associated with to be included
|
141
|
+
# @param [String, NilClass] token uniquely identifying this request;
|
142
|
+
# defaults to randomly generated ID
|
143
|
+
#
|
144
|
+
# @return [Result, NilClass] response from request
|
145
|
+
#
|
146
|
+
# @raise [Exceptions::Unauthorized] authorization failed
|
147
|
+
# @raise [Exceptions::ConnectivityFailure] cannot connect to server, lost connection
|
148
|
+
# to it, or it is out of service or too busy to respond
|
149
|
+
# @raise [Exceptions::RetryableError] request failed but if retried may succeed
|
150
|
+
# @raise [Exceptions::Terminating] closing client and terminating service
|
151
|
+
# @raise [Exceptions::InternalServerError] internal error in server being accessed
|
152
|
+
def request(type, payload, target, token = nil)
|
153
|
+
map_request(type, payload, token)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Determine whether request supported by this client
|
157
|
+
#
|
158
|
+
# @param [String] type of request as path specifying actor and action
|
159
|
+
#
|
160
|
+
# @return [Array] HTTP verb and path
|
161
|
+
def support?(type)
|
162
|
+
API_MAP.has_key?(type)
|
163
|
+
end
|
164
|
+
|
165
|
+
protected
|
166
|
+
|
167
|
+
# Convert request to RightApi form and then make request via HTTP
|
168
|
+
#
|
169
|
+
# @param [String] type of request as path specifying actor and action
|
170
|
+
# @param [Hash, NilClass] payload for request
|
171
|
+
# @param [String, NilClass] token uniquely identifying this request;
|
172
|
+
# defaults to randomly generated ID
|
173
|
+
#
|
174
|
+
# @return [Object, NilClass] response from request
|
175
|
+
#
|
176
|
+
# @raise [Exceptions::Unauthorized] authorization failed
|
177
|
+
# @raise [Exceptions::ConnectivityFailure] cannot connect to server, lost connection
|
178
|
+
# to it, or it is too busy to respond
|
179
|
+
# @raise [Exceptions::RetryableError] request failed but if retried may succeed
|
180
|
+
# @raise [Exceptions::Terminating] closing client and terminating service
|
181
|
+
# @raise [Exceptions::InternalServerError] internal error in server being accessed
|
182
|
+
def map_request(type, payload, token)
|
183
|
+
verb, path = API_MAP[type]
|
184
|
+
raise ArgumentError, "Unsupported request type: #{type}" if path.nil?
|
185
|
+
actor, action = type.split("/")[1..-1]
|
186
|
+
path, params, options = parameterize(actor, action, payload, path)
|
187
|
+
if action == "query_tags"
|
188
|
+
map_query_tags(verb, params, action, token, options)
|
189
|
+
else
|
190
|
+
map_response(make_request(verb, path, params, action, token, options), path)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Convert response from request into required form where necessary
|
195
|
+
#
|
196
|
+
# @param [Object] response received
|
197
|
+
# @param [String] path in URI for desired resource
|
198
|
+
#
|
199
|
+
# @return [Object] converted response
|
200
|
+
def map_response(response, path)
|
201
|
+
case path
|
202
|
+
when "/audit_entries"
|
203
|
+
# Convert returned audit entry href to audit ID
|
204
|
+
response.sub!(/^.*\/api\/audit_entries\//, "") if response.is_a?(String)
|
205
|
+
when "/tags/by_resource", "/tags/by_tag"
|
206
|
+
# Extract tags for each instance resource from response array with members of form
|
207
|
+
# {"actions" => [], "links" => [{"rel" => "resource", "href" => <href>}, ...]}, "tags" => [{"name" => <tag>}, ...]
|
208
|
+
tags = {}
|
209
|
+
if response
|
210
|
+
response.each do |hash|
|
211
|
+
r = {}
|
212
|
+
hash["links"].each { |l| r[l["href"]] = {"tags" => []} if l["href"] =~ /instances/ }
|
213
|
+
hash["tags"].each { |t| r.each_key { |k| r[k]["tags"] << t["name"] } } if r.any?
|
214
|
+
tags.merge!(r)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
response = tags
|
218
|
+
end
|
219
|
+
response
|
220
|
+
end
|
221
|
+
|
222
|
+
# Convert tag query request into one or more API requests and then convert responses
|
223
|
+
# Currently only retrieving "instances" resources
|
224
|
+
#
|
225
|
+
# @param [Symbol] verb for HTTP REST request
|
226
|
+
# @param [Hash] params for HTTP request
|
227
|
+
# @param [String] action from request type
|
228
|
+
# @param [String, NilClass] token uniquely identifying this request;
|
229
|
+
# defaults to randomly generated ID
|
230
|
+
# @param [Hash] options augmenting or overriding default options for HTTP request
|
231
|
+
#
|
232
|
+
# @return [Hash] tags retrieved with resource href as key and tags array as value
|
233
|
+
def map_query_tags(verb, params, action, token, options)
|
234
|
+
response = {}
|
235
|
+
hrefs = params[:resource_hrefs] || []
|
236
|
+
hrefs.concat(query_by_tag(verb, params[:tags], action, token, options)) if params[:tags]
|
237
|
+
response = query_by_resource(verb, hrefs, action, token, options) if hrefs.any?
|
238
|
+
response
|
239
|
+
end
|
240
|
+
|
241
|
+
# Query API for resources with specified tags
|
242
|
+
#
|
243
|
+
# @param [Symbol] verb for HTTP REST request
|
244
|
+
# @param [Array] tags that all resources retrieved must have
|
245
|
+
# @param [String] action from request type
|
246
|
+
# @param [String, NilClass] token uniquely identifying this request;
|
247
|
+
# defaults to randomly generated ID
|
248
|
+
# @param [Hash] options augmenting or overriding default options for HTTP request
|
249
|
+
#
|
250
|
+
# @return [Array] resource hrefs
|
251
|
+
def query_by_tag(verb, tags, action, token, options)
|
252
|
+
path = "/tags/by_tag"
|
253
|
+
params = {:tags => tags, :match_all => false, :resource_type => "instances"}
|
254
|
+
map_response(make_request(verb, path, params, action, token, options), path).keys
|
255
|
+
end
|
256
|
+
|
257
|
+
# Query API for tags associated with a set of resources
|
258
|
+
#
|
259
|
+
# @param [Symbol] verb for HTTP REST request
|
260
|
+
# @param [Array] hrefs for resources whose tags are to be retrieved
|
261
|
+
# @param [String] action from request type
|
262
|
+
# @param [String, NilClass] token uniquely identifying this request;
|
263
|
+
# defaults to randomly generated ID
|
264
|
+
# @param [Hash] options augmenting or overriding default options for HTTP request
|
265
|
+
#
|
266
|
+
# @return [Hash] tags retrieved with resource href as key and tags array as value
|
267
|
+
def query_by_resource(verb, hrefs, action, token, options)
|
268
|
+
path = "/tags/by_resource"
|
269
|
+
params = {:resource_hrefs => hrefs}
|
270
|
+
map_response(make_request(verb, path, params, action, token, options), path)
|
271
|
+
end
|
272
|
+
|
273
|
+
# Convert payload to HTTP parameters
|
274
|
+
#
|
275
|
+
# @param [String] actor from request type
|
276
|
+
# @param [String] action from request type
|
277
|
+
# @param [Hash, NilClass] payload for request
|
278
|
+
# @param [String] path in URI for desired resource
|
279
|
+
#
|
280
|
+
# @return [Array] path string and parameters and options hashes
|
281
|
+
def parameterize(actor, action, payload, path)
|
282
|
+
options = {}
|
283
|
+
params = {}
|
284
|
+
if actor == "auditor"
|
285
|
+
path = path.sub(/:id/, payload[:audit_id].to_s || "")
|
286
|
+
params = parameterize_audit(action, payload)
|
287
|
+
options = {:filter_params => AUDIT_FILTER_PARAMS}
|
288
|
+
elsif actor == "router" && action =~ /_tags/
|
289
|
+
if action != "query_tags"
|
290
|
+
params[:resource_hrefs] = [@self_href]
|
291
|
+
else
|
292
|
+
params[:resource_hrefs] = Array(payload[:hrefs]).flatten.compact if payload[:hrefs]
|
293
|
+
end
|
294
|
+
params[:tags] = Array(payload[:tags]).flatten.compact if payload[:tags]
|
295
|
+
else
|
296
|
+
# Can remove :agent_identity here since now carried in the authorization as the :agent
|
297
|
+
payload.each { |k, v| params[k.to_sym] = v if k.to_sym != :agent_identity } if payload.is_a?(Hash)
|
298
|
+
end
|
299
|
+
[path, params, options]
|
300
|
+
end
|
301
|
+
|
302
|
+
# Translate audit request payload to HTTP parameters
|
303
|
+
# Truncate audit summary to MAX_AUDIT_SUMMARY_LENGTH, the limit imposed by RightApi
|
304
|
+
#
|
305
|
+
# @param [String] action requested: create_entry or update_entry
|
306
|
+
# @param [Hash] payload from submitted request
|
307
|
+
#
|
308
|
+
# @return [Hash] HTTP request parameters
|
309
|
+
#
|
310
|
+
# @raise [ArgumentError] unknown request action
|
311
|
+
def parameterize_audit(action, payload)
|
312
|
+
params = {}
|
313
|
+
summary = non_blank(payload[:summary])
|
314
|
+
detail = non_blank(payload[:detail])
|
315
|
+
case action
|
316
|
+
when "create_entry"
|
317
|
+
params[:audit_entry] = {:auditee_href => @self_href}
|
318
|
+
params[:audit_entry][:summary] = truncate(summary, MAX_AUDIT_SUMMARY_LENGTH) if summary
|
319
|
+
params[:audit_entry][:detail] = detail if detail
|
320
|
+
if (user_email = non_blank(payload[:user_email]))
|
321
|
+
params[:user_email] = user_email
|
322
|
+
end
|
323
|
+
params[:notify] = payload[:category] if payload[:category]
|
324
|
+
when "update_entry"
|
325
|
+
params[:offset] = payload[:offset] if payload[:offset]
|
326
|
+
if summary
|
327
|
+
params[:summary] = truncate(summary, MAX_AUDIT_SUMMARY_LENGTH)
|
328
|
+
params[:notify] = payload[:category] if payload[:category]
|
329
|
+
end
|
330
|
+
params[:detail] = detail if detail
|
331
|
+
else
|
332
|
+
raise ArgumentError, "Unknown audit request action: #{action}"
|
333
|
+
end
|
334
|
+
params
|
335
|
+
end
|
336
|
+
|
337
|
+
# Truncate string if it exceeds maximum length
|
338
|
+
# Do length check with bytesize rather than size since this code
|
339
|
+
# is running with ruby 1.9.2 while the API uses 1.8.7, otherwise
|
340
|
+
# multi-byte characters could cause this code to be too lenient
|
341
|
+
#
|
342
|
+
# @param [String, NilClass] value to be truncated
|
343
|
+
# @param [Integer] max_length allowed; must be greater than 3
|
344
|
+
#
|
345
|
+
# @return [String, NilClass] truncated string or original value if it is not a string
|
346
|
+
#
|
347
|
+
# @raise [ArgumentError] max_length too small
|
348
|
+
def truncate(value, max_length)
|
349
|
+
raise ArgumentError, "max_length must be greater than 3" if max_length <= 3
|
350
|
+
if value.is_a?(String) && value.bytesize > max_length
|
351
|
+
max_truncated = max_length - 3
|
352
|
+
truncated = value[0, max_truncated]
|
353
|
+
while truncated.bytesize > max_truncated do
|
354
|
+
truncated.chop!
|
355
|
+
end
|
356
|
+
truncated + "..."
|
357
|
+
else
|
358
|
+
value
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
# Determine whether value is non-blank
|
363
|
+
#
|
364
|
+
# @param [String, NilClass] value to be tested
|
365
|
+
#
|
366
|
+
# @return [String, NilClass] value if non-blank, otherwise nil
|
367
|
+
def non_blank(value)
|
368
|
+
value && !value.empty? ? value : nil
|
369
|
+
end
|
370
|
+
|
371
|
+
# Perform any other steps needed to make this client fully usable
|
372
|
+
# once HTTP client has been created and server known to be accessible
|
373
|
+
#
|
374
|
+
# @return [TrueClass] always true
|
375
|
+
def enable_use
|
376
|
+
result = make_request(:get, "/sessions/instance", {}, "instance")
|
377
|
+
@self_href = result["links"].select { |link| link["rel"] == "self" }.first["href"]
|
378
|
+
true
|
379
|
+
end
|
380
|
+
|
381
|
+
end # ApiClient
|
382
|
+
|
383
|
+
end # RightScale
|