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
@@ -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. This directory should contain at least:
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
- # mapper.cert - mapper's' public certificate
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 = 22
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
- # (String):: Ordered list of directory paths to be searched
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.__send__(type, d)
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.__send__(type, d), name))
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
- # RightScale::Exceptions::Argument:: Invalid argument
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 RightScale::Exceptions::Argument, err if err
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
- # (RightScale::Exceptions::Argument):: Serialized agent identity is invalid
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 RightScale::Exceptions::Argument, "Invalid agent identity: #{serialized_id.inspect}" unless prefix && agent_type && token && bid
105
+ raise ArgumentError, "Invalid agent identity: #{serialized_id.inspect}" unless prefix && agent_type && token && bid
104
106
  base_id = bid.to_i
105
- raise RightScale::Exceptions::Argument, "Invalid agent identity base ID: #{bid ? bid : bid.inspect}" unless base_id.to_s == bid
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 = 10)
120
- if version < 10
121
- serialized_id = "nanite-#{serialized_id}" if self.valid_parts?(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[7..-1] if serialized_id =~ /^nanite-|^mapper-/
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
- do_query(nil, @agent.identity, options) do |result|
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
- # agent_ids(Array):: agent IDs to query or empty or nil
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, agent_ids = nil, options = {})
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, agent_ids, options) { |raw_response| yield raw_response }
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
- update_tags(new_tags, []) { |raw_response| yield raw_response if block_given? }
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
- update_tags([], old_tags) { |raw_response| yield raw_response if block_given? }
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
- update_tags([], @agent.tags) { |raw_response| yield raw_response }
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, "Must set agent= before using tag manager" unless @agent
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
- # agent_ids(Array):: IDs of agents to query with empty or nil meaning all agents in deployment
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, agent_ids = nil, options = {})
195
- raw = options[:raw] || false
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[:agent_ids] = ensure_flat_array_value(agent_ids) unless agent_ids.nil? || agent_ids.empty?
205
- request = RightScale::IdempotentRequest.new("/mapper/query_tags",
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: #{message}")
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
- # Ensures value is a flat array, making an array from the single value if
218
- # necessary.
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
- if value.kind_of?(Array)
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) 2009-2011 RightScale Inc
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
- # Mock for request results
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
- end
44
-
45
- end
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