choria-mcorpc-support 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/mco +64 -0
- data/lib/mcollective.rb +63 -0
- data/lib/mcollective/agent.rb +5 -0
- data/lib/mcollective/agents.rb +149 -0
- data/lib/mcollective/aggregate.rb +85 -0
- data/lib/mcollective/aggregate/average.ddl +33 -0
- data/lib/mcollective/aggregate/average.rb +29 -0
- data/lib/mcollective/aggregate/base.rb +40 -0
- data/lib/mcollective/aggregate/result.rb +9 -0
- data/lib/mcollective/aggregate/result/base.rb +25 -0
- data/lib/mcollective/aggregate/result/collection_result.rb +19 -0
- data/lib/mcollective/aggregate/result/numeric_result.rb +13 -0
- data/lib/mcollective/aggregate/sum.ddl +33 -0
- data/lib/mcollective/aggregate/sum.rb +18 -0
- data/lib/mcollective/aggregate/summary.ddl +33 -0
- data/lib/mcollective/aggregate/summary.rb +53 -0
- data/lib/mcollective/application.rb +365 -0
- data/lib/mcollective/application/completion.rb +104 -0
- data/lib/mcollective/application/describe_filter.rb +87 -0
- data/lib/mcollective/application/facts.rb +62 -0
- data/lib/mcollective/application/find.rb +23 -0
- data/lib/mcollective/application/help.rb +28 -0
- data/lib/mcollective/application/inventory.rb +344 -0
- data/lib/mcollective/application/ping.rb +82 -0
- data/lib/mcollective/application/plugin.rb +369 -0
- data/lib/mcollective/application/rpc.rb +111 -0
- data/lib/mcollective/applications.rb +134 -0
- data/lib/mcollective/cache.rb +145 -0
- data/lib/mcollective/client.rb +353 -0
- data/lib/mcollective/config.rb +245 -0
- data/lib/mcollective/connector.rb +18 -0
- data/lib/mcollective/connector/base.rb +26 -0
- data/lib/mcollective/data.rb +91 -0
- data/lib/mcollective/data/agent_data.ddl +22 -0
- data/lib/mcollective/data/agent_data.rb +17 -0
- data/lib/mcollective/data/base.rb +67 -0
- data/lib/mcollective/data/collective_data.ddl +20 -0
- data/lib/mcollective/data/collective_data.rb +9 -0
- data/lib/mcollective/data/fact_data.ddl +28 -0
- data/lib/mcollective/data/fact_data.rb +55 -0
- data/lib/mcollective/data/fstat_data.ddl +89 -0
- data/lib/mcollective/data/fstat_data.rb +56 -0
- data/lib/mcollective/data/result.rb +45 -0
- data/lib/mcollective/ddl.rb +113 -0
- data/lib/mcollective/ddl/agentddl.rb +253 -0
- data/lib/mcollective/ddl/base.rb +217 -0
- data/lib/mcollective/ddl/dataddl.rb +56 -0
- data/lib/mcollective/ddl/discoveryddl.rb +52 -0
- data/lib/mcollective/ddl/validatorddl.rb +6 -0
- data/lib/mcollective/discovery.rb +143 -0
- data/lib/mcollective/discovery/flatfile.ddl +11 -0
- data/lib/mcollective/discovery/flatfile.rb +48 -0
- data/lib/mcollective/discovery/mc.ddl +11 -0
- data/lib/mcollective/discovery/mc.rb +30 -0
- data/lib/mcollective/discovery/stdin.ddl +11 -0
- data/lib/mcollective/discovery/stdin.rb +68 -0
- data/lib/mcollective/exceptions.rb +28 -0
- data/lib/mcollective/facts.rb +39 -0
- data/lib/mcollective/facts/base.rb +100 -0
- data/lib/mcollective/facts/yaml_facts.rb +65 -0
- data/lib/mcollective/generators.rb +7 -0
- data/lib/mcollective/generators/agent_generator.rb +51 -0
- data/lib/mcollective/generators/base.rb +46 -0
- data/lib/mcollective/generators/data_generator.rb +51 -0
- data/lib/mcollective/generators/templates/action_snippet.erb +13 -0
- data/lib/mcollective/generators/templates/data_input_snippet.erb +7 -0
- data/lib/mcollective/generators/templates/ddl.erb +8 -0
- data/lib/mcollective/generators/templates/plugin.erb +7 -0
- data/lib/mcollective/log.rb +118 -0
- data/lib/mcollective/logger.rb +5 -0
- data/lib/mcollective/logger/base.rb +77 -0
- data/lib/mcollective/logger/console_logger.rb +61 -0
- data/lib/mcollective/logger/file_logger.rb +53 -0
- data/lib/mcollective/logger/syslog_logger.rb +53 -0
- data/lib/mcollective/matcher.rb +224 -0
- data/lib/mcollective/matcher/parser.rb +128 -0
- data/lib/mcollective/matcher/scanner.rb +241 -0
- data/lib/mcollective/message.rb +248 -0
- data/lib/mcollective/monkey_patches.rb +152 -0
- data/lib/mcollective/optionparser.rb +197 -0
- data/lib/mcollective/pluginmanager.rb +180 -0
- data/lib/mcollective/pluginpackager.rb +98 -0
- data/lib/mcollective/pluginpackager/agent_definition.rb +94 -0
- data/lib/mcollective/pluginpackager/debpackage_packager.rb +237 -0
- data/lib/mcollective/pluginpackager/modulepackage_packager.rb +127 -0
- data/lib/mcollective/pluginpackager/ospackage_packager.rb +59 -0
- data/lib/mcollective/pluginpackager/rpmpackage_packager.rb +180 -0
- data/lib/mcollective/pluginpackager/standard_definition.rb +69 -0
- data/lib/mcollective/pluginpackager/templates/debian/Makefile.erb +7 -0
- data/lib/mcollective/pluginpackager/templates/debian/changelog.erb +5 -0
- data/lib/mcollective/pluginpackager/templates/debian/compat.erb +1 -0
- data/lib/mcollective/pluginpackager/templates/debian/control.erb +15 -0
- data/lib/mcollective/pluginpackager/templates/debian/copyright.erb +8 -0
- data/lib/mcollective/pluginpackager/templates/debian/rules.erb +6 -0
- data/lib/mcollective/pluginpackager/templates/module/Modulefile.erb +5 -0
- data/lib/mcollective/pluginpackager/templates/module/README.md.erb +37 -0
- data/lib/mcollective/pluginpackager/templates/module/_manifest.pp.erb +9 -0
- data/lib/mcollective/pluginpackager/templates/redhat/rpm_spec.erb +63 -0
- data/lib/mcollective/registration/base.rb +91 -0
- data/lib/mcollective/rpc.rb +182 -0
- data/lib/mcollective/rpc/actionrunner.rb +158 -0
- data/lib/mcollective/rpc/agent.rb +374 -0
- data/lib/mcollective/rpc/audit.rb +38 -0
- data/lib/mcollective/rpc/client.rb +1066 -0
- data/lib/mcollective/rpc/helpers.rb +321 -0
- data/lib/mcollective/rpc/progress.rb +63 -0
- data/lib/mcollective/rpc/reply.rb +87 -0
- data/lib/mcollective/rpc/request.rb +86 -0
- data/lib/mcollective/rpc/result.rb +90 -0
- data/lib/mcollective/rpc/stats.rb +294 -0
- data/lib/mcollective/runnerstats.rb +90 -0
- data/lib/mcollective/security.rb +26 -0
- data/lib/mcollective/security/base.rb +244 -0
- data/lib/mcollective/shell.rb +126 -0
- data/lib/mcollective/ssl.rb +285 -0
- data/lib/mcollective/util.rb +579 -0
- data/lib/mcollective/validator.rb +85 -0
- data/lib/mcollective/validator/array_validator.ddl +7 -0
- data/lib/mcollective/validator/array_validator.rb +9 -0
- data/lib/mcollective/validator/ipv4address_validator.ddl +7 -0
- data/lib/mcollective/validator/ipv4address_validator.rb +16 -0
- data/lib/mcollective/validator/ipv6address_validator.ddl +7 -0
- data/lib/mcollective/validator/ipv6address_validator.rb +16 -0
- data/lib/mcollective/validator/length_validator.ddl +7 -0
- data/lib/mcollective/validator/length_validator.rb +11 -0
- data/lib/mcollective/validator/regex_validator.ddl +7 -0
- data/lib/mcollective/validator/regex_validator.rb +9 -0
- data/lib/mcollective/validator/shellsafe_validator.ddl +7 -0
- data/lib/mcollective/validator/shellsafe_validator.rb +13 -0
- data/lib/mcollective/validator/typecheck_validator.ddl +7 -0
- data/lib/mcollective/validator/typecheck_validator.rb +28 -0
- metadata +215 -0
@@ -0,0 +1,374 @@
|
|
1
|
+
module MCollective
|
2
|
+
module RPC
|
3
|
+
# A wrapper around the traditional agent, it takes care of a lot of the tedious setup
|
4
|
+
# you would do for each agent allowing you to just create methods following a naming
|
5
|
+
# standard leaving the heavy lifting up to this clas.
|
6
|
+
#
|
7
|
+
# See https://docs.puppetlabs.com/mcollective/simplerpc/agents.html
|
8
|
+
#
|
9
|
+
# It only really makes sense to use this with a Simple RPC client on the other end, basic
|
10
|
+
# usage would be:
|
11
|
+
#
|
12
|
+
# module MCollective
|
13
|
+
# module Agent
|
14
|
+
# class Helloworld<RPC::Agent
|
15
|
+
# action "hello" do
|
16
|
+
# reply[:msg] = "Hello #{request[:name]}"
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# action "foo" do
|
20
|
+
# implemented_by "/some/script.sh"
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# If you wish to implement the logic for an action using an external script use the
|
27
|
+
# implemented_by method that will cause your script to be run with 2 arguments.
|
28
|
+
#
|
29
|
+
# The first argument is a file containing JSON with the request and the 2nd argument
|
30
|
+
# is where the script should save its output as a JSON hash.
|
31
|
+
#
|
32
|
+
# We also currently have the validation code in here, this will be moved to plugins soon.
|
33
|
+
class Agent
|
34
|
+
attr_accessor :reply, :request, :agent_name
|
35
|
+
attr_reader :logger, :config, :timeout, :ddl, :meta
|
36
|
+
|
37
|
+
def initialize
|
38
|
+
@agent_name = self.class.to_s.split("::").last.downcase
|
39
|
+
|
40
|
+
load_ddl
|
41
|
+
|
42
|
+
@logger = Log.instance
|
43
|
+
@config = Config.instance
|
44
|
+
|
45
|
+
# if we have a global authorization provider enable it
|
46
|
+
# plugins can still override it per plugin
|
47
|
+
self.class.authorized_by(@config.rpcauthprovider) if @config.rpcauthorization
|
48
|
+
|
49
|
+
startup_hook
|
50
|
+
end
|
51
|
+
|
52
|
+
def load_ddl
|
53
|
+
@ddl = DDL.new(@agent_name, :agent)
|
54
|
+
@meta = @ddl.meta
|
55
|
+
@timeout = @meta[:timeout] || 10
|
56
|
+
|
57
|
+
rescue Exception => e
|
58
|
+
Log.error("Failed to load DDL for the '%s' agent, DDLs are required: %s: %s" % [@agent_name, e.class, e.to_s])
|
59
|
+
raise DDLValidationError
|
60
|
+
end
|
61
|
+
|
62
|
+
def handlemsg(msg, connection)
|
63
|
+
@request = RPC::Request.new(msg, @ddl)
|
64
|
+
@reply = RPC::Reply.new(@request.action, @ddl)
|
65
|
+
|
66
|
+
begin
|
67
|
+
# Incoming requests need to be validated against the DDL thus reusing
|
68
|
+
# all the work users put into creating DDLs and creating a consistent
|
69
|
+
# quality of input validation everywhere with the a simple once off
|
70
|
+
# investment of writing a DDL
|
71
|
+
@request.validate!
|
72
|
+
|
73
|
+
# Calls the authorization plugin if any is defined
|
74
|
+
# if this raises an exception we wil just skip processing this
|
75
|
+
# message
|
76
|
+
authorization_hook(@request) if respond_to?("authorization_hook")
|
77
|
+
|
78
|
+
# Audits the request, currently continues processing the message
|
79
|
+
# we should make this a configurable so that an audit failure means
|
80
|
+
# a message wont be processed by this node depending on config
|
81
|
+
audit_request(@request, connection)
|
82
|
+
|
83
|
+
before_processing_hook(msg, connection)
|
84
|
+
|
85
|
+
if respond_to?("#{@request.action}_action")
|
86
|
+
send("#{@request.action}_action")
|
87
|
+
else
|
88
|
+
raise UnknownRPCAction, "Unknown action '#{@request.action}' for agent '#{@request.agent}'"
|
89
|
+
end
|
90
|
+
rescue RPCAborted => e
|
91
|
+
@reply.fail e.to_s, 1
|
92
|
+
|
93
|
+
rescue UnknownRPCAction => e
|
94
|
+
@reply.fail e.to_s, 2
|
95
|
+
|
96
|
+
rescue MissingRPCData => e
|
97
|
+
@reply.fail e.to_s, 3
|
98
|
+
|
99
|
+
rescue InvalidRPCData, DDLValidationError => e
|
100
|
+
@reply.fail e.to_s, 4
|
101
|
+
|
102
|
+
rescue UnknownRPCError => e
|
103
|
+
Log.error("%s#%s failed: %s: %s" % [@agent_name, @request.action, e.class, e.to_s])
|
104
|
+
Log.error(e.backtrace.join("\n\t"))
|
105
|
+
@reply.fail e.to_s, 5
|
106
|
+
|
107
|
+
rescue Exception => e
|
108
|
+
Log.error("%s#%s failed: %s: %s" % [@agent_name, @request.action, e.class, e.to_s])
|
109
|
+
Log.error(e.backtrace.join("\n\t"))
|
110
|
+
@reply.fail e.to_s, 5
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
after_processing_hook
|
115
|
+
|
116
|
+
if @request.should_respond?
|
117
|
+
return @reply.to_hash
|
118
|
+
else
|
119
|
+
Log.debug("Client did not request a response, surpressing reply")
|
120
|
+
return nil
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# By default RPC Agents support a toggle in the configuration that
|
125
|
+
# can enable and disable them based on the agent name
|
126
|
+
#
|
127
|
+
# Example an agent called Foo can have:
|
128
|
+
#
|
129
|
+
# plugin.foo.activate_agent = false
|
130
|
+
#
|
131
|
+
# and this will prevent the agent from loading on this particular
|
132
|
+
# machine.
|
133
|
+
#
|
134
|
+
# Agents can use the activate_when helper to override this for example:
|
135
|
+
#
|
136
|
+
# activate_when do
|
137
|
+
# File.exist?("/usr/bin/puppet")
|
138
|
+
# end
|
139
|
+
def self.activate?
|
140
|
+
agent_name = self.to_s.split("::").last.downcase
|
141
|
+
config = Config.instance
|
142
|
+
|
143
|
+
Log.debug("Starting default activation checks for #{agent_name}")
|
144
|
+
|
145
|
+
# Check global state to determine if agent should be loaded
|
146
|
+
should_activate = config.activate_agents
|
147
|
+
|
148
|
+
# Check agent specific state to determine if agent should be loaded
|
149
|
+
should_activate = Util.str_to_bool(config.pluginconf.fetch("#{agent_name}.activate_agent",
|
150
|
+
should_activate))
|
151
|
+
|
152
|
+
unless should_activate
|
153
|
+
Log.debug("Found plugin configuration '#{agent_name}.activate_agent' with value '#{should_activate}'")
|
154
|
+
end
|
155
|
+
|
156
|
+
return should_activate
|
157
|
+
end
|
158
|
+
|
159
|
+
# Returns an array of actions this agent support
|
160
|
+
def self.actions
|
161
|
+
public_instance_methods.sort.grep(/_action$/).map do |method|
|
162
|
+
$1 if method =~ /(.+)_action$/
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
# Runs a command via the MC::Shell wrapper, options are as per MC::Shell
|
168
|
+
#
|
169
|
+
# The simplest use is:
|
170
|
+
#
|
171
|
+
# out = ""
|
172
|
+
# err = ""
|
173
|
+
# status = run("echo 1", :stdout => out, :stderr => err)
|
174
|
+
#
|
175
|
+
# reply[:out] = out
|
176
|
+
# reply[:error] = err
|
177
|
+
# reply[:exitstatus] = status
|
178
|
+
#
|
179
|
+
# This can be simplified as:
|
180
|
+
#
|
181
|
+
# reply[:exitstatus] = run("echo 1", :stdout => :out, :stderr => :error)
|
182
|
+
#
|
183
|
+
# You can set a command specific environment and cwd:
|
184
|
+
#
|
185
|
+
# run("echo 1", :cwd => "/tmp", :environment => {"FOO" => "BAR"})
|
186
|
+
#
|
187
|
+
# This will run 'echo 1' from /tmp with FOO=BAR in addition to a setting forcing
|
188
|
+
# LC_ALL = C. To prevent LC_ALL from being set either set it specifically or:
|
189
|
+
#
|
190
|
+
# run("echo 1", :cwd => "/tmp", :environment => nil)
|
191
|
+
#
|
192
|
+
# Exceptions here will be handled by the usual agent exception handler or any
|
193
|
+
# specific one you create, if you dont it will just fall through and be sent
|
194
|
+
# to the client.
|
195
|
+
#
|
196
|
+
# If the shell handler fails to return a Process::Status instance for exit
|
197
|
+
# status this method will return -1 as the exit status
|
198
|
+
def run(command, options={})
|
199
|
+
shellopts = {}
|
200
|
+
|
201
|
+
# force stderr and stdout to be strings as the library
|
202
|
+
# will append data to them if given using the << method.
|
203
|
+
#
|
204
|
+
# if the data pased to :stderr or :stdin is a Symbol
|
205
|
+
# add that into the reply hash with that Symbol
|
206
|
+
[:stderr, :stdout].each do |k|
|
207
|
+
if options.include?(k)
|
208
|
+
if options[k].is_a?(Symbol)
|
209
|
+
reply[ options[k] ] = ""
|
210
|
+
shellopts[k] = reply[ options[k] ]
|
211
|
+
else
|
212
|
+
if options[k].respond_to?("<<")
|
213
|
+
shellopts[k] = options[k]
|
214
|
+
else
|
215
|
+
reply.fail! "#{k} should support << while calling run(#{command})"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
[:stdin, :cwd, :environment, :timeout].each do |k|
|
222
|
+
if options.include?(k)
|
223
|
+
shellopts[k] = options[k]
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
shell = Shell.new(command, shellopts)
|
228
|
+
|
229
|
+
shell.runcommand
|
230
|
+
|
231
|
+
if options[:chomp]
|
232
|
+
shellopts[:stdout].chomp! if shellopts[:stdout].is_a?(String)
|
233
|
+
shellopts[:stderr].chomp! if shellopts[:stderr].is_a?(String)
|
234
|
+
end
|
235
|
+
|
236
|
+
shell.status.exitstatus rescue -1
|
237
|
+
end
|
238
|
+
|
239
|
+
# Registers meta data for the introspection hash
|
240
|
+
def self.metadata(data)
|
241
|
+
agent = File.basename(caller.first).split(":").first
|
242
|
+
|
243
|
+
Log.warn("Setting metadata in agents has been deprecated, DDL files are now being used for this information. Please update the '#{agent}' agent")
|
244
|
+
end
|
245
|
+
|
246
|
+
# Creates the needed activate? class in a manner similar to the other
|
247
|
+
# helpers like action, authorized_by etc
|
248
|
+
#
|
249
|
+
# activate_when do
|
250
|
+
# File.exist?("/usr/bin/puppet")
|
251
|
+
# end
|
252
|
+
def self.activate_when(&block)
|
253
|
+
(class << self; self; end).instance_eval do
|
254
|
+
define_method("activate?", &block)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# Creates a new action with the block passed and sets some defaults
|
259
|
+
#
|
260
|
+
# action "status" do
|
261
|
+
# # logic here to restart service
|
262
|
+
# end
|
263
|
+
def self.action(name, &block)
|
264
|
+
raise "Need to pass a body for the action" unless block_given?
|
265
|
+
|
266
|
+
self.module_eval { define_method("#{name}_action", &block) }
|
267
|
+
end
|
268
|
+
|
269
|
+
# Helper that creates a method on the class that will call your authorization
|
270
|
+
# plugin. If your plugin raises an exception that will abort the request
|
271
|
+
def self.authorized_by(plugin)
|
272
|
+
plugin = plugin.to_s.capitalize
|
273
|
+
|
274
|
+
# turns foo_bar into FooBar
|
275
|
+
plugin = plugin.to_s.split("_").map {|v| v.capitalize}.join
|
276
|
+
pluginname = "MCollective::Util::#{plugin}"
|
277
|
+
|
278
|
+
PluginManager.loadclass(pluginname) unless MCollective::Util.constants.include?(plugin)
|
279
|
+
|
280
|
+
class_eval("
|
281
|
+
def authorization_hook(request)
|
282
|
+
#{pluginname}.authorize(request)
|
283
|
+
end
|
284
|
+
")
|
285
|
+
end
|
286
|
+
|
287
|
+
# Validates a data member, if validation is a regex then it will try to match it
|
288
|
+
# else it supports testing object types only:
|
289
|
+
#
|
290
|
+
# validate :msg, String
|
291
|
+
# validate :msg, /^[\w\s]+$/
|
292
|
+
#
|
293
|
+
# There are also some special helper validators:
|
294
|
+
#
|
295
|
+
# validate :command, :shellsafe
|
296
|
+
# validate :command, :ipv6address
|
297
|
+
# validate :command, :ipv4address
|
298
|
+
# validate :command, :boolean
|
299
|
+
# validate :command, ["start", "stop"]
|
300
|
+
#
|
301
|
+
# It will raise appropriate exceptions that the RPC system understand
|
302
|
+
def validate(key, validation)
|
303
|
+
raise MissingRPCData, "please supply a #{key} argument" unless @request.include?(key)
|
304
|
+
|
305
|
+
Validator.validate(@request[key], validation)
|
306
|
+
rescue ValidatorError => e
|
307
|
+
raise InvalidRPCData, "Input %s did not pass validation: %s" % [ key, e.message ]
|
308
|
+
end
|
309
|
+
|
310
|
+
# convenience wrapper around Util#shellescape
|
311
|
+
def shellescape(str)
|
312
|
+
Util.shellescape(str)
|
313
|
+
end
|
314
|
+
|
315
|
+
# handles external actions
|
316
|
+
def implemented_by(command, type=:json)
|
317
|
+
runner = ActionRunner.new(command, request, type)
|
318
|
+
|
319
|
+
res = runner.run
|
320
|
+
|
321
|
+
reply.fail! "Did not receive data from #{command}" unless res.include?(:data)
|
322
|
+
reply.fail! "Reply data from #{command} is not a Hash" unless res[:data].is_a?(Hash)
|
323
|
+
|
324
|
+
reply.data.merge!(res[:data])
|
325
|
+
|
326
|
+
if res[:exitstatus] > 0
|
327
|
+
reply.fail "Failed to run #{command}: #{res[:stderr]}", res[:exitstatus]
|
328
|
+
end
|
329
|
+
rescue Exception => e
|
330
|
+
Log.warn("Unhandled #{e.class} exception during #{request.agent}##{request.action}: #{e}")
|
331
|
+
reply.fail! "Unexpected failure calling #{command}: #{e.class}: #{e}"
|
332
|
+
end
|
333
|
+
|
334
|
+
# Called at the end of the RPC::Agent standard initialize method
|
335
|
+
# use this to adjust meta parameters, timeouts and any setup you
|
336
|
+
# need to do.
|
337
|
+
#
|
338
|
+
# This will not be called right when the daemon starts up, we use
|
339
|
+
# lazy loading and initialization so it will only be called the first
|
340
|
+
# time a request for this agent arrives.
|
341
|
+
def startup_hook
|
342
|
+
end
|
343
|
+
|
344
|
+
# Called just after a message was received from the middleware before
|
345
|
+
# it gets passed to the handlers. @request and @reply will already be
|
346
|
+
# set, the msg passed is the message as received from the normal
|
347
|
+
# mcollective runner and the connection is the actual connector.
|
348
|
+
def before_processing_hook(msg, connection)
|
349
|
+
end
|
350
|
+
|
351
|
+
# Called at the end of processing just before the response gets sent
|
352
|
+
# to the middleware.
|
353
|
+
#
|
354
|
+
# This gets run outside of the main exception handling block of the agent
|
355
|
+
# so you should handle any exceptions you could raise yourself. The reason
|
356
|
+
# it is outside of the block is so you'll have access to even status codes
|
357
|
+
# set by the exception handlers. If you do raise an exception it will just
|
358
|
+
# be passed onto the runner and processing will fail.
|
359
|
+
def after_processing_hook
|
360
|
+
end
|
361
|
+
|
362
|
+
# Gets called right after a request was received and calls audit plugins
|
363
|
+
#
|
364
|
+
# Agents can disable auditing by just overriding this method with a noop one
|
365
|
+
# this might be useful for agents that gets a lot of requests or simply if you
|
366
|
+
# do not care for the auditing in a specific agent.
|
367
|
+
def audit_request(msg, connection)
|
368
|
+
PluginManager["rpcaudit_plugin"].audit_request(msg, connection) if @config.rpcaudit
|
369
|
+
rescue Exception => e
|
370
|
+
Log.warn("Audit failed - #{e} - continuing to process message")
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module MCollective
|
2
|
+
module RPC
|
3
|
+
# Auditing of requests is done only for SimpleRPC requests, you provide
|
4
|
+
# a plugin in the MCollective::Audit::* namespace which the SimpleRPC
|
5
|
+
# framework calls for each message
|
6
|
+
#
|
7
|
+
# We provide a simple one that logs to a logfile in the class
|
8
|
+
# MCollective::Audit::Logfile you can create your own:
|
9
|
+
#
|
10
|
+
# Create a class in plugins/mcollective/audit/<yourplugin>.rb
|
11
|
+
#
|
12
|
+
# You must inherit from MCollective::RPC::Audit which will take
|
13
|
+
# care of registering you with the plugin system.
|
14
|
+
#
|
15
|
+
# Your plugin must provide audit_request(request, connection)
|
16
|
+
# the request parameter will be an instance of MCollective::RPC::Request
|
17
|
+
#
|
18
|
+
# To enable auditing you should set:
|
19
|
+
#
|
20
|
+
# rpcaudit = 1
|
21
|
+
# rpcauditprovider = Logfile
|
22
|
+
#
|
23
|
+
# in the config file this will enable logging using the
|
24
|
+
# MCollective::Audit::Logile class
|
25
|
+
#
|
26
|
+
# The Audit class acts as a base for audit plugins and takes care of registering them
|
27
|
+
# with the plugin manager
|
28
|
+
class Audit
|
29
|
+
def self.inherited(klass)
|
30
|
+
PluginManager << {:type => "rpcaudit_plugin", :class => klass.to_s}
|
31
|
+
end
|
32
|
+
|
33
|
+
def audit_request(request, connection)
|
34
|
+
@log.error("audit_request is not implimented in #{this.class}")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,1066 @@
|
|
1
|
+
module MCollective
|
2
|
+
module RPC
|
3
|
+
# The main component of the Simple RPC client system, this wraps around MCollective::Client
|
4
|
+
# and just brings in a lot of convention and standard approached.
|
5
|
+
class Client
|
6
|
+
attr_accessor :timeout, :verbose, :filter, :config, :progress, :ttl, :reply_to
|
7
|
+
attr_reader :client, :stats, :ddl, :agent, :limit_targets, :limit_method, :output_format, :batch_size, :batch_sleep_time, :batch_mode
|
8
|
+
attr_reader :discovery_options, :discovery_method, :default_discovery_method, :limit_seed
|
9
|
+
|
10
|
+
@@initial_options = nil
|
11
|
+
|
12
|
+
# Creates a stub for a remote agent, you can pass in an options array in the flags
|
13
|
+
# which will then be used else it will just create a default options array with
|
14
|
+
# filtering enabled based on the standard command line use.
|
15
|
+
#
|
16
|
+
# rpc = RPC::Client.new("rpctest", :configfile => "client.cfg", :options => options)
|
17
|
+
#
|
18
|
+
# You typically would not call this directly you'd use MCollective::RPC#rpcclient instead
|
19
|
+
# which is a wrapper around this that can be used as a Mixin
|
20
|
+
def initialize(agent, flags = {})
|
21
|
+
if flags.include?(:options)
|
22
|
+
initial_options = flags[:options]
|
23
|
+
|
24
|
+
elsif @@initial_options
|
25
|
+
initial_options = Marshal.load(@@initial_options)
|
26
|
+
|
27
|
+
else
|
28
|
+
oparser = MCollective::Optionparser.new({ :verbose => false,
|
29
|
+
:progress_bar => true,
|
30
|
+
:mcollective_limit_targets => false,
|
31
|
+
:batch_size => nil,
|
32
|
+
:batch_sleep_time => 1 },
|
33
|
+
"filter")
|
34
|
+
|
35
|
+
initial_options = oparser.parse do |parser, opts|
|
36
|
+
if block_given?
|
37
|
+
yield(parser, opts)
|
38
|
+
end
|
39
|
+
|
40
|
+
Helpers.add_simplerpc_options(parser, opts)
|
41
|
+
end
|
42
|
+
|
43
|
+
@@initial_options = Marshal.dump(initial_options)
|
44
|
+
end
|
45
|
+
|
46
|
+
@initial_options = initial_options
|
47
|
+
|
48
|
+
@config = initial_options[:config]
|
49
|
+
@client = MCollective::Client.new(@initial_options)
|
50
|
+
|
51
|
+
@stats = Stats.new
|
52
|
+
@agent = agent
|
53
|
+
@timeout = initial_options[:timeout] || 5
|
54
|
+
@verbose = initial_options[:verbose]
|
55
|
+
@filter = initial_options[:filter] || Util.empty_filter
|
56
|
+
@discovered_agents = nil
|
57
|
+
@progress = initial_options[:progress_bar]
|
58
|
+
@limit_targets = initial_options[:mcollective_limit_targets]
|
59
|
+
@limit_method = Config.instance.rpclimitmethod
|
60
|
+
@limit_seed = initial_options[:limit_seed] || nil
|
61
|
+
@output_format = initial_options[:output_format] || :console
|
62
|
+
@force_direct_request = false
|
63
|
+
@reply_to = initial_options[:reply_to]
|
64
|
+
@discovery_method = initial_options[:discovery_method]
|
65
|
+
if !@discovery_method
|
66
|
+
@discovery_method = Config.instance.default_discovery_method
|
67
|
+
@default_discovery_method = true
|
68
|
+
else
|
69
|
+
@default_discovery_method = false
|
70
|
+
end
|
71
|
+
@discovery_options = initial_options[:discovery_options] || []
|
72
|
+
@force_display_mode = initial_options[:force_display_mode] || false
|
73
|
+
|
74
|
+
@batch_size = initial_options[:batch_size] || Config.instance.default_batch_size
|
75
|
+
@batch_sleep_time = Float(initial_options[:batch_sleep_time] || Config.instance.default_batch_sleep_time)
|
76
|
+
@batch_mode = determine_batch_mode(@batch_size)
|
77
|
+
|
78
|
+
agent_filter agent
|
79
|
+
|
80
|
+
@discovery_timeout = @initial_options.fetch(:disctimeout, nil) || Config.instance.discovery_timeout
|
81
|
+
|
82
|
+
@collective = @client.collective
|
83
|
+
@ttl = initial_options[:ttl] || Config.instance.ttl
|
84
|
+
@publish_timeout = initial_options[:publish_timeout] || Config.instance.publish_timeout
|
85
|
+
@threaded = initial_options[:threaded] || Config.instance.threaded
|
86
|
+
|
87
|
+
# if we can find a DDL for the service override
|
88
|
+
# the timeout of the client so we always magically
|
89
|
+
# wait appropriate amounts of time.
|
90
|
+
#
|
91
|
+
# We add the discovery timeout to the ddl supplied
|
92
|
+
# timeout as the discovery timeout tends to be tuned
|
93
|
+
# for local network conditions and fact source speed
|
94
|
+
# which would other wise not be accounted for and
|
95
|
+
# some results might get missed.
|
96
|
+
#
|
97
|
+
# We do this only if the timeout is the default 5
|
98
|
+
# seconds, so that users cli overrides will still
|
99
|
+
# get applied
|
100
|
+
#
|
101
|
+
# DDLs are required, failure to find a DDL is fatal
|
102
|
+
@ddl = DDL.new(agent)
|
103
|
+
@stats.ddl = @ddl
|
104
|
+
@timeout = @ddl.meta[:timeout] + discovery_timeout if @timeout == 5
|
105
|
+
|
106
|
+
# allows stderr and stdout to be overridden for testing
|
107
|
+
# but also for web apps that might not want a bunch of stuff
|
108
|
+
# generated to actual file handles
|
109
|
+
if initial_options[:stderr]
|
110
|
+
@stderr = initial_options[:stderr]
|
111
|
+
else
|
112
|
+
@stderr = STDERR
|
113
|
+
@stderr.sync = true
|
114
|
+
end
|
115
|
+
|
116
|
+
if initial_options[:stdout]
|
117
|
+
@stdout = initial_options[:stdout]
|
118
|
+
else
|
119
|
+
@stdout = STDOUT
|
120
|
+
@stdout.sync = true
|
121
|
+
end
|
122
|
+
|
123
|
+
if initial_options[:stdin]
|
124
|
+
@stdin = initial_options[:stdin]
|
125
|
+
else
|
126
|
+
@stdin = STDIN
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Disconnects cleanly from the middleware
|
131
|
+
def disconnect
|
132
|
+
@client.disconnect
|
133
|
+
end
|
134
|
+
|
135
|
+
# Returns help for an agent if a DDL was found
|
136
|
+
def help(template)
|
137
|
+
@ddl.help(template)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Creates a suitable request hash for the SimpleRPC agent.
|
141
|
+
#
|
142
|
+
# You'd use this if you ever wanted to take care of sending
|
143
|
+
# requests on your own - perhaps via Client#sendreq if you
|
144
|
+
# didn't care for responses.
|
145
|
+
#
|
146
|
+
# In that case you can just do:
|
147
|
+
#
|
148
|
+
# msg = your_rpc.new_request("some_action", :foo => :bar)
|
149
|
+
# filter = your_rpc.filter
|
150
|
+
#
|
151
|
+
# your_rpc.client.sendreq(msg, msg[:agent], filter)
|
152
|
+
#
|
153
|
+
# This will send a SimpleRPC request to the action some_action
|
154
|
+
# with arguments :foo = :bar, it will return immediately and
|
155
|
+
# you will have no indication at all if the request was receieved or not
|
156
|
+
#
|
157
|
+
# Clearly the use of this technique should be limited and done only
|
158
|
+
# if your code requires such a thing
|
159
|
+
def new_request(action, data)
|
160
|
+
callerid = PluginManager["security_plugin"].callerid
|
161
|
+
|
162
|
+
raise 'callerid received from security plugin is not valid' unless PluginManager["security_plugin"].valid_callerid?(callerid)
|
163
|
+
|
164
|
+
{:agent => @agent,
|
165
|
+
:action => action,
|
166
|
+
:caller => callerid,
|
167
|
+
:data => data}
|
168
|
+
end
|
169
|
+
|
170
|
+
# For the provided arguments and action the input arguments get
|
171
|
+
# modified by supplying any defaults provided in the DDL for arguments
|
172
|
+
# that were not supplied in the request
|
173
|
+
#
|
174
|
+
# We then pass the modified arguments to the DDL for validation
|
175
|
+
def validate_request(action, args)
|
176
|
+
raise "No DDL found for agent %s cannot validate inputs" % @agent unless @ddl
|
177
|
+
|
178
|
+
@ddl.set_default_input_arguments(action, args)
|
179
|
+
@ddl.validate_rpc_request(action, args)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Magic handler to invoke remote methods
|
183
|
+
#
|
184
|
+
# Once the stub is created using the constructor or the RPC#rpcclient helper you can
|
185
|
+
# call remote actions easily:
|
186
|
+
#
|
187
|
+
# ret = rpc.echo(:msg => "hello world")
|
188
|
+
#
|
189
|
+
# This will call the 'echo' action of the 'rpctest' agent and return the result as an array,
|
190
|
+
# the array will be a simplified result set from the usual full MCollective::Client#req with
|
191
|
+
# additional error codes and error text:
|
192
|
+
#
|
193
|
+
# {
|
194
|
+
# :sender => "remote.box.com",
|
195
|
+
# :statuscode => 0,
|
196
|
+
# :statusmsg => "OK",
|
197
|
+
# :data => "hello world"
|
198
|
+
# }
|
199
|
+
#
|
200
|
+
# If :statuscode is 0 then everything went find, if it's 1 then you supplied the correct arguments etc
|
201
|
+
# but the request could not be completed, you'll find a human parsable reason in :statusmsg then.
|
202
|
+
#
|
203
|
+
# Codes 2 to 5 maps directly to UnknownRPCAction, MissingRPCData, InvalidRPCData and UnknownRPCError
|
204
|
+
# see below for a description of those, in each case :statusmsg would be the reason for failure.
|
205
|
+
#
|
206
|
+
# To get access to the full result of the MCollective::Client#req calls you can pass in a block:
|
207
|
+
#
|
208
|
+
# rpc.echo(:msg => "hello world") do |resp|
|
209
|
+
# pp resp
|
210
|
+
# end
|
211
|
+
#
|
212
|
+
# In this case resp will the result from MCollective::Client#req. Instead of returning simple
|
213
|
+
# text and codes as above you'll also need to handle the following exceptions:
|
214
|
+
#
|
215
|
+
# UnknownRPCAction - There is no matching action on the agent
|
216
|
+
# MissingRPCData - You did not supply all the needed parameters for the action
|
217
|
+
# InvalidRPCData - The data you did supply did not pass validation
|
218
|
+
# UnknownRPCError - Some other error prevented the agent from running
|
219
|
+
#
|
220
|
+
# During calls a progress indicator will be shown of how many results we've received against
|
221
|
+
# how many nodes were discovered, you can disable this by setting progress to false:
|
222
|
+
#
|
223
|
+
# rpc.progress = false
|
224
|
+
#
|
225
|
+
# This supports a 2nd mode where it will send the SimpleRPC request and never handle the
|
226
|
+
# responses. It's a bit like UDP, it sends the request with the filter attached and you
|
227
|
+
# only get back the requestid, you have no indication about results.
|
228
|
+
#
|
229
|
+
# You can invoke this using:
|
230
|
+
#
|
231
|
+
# puts rpc.echo(:process_results => false)
|
232
|
+
#
|
233
|
+
# This will output just the request id.
|
234
|
+
#
|
235
|
+
# Batched processing is supported:
|
236
|
+
#
|
237
|
+
# printrpc rpc.ping(:batch_size => 5)
|
238
|
+
#
|
239
|
+
# This will do everything exactly as normal but communicate to only 5
|
240
|
+
# agents at a time
|
241
|
+
def method_missing(method_name, *args, &block)
|
242
|
+
# set args to an empty hash if nothings given
|
243
|
+
args = args[0]
|
244
|
+
args = {} if args.nil?
|
245
|
+
|
246
|
+
action = method_name.to_s
|
247
|
+
|
248
|
+
@stats.reset
|
249
|
+
|
250
|
+
validate_request(action, args)
|
251
|
+
|
252
|
+
# TODO(ploubser): The logic here seems poor. It implies that it is valid to
|
253
|
+
# pass arguments where batch_mode is set to false and batch_mode > 0.
|
254
|
+
# If this is the case we completely ignore the supplied value of batch_mode
|
255
|
+
# and do our own thing.
|
256
|
+
|
257
|
+
# if a global batch size is set just use that else set it
|
258
|
+
# in the case that it was passed as an argument
|
259
|
+
batch_mode = args.include?(:batch_size) || @batch_mode
|
260
|
+
batch_size = args.delete(:batch_size) || @batch_size
|
261
|
+
batch_sleep_time = args.delete(:batch_sleep_time) || @batch_sleep_time
|
262
|
+
|
263
|
+
# if we were given a batch_size argument thats 0 and batch_mode was
|
264
|
+
# determined to be on via global options etc this will allow a batch_size
|
265
|
+
# of 0 to disable or batch_mode for this call only
|
266
|
+
batch_mode = determine_batch_mode(batch_size)
|
267
|
+
|
268
|
+
# Handle single target requests by doing discovery and picking
|
269
|
+
# a random node. Then do a custom request specifying a filter
|
270
|
+
# that will only match the one node.
|
271
|
+
if @limit_targets
|
272
|
+
target_nodes = pick_nodes_from_discovered(@limit_targets)
|
273
|
+
Log.debug("Picked #{target_nodes.join(',')} as limited target(s)")
|
274
|
+
|
275
|
+
custom_request(action, args, target_nodes, {"identity" => /^(#{target_nodes.join('|')})$/}, &block)
|
276
|
+
elsif batch_mode
|
277
|
+
call_agent_batched(action, args, options, batch_size, batch_sleep_time, &block)
|
278
|
+
else
|
279
|
+
call_agent(action, args, options, :auto, &block)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# Constructs custom requests with custom filters and discovery data
|
284
|
+
# the idea is that this would be used in web applications where you
|
285
|
+
# might be using a cached copy of data provided by a registration agent
|
286
|
+
# to figure out on your own what nodes will be responding and what your
|
287
|
+
# filter would be.
|
288
|
+
#
|
289
|
+
# This will help you essentially short circuit the traditional cycle of:
|
290
|
+
#
|
291
|
+
# mc discover / call / wait for discovered nodes
|
292
|
+
#
|
293
|
+
# by doing discovery however you like, contructing a filter and a list of
|
294
|
+
# nodes you expect responses from.
|
295
|
+
#
|
296
|
+
# Other than that it will work exactly like a normal call, blocks will behave
|
297
|
+
# the same way, stats will be handled the same way etcetc
|
298
|
+
#
|
299
|
+
# If you just wanted to contact one machine for example with a client that
|
300
|
+
# already has other filter options setup you can do:
|
301
|
+
#
|
302
|
+
# puppet.custom_request("runonce", {}, ["your.box.com"], {:identity => "your.box.com"})
|
303
|
+
#
|
304
|
+
# This will do runonce action on just 'your.box.com', no discovery will be
|
305
|
+
# done and after receiving just one response it will stop waiting for responses
|
306
|
+
#
|
307
|
+
# If direct_addressing is enabled in the config file you can provide an empty
|
308
|
+
# hash as a filter, this will force that request to be a directly addressed
|
309
|
+
# request which technically does not need filters. If you try to use this
|
310
|
+
# mode with direct addressing disabled an exception will be raise
|
311
|
+
def custom_request(action, args, expected_agents, filter = {}, &block)
|
312
|
+
validate_request(action, args)
|
313
|
+
|
314
|
+
if filter == {} && !Config.instance.direct_addressing
|
315
|
+
raise "Attempted to do a filterless custom_request without direct_addressing enabled, preventing unexpected call to all nodes"
|
316
|
+
end
|
317
|
+
|
318
|
+
@stats.reset
|
319
|
+
|
320
|
+
custom_filter = Util.empty_filter
|
321
|
+
custom_options = options.clone
|
322
|
+
|
323
|
+
# merge the supplied filter with the standard empty one
|
324
|
+
# we could just use the merge method but I want to be sure
|
325
|
+
# we dont merge in stuff that isnt actually valid
|
326
|
+
["identity", "fact", "agent", "cf_class", "compound"].each do |ftype|
|
327
|
+
if filter.include?(ftype)
|
328
|
+
custom_filter[ftype] = [filter[ftype], custom_filter[ftype]].flatten
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
# ensure that all filters at least restrict the call to the agent we're a proxy for
|
333
|
+
custom_filter["agent"] << @agent unless custom_filter["agent"].include?(@agent)
|
334
|
+
custom_options[:filter] = custom_filter
|
335
|
+
|
336
|
+
# Fake out the stats discovery would have put there
|
337
|
+
@stats.discovered_agents([expected_agents].flatten)
|
338
|
+
|
339
|
+
# Handle fire and forget requests
|
340
|
+
#
|
341
|
+
# If a specific reply-to was set then from the client perspective this should
|
342
|
+
# be a fire and forget request too since no response will ever reach us - it
|
343
|
+
# will go to the reply-to destination
|
344
|
+
if args[:process_results] == false || @reply_to
|
345
|
+
return fire_and_forget_request(action, args, custom_filter)
|
346
|
+
end
|
347
|
+
|
348
|
+
# Now do a call pretty much exactly like in method_missing except with our own
|
349
|
+
# options and discovery magic
|
350
|
+
if block_given?
|
351
|
+
call_agent(action, args, custom_options, [expected_agents].flatten) do |r|
|
352
|
+
block.call(r)
|
353
|
+
end
|
354
|
+
else
|
355
|
+
call_agent(action, args, custom_options, [expected_agents].flatten)
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
def discovery_timeout
|
360
|
+
return @discovery_timeout if @discovery_timeout
|
361
|
+
return @client.discoverer.ddl.meta[:timeout]
|
362
|
+
end
|
363
|
+
|
364
|
+
def discovery_timeout=(timeout)
|
365
|
+
@discovery_timeout = Float(timeout)
|
366
|
+
|
367
|
+
# we calculate the overall timeout from the DDL of the agent and
|
368
|
+
# the supplied discovery timeout unless someone specifically
|
369
|
+
# specifies a timeout to the constructor
|
370
|
+
#
|
371
|
+
# But if we also then specifically set a discovery_timeout on the
|
372
|
+
# agent that has to override the supplied timeout so we then
|
373
|
+
# calculate a correct timeout based on DDL timeout and the
|
374
|
+
# supplied discovery timeout
|
375
|
+
@timeout = @ddl.meta[:timeout] + discovery_timeout
|
376
|
+
end
|
377
|
+
|
378
|
+
# Sets the discovery method. If we change the method there are a
|
379
|
+
# number of steps to take:
|
380
|
+
#
|
381
|
+
# - set the new method
|
382
|
+
# - if discovery options were provided, re-set those to initially
|
383
|
+
# provided ones else clear them as they might now apply to a
|
384
|
+
# different provider
|
385
|
+
# - update the client options so it knows there is a new discovery
|
386
|
+
# method in force
|
387
|
+
# - reset discovery data forcing a discover on the next request
|
388
|
+
#
|
389
|
+
# The remaining item is the discovery timeout, we leave that as is
|
390
|
+
# since that is the user supplied timeout either via initial options
|
391
|
+
# or via specifically setting it on the client.
|
392
|
+
def discovery_method=(method)
|
393
|
+
@default_discovery_method = false
|
394
|
+
@discovery_method = method
|
395
|
+
|
396
|
+
if @initial_options[:discovery_options]
|
397
|
+
@discovery_options = @initial_options[:discovery_options]
|
398
|
+
else
|
399
|
+
@discovery_options.clear
|
400
|
+
end
|
401
|
+
|
402
|
+
@client.options = options
|
403
|
+
|
404
|
+
reset
|
405
|
+
end
|
406
|
+
|
407
|
+
def discovery_options=(options)
|
408
|
+
@discovery_options = [options].flatten
|
409
|
+
reset
|
410
|
+
end
|
411
|
+
|
412
|
+
# Sets the class filter
|
413
|
+
def class_filter(klass)
|
414
|
+
@filter["cf_class"] = @filter["cf_class"] | [klass]
|
415
|
+
@filter["cf_class"].compact!
|
416
|
+
reset
|
417
|
+
end
|
418
|
+
|
419
|
+
# Sets the fact filter
|
420
|
+
def fact_filter(fact, value=nil, operator="=")
|
421
|
+
return if fact.nil?
|
422
|
+
return if fact == false
|
423
|
+
|
424
|
+
if value.nil?
|
425
|
+
parsed = Util.parse_fact_string(fact)
|
426
|
+
@filter["fact"] = @filter["fact"] | [parsed] unless parsed == false
|
427
|
+
else
|
428
|
+
parsed = Util.parse_fact_string("#{fact}#{operator}#{value}")
|
429
|
+
@filter["fact"] = @filter["fact"] | [parsed] unless parsed == false
|
430
|
+
end
|
431
|
+
|
432
|
+
@filter["fact"].compact!
|
433
|
+
reset
|
434
|
+
end
|
435
|
+
|
436
|
+
# Sets the agent filter
|
437
|
+
def agent_filter(agent)
|
438
|
+
@filter["agent"] = @filter["agent"] | [agent]
|
439
|
+
@filter["agent"].compact!
|
440
|
+
reset
|
441
|
+
end
|
442
|
+
|
443
|
+
# Sets the identity filter
|
444
|
+
def identity_filter(identity)
|
445
|
+
@filter["identity"] = @filter["identity"] | [identity]
|
446
|
+
@filter["identity"].compact!
|
447
|
+
reset
|
448
|
+
end
|
449
|
+
|
450
|
+
# Set a compound filter
|
451
|
+
def compound_filter(filter)
|
452
|
+
@filter["compound"] = @filter["compound"] | [Matcher.create_compound_callstack(filter)]
|
453
|
+
reset
|
454
|
+
end
|
455
|
+
|
456
|
+
# Resets various internal parts of the class, most importantly it clears
|
457
|
+
# out the cached discovery
|
458
|
+
def reset
|
459
|
+
@discovered_agents = nil
|
460
|
+
end
|
461
|
+
|
462
|
+
# Reet the filter to an empty one
|
463
|
+
def reset_filter
|
464
|
+
@filter = Util.empty_filter
|
465
|
+
agent_filter @agent
|
466
|
+
end
|
467
|
+
|
468
|
+
# Detects data on STDIN and sets the STDIN discovery method
|
469
|
+
#
|
470
|
+
# IF the discovery method hasn't been explicitly overridden
|
471
|
+
# and we're not being run interactively,
|
472
|
+
# and someone has piped us some data
|
473
|
+
#
|
474
|
+
# Then we assume it's a discovery list - this can be either:
|
475
|
+
# - list of hosts in plaintext
|
476
|
+
# - JSON that came from another rpc or printrpc
|
477
|
+
#
|
478
|
+
# Then we override discovery to try to grok the data on STDIN
|
479
|
+
def detect_and_set_stdin_discovery
|
480
|
+
if self.default_discovery_method && !@stdin.tty? && !@stdin.eof?
|
481
|
+
self.discovery_method = 'stdin'
|
482
|
+
self.discovery_options = 'auto'
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
# Does discovery based on the filters set, if a discovery was
|
487
|
+
# previously done return that else do a new discovery.
|
488
|
+
#
|
489
|
+
# Alternatively if identity filters are given and none of them are
|
490
|
+
# regular expressions then just use the provided data as discovered
|
491
|
+
# data, avoiding discovery
|
492
|
+
#
|
493
|
+
# Discovery can be forced if direct_addressing is enabled by passing
|
494
|
+
# in an array of nodes with :nodes or JSON data like those produced
|
495
|
+
# by mcollective RPC JSON output using :json
|
496
|
+
#
|
497
|
+
# Will show a message indicating its doing discovery if running
|
498
|
+
# verbose or if the :verbose flag is passed in.
|
499
|
+
#
|
500
|
+
# Use reset to force a new discovery
|
501
|
+
def discover(flags={})
|
502
|
+
flags.keys.each do |key|
|
503
|
+
raise "Unknown option #{key} passed to discover" unless [:verbose, :hosts, :nodes, :json].include?(key)
|
504
|
+
end
|
505
|
+
|
506
|
+
flags.include?(:verbose) ? verbose = flags[:verbose] : verbose = @verbose
|
507
|
+
|
508
|
+
verbose = false unless @output_format == :console
|
509
|
+
|
510
|
+
# flags[:nodes] and flags[:hosts] are the same thing, we should never have
|
511
|
+
# allowed :hosts as that was inconsistent with the established terminology
|
512
|
+
flags[:nodes] = flags.delete(:hosts) if flags.include?(:hosts)
|
513
|
+
|
514
|
+
reset if flags[:nodes] || flags[:json]
|
515
|
+
|
516
|
+
unless @discovered_agents
|
517
|
+
# if either hosts or JSON is supplied try to figure out discovery data from there
|
518
|
+
# if direct_addressing is not enabled this is a critical error as the user might
|
519
|
+
# not have supplied filters so raise an exception
|
520
|
+
if flags[:nodes] || flags[:json]
|
521
|
+
raise "Can only supply discovery data if direct_addressing is enabled" unless Config.instance.direct_addressing
|
522
|
+
|
523
|
+
hosts = []
|
524
|
+
|
525
|
+
if flags[:nodes]
|
526
|
+
hosts = Helpers.extract_hosts_from_array(flags[:nodes])
|
527
|
+
elsif flags[:json]
|
528
|
+
hosts = Helpers.extract_hosts_from_json(flags[:json])
|
529
|
+
end
|
530
|
+
|
531
|
+
raise "Could not find any hosts in discovery data provided" if hosts.empty?
|
532
|
+
|
533
|
+
@discovered_agents = hosts
|
534
|
+
@force_direct_request = true
|
535
|
+
|
536
|
+
else
|
537
|
+
identity_filter_discovery_optimization
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
# All else fails we do it the hard way using a traditional broadcast
|
542
|
+
unless @discovered_agents
|
543
|
+
@stats.time_discovery :start
|
544
|
+
|
545
|
+
@client.options = options
|
546
|
+
|
547
|
+
# if compound filters are used the only real option is to use the mc
|
548
|
+
# discovery plugin since its the only capable of using data queries etc
|
549
|
+
# and we do not want to degrade that experience just to allow compounds
|
550
|
+
# on other discovery plugins the UX would be too bad raising complex sets
|
551
|
+
# of errors etc.
|
552
|
+
@client.discoverer.force_discovery_method_by_filter(options[:filter])
|
553
|
+
|
554
|
+
if verbose
|
555
|
+
actual_timeout = @client.discoverer.discovery_timeout(discovery_timeout, options[:filter])
|
556
|
+
|
557
|
+
if actual_timeout > 0
|
558
|
+
@stderr.print("Discovering hosts using the %s method for %d second(s) .... " % [@client.discoverer.discovery_method, actual_timeout])
|
559
|
+
else
|
560
|
+
@stderr.print("Discovering hosts using the %s method .... " % [@client.discoverer.discovery_method])
|
561
|
+
end
|
562
|
+
end
|
563
|
+
|
564
|
+
# if the requested limit is a pure number and not a percent
|
565
|
+
# and if we're configured to use the first found hosts as the
|
566
|
+
# limit method then pass in the limit thus minimizing the amount
|
567
|
+
# of work we do in the discover phase and speeding it up significantly
|
568
|
+
filter = @filter.merge({'collective' => @collective})
|
569
|
+
if @limit_method == :first and @limit_targets.is_a?(Integer)
|
570
|
+
@discovered_agents = @client.discover(filter, discovery_timeout, @limit_targets)
|
571
|
+
else
|
572
|
+
@discovered_agents = @client.discover(filter, discovery_timeout)
|
573
|
+
end
|
574
|
+
|
575
|
+
@stderr.puts(@discovered_agents.size) if verbose
|
576
|
+
|
577
|
+
@force_direct_request = @client.discoverer.force_direct_mode?
|
578
|
+
|
579
|
+
@stats.time_discovery :end
|
580
|
+
end
|
581
|
+
|
582
|
+
@stats.discovered_agents(@discovered_agents)
|
583
|
+
RPC.discovered(@discovered_agents)
|
584
|
+
|
585
|
+
@discovered_agents
|
586
|
+
end
|
587
|
+
|
588
|
+
# Provides a normal options hash like you would get from
|
589
|
+
# Optionparser
|
590
|
+
def options
|
591
|
+
{:disctimeout => discovery_timeout,
|
592
|
+
:timeout => @timeout,
|
593
|
+
:verbose => @verbose,
|
594
|
+
:filter => @filter,
|
595
|
+
:collective => @collective,
|
596
|
+
:output_format => @output_format,
|
597
|
+
:ttl => @ttl,
|
598
|
+
:discovery_method => @discovery_method,
|
599
|
+
:discovery_options => @discovery_options,
|
600
|
+
:force_display_mode => @force_display_mode,
|
601
|
+
:config => @config,
|
602
|
+
:publish_timeout => @publish_timeout,
|
603
|
+
:threaded => @threaded}
|
604
|
+
end
|
605
|
+
|
606
|
+
# Sets the collective we are communicating with
|
607
|
+
def collective=(c)
|
608
|
+
raise "Unknown collective #{c}" unless Config.instance.collectives.include?(c)
|
609
|
+
|
610
|
+
@collective = c
|
611
|
+
@client.options = options
|
612
|
+
reset
|
613
|
+
end
|
614
|
+
|
615
|
+
# Sets and sanity checks the limit_targets variable
|
616
|
+
# used to restrict how many nodes we'll target
|
617
|
+
# Limit targets can be reset by passing nil or false
|
618
|
+
def limit_targets=(limit)
|
619
|
+
if !limit
|
620
|
+
@limit_targets = nil
|
621
|
+
return
|
622
|
+
end
|
623
|
+
|
624
|
+
if limit.is_a?(String)
|
625
|
+
raise "Invalid limit specified: #{limit} valid limits are /^\d+%*$/" unless limit =~ /^\d+%*$/
|
626
|
+
|
627
|
+
begin
|
628
|
+
@limit_targets = Integer(limit)
|
629
|
+
rescue
|
630
|
+
@limit_targets = limit
|
631
|
+
end
|
632
|
+
else
|
633
|
+
@limit_targets = Integer(limit)
|
634
|
+
end
|
635
|
+
end
|
636
|
+
|
637
|
+
# Sets and sanity check the limit_method variable
|
638
|
+
# used to determine how to limit targets if limit_targets is set
|
639
|
+
def limit_method=(method)
|
640
|
+
method = method.to_sym unless method.is_a?(Symbol)
|
641
|
+
|
642
|
+
raise "Unknown limit method #{method} must be :random or :first" unless [:random, :first].include?(method)
|
643
|
+
|
644
|
+
@limit_method = method
|
645
|
+
end
|
646
|
+
|
647
|
+
# Sets the batch size, if the size is set to 0 that will disable batch mode
|
648
|
+
def batch_size=(limit)
|
649
|
+
unless Config.instance.direct_addressing
|
650
|
+
raise "Can only set batch size if direct addressing is supported"
|
651
|
+
end
|
652
|
+
|
653
|
+
validate_batch_size(limit)
|
654
|
+
|
655
|
+
@batch_size = limit
|
656
|
+
@batch_mode = determine_batch_mode(@batch_size)
|
657
|
+
end
|
658
|
+
|
659
|
+
def batch_sleep_time=(time)
|
660
|
+
raise "Can only set batch sleep time if direct addressing is supported" unless Config.instance.direct_addressing
|
661
|
+
|
662
|
+
@batch_sleep_time = Float(time)
|
663
|
+
end
|
664
|
+
|
665
|
+
# Pick a number of nodes from the discovered nodes
|
666
|
+
#
|
667
|
+
# The count should be a string that can be either
|
668
|
+
# just a number or a percentage like 10%
|
669
|
+
#
|
670
|
+
# It will select nodes from the discovered list based
|
671
|
+
# on the rpclimitmethod configuration option which can
|
672
|
+
# be either :first or anything else
|
673
|
+
#
|
674
|
+
# - :first would be a simple way to do a distance based
|
675
|
+
# selection
|
676
|
+
# - anything else will just pick one at random
|
677
|
+
# - if random chosen, and batch-seed set, then set srand
|
678
|
+
# for the generator, and reset afterwards
|
679
|
+
def pick_nodes_from_discovered(count)
|
680
|
+
if count =~ /%$/
|
681
|
+
pct = Integer((discover.size * (count.to_f / 100)))
|
682
|
+
pct == 0 ? count = 1 : count = pct
|
683
|
+
else
|
684
|
+
count = Integer(count)
|
685
|
+
end
|
686
|
+
|
687
|
+
return discover if discover.size <= count
|
688
|
+
|
689
|
+
result = []
|
690
|
+
|
691
|
+
if @limit_method == :first
|
692
|
+
return discover[0, count]
|
693
|
+
else
|
694
|
+
# we delete from the discovered list because we want
|
695
|
+
# to be sure there is no chance that the same node will
|
696
|
+
# be randomly picked twice. So we have to clone the
|
697
|
+
# discovered list else this method will only ever work
|
698
|
+
# once per discovery cycle and not actually return the
|
699
|
+
# right nodes.
|
700
|
+
haystack = discover.clone
|
701
|
+
|
702
|
+
if @limit_seed
|
703
|
+
haystack.sort!
|
704
|
+
srand(@limit_seed)
|
705
|
+
end
|
706
|
+
|
707
|
+
count.times do
|
708
|
+
rnd = rand(haystack.size)
|
709
|
+
result << haystack.delete_at(rnd)
|
710
|
+
end
|
711
|
+
|
712
|
+
# Reset random number generator to fresh seed
|
713
|
+
# As our seed from options is most likely short
|
714
|
+
srand if @limit_seed
|
715
|
+
end
|
716
|
+
|
717
|
+
[result].flatten
|
718
|
+
end
|
719
|
+
|
720
|
+
def load_aggregate_functions(action, ddl)
|
721
|
+
return nil unless ddl
|
722
|
+
return nil unless ddl.action_interface(action).keys.include?(:aggregate)
|
723
|
+
|
724
|
+
return Aggregate.new(ddl.action_interface(action))
|
725
|
+
|
726
|
+
rescue => e
|
727
|
+
Log.error("Failed to load aggregate functions, calculating summaries disabled: %s: %s (%s)" % [e.backtrace.first, e.to_s, e.class])
|
728
|
+
return nil
|
729
|
+
end
|
730
|
+
|
731
|
+
def aggregate_reply(reply, aggregate)
|
732
|
+
return nil unless aggregate
|
733
|
+
|
734
|
+
aggregate.call_functions(reply)
|
735
|
+
return aggregate
|
736
|
+
rescue Exception => e
|
737
|
+
Log.error("Failed to calculate aggregate summaries for reply from %s, calculating summaries disabled: %s: %s (%s)" % [reply[:senderid], e.backtrace.first, e.to_s, e.class])
|
738
|
+
return nil
|
739
|
+
end
|
740
|
+
|
741
|
+
def rpc_result_from_reply(agent, action, reply)
|
742
|
+
senderid = reply.include?("senderid") ? reply["senderid"] : reply[:senderid]
|
743
|
+
body = reply.include?("body") ? reply["body"] : reply[:body]
|
744
|
+
s_code = body.include?("statuscode") ? body["statuscode"] : body[:statuscode]
|
745
|
+
s_msg = body.include?("statusmsg") ? body["statusmsg"] : body[:statusmsg]
|
746
|
+
data = body.include?("data") ? body["data"] : body[:data]
|
747
|
+
|
748
|
+
Result.new(agent, action, {:sender => senderid, :statuscode => s_code, :statusmsg => s_msg, :data => data})
|
749
|
+
end
|
750
|
+
|
751
|
+
# for requests that do not care for results just
|
752
|
+
# return the request id and don't do any of the
|
753
|
+
# response processing.
|
754
|
+
#
|
755
|
+
# We send the :process_results flag with to the
|
756
|
+
# nodes so they can make decisions based on that.
|
757
|
+
#
|
758
|
+
# Should only be called via method_missing
|
759
|
+
def fire_and_forget_request(action, args, filter=nil)
|
760
|
+
validate_request(action, args)
|
761
|
+
|
762
|
+
identity_filter_discovery_optimization
|
763
|
+
|
764
|
+
req = new_request(action.to_s, args)
|
765
|
+
|
766
|
+
filter = options[:filter] unless filter
|
767
|
+
|
768
|
+
message = Message.new(req, nil, {:agent => @agent, :type => :request, :collective => @collective, :filter => filter, :options => options})
|
769
|
+
message.reply_to = @reply_to if @reply_to
|
770
|
+
|
771
|
+
if @force_direct_request || @client.discoverer.force_direct_mode?
|
772
|
+
message.discovered_hosts = discover.clone
|
773
|
+
message.type = :direct_request
|
774
|
+
end
|
775
|
+
|
776
|
+
client.sendreq(message, nil)
|
777
|
+
end
|
778
|
+
|
779
|
+
# if an identity filter is supplied and it is all strings no regex we can use that
|
780
|
+
# as discovery data, technically the identity filter is then redundant if we are
|
781
|
+
# in direct addressing mode and we could empty it out but this use case should
|
782
|
+
# only really be for a few -I's on the CLI
|
783
|
+
#
|
784
|
+
# For safety we leave the filter in place for now, that way we can support this
|
785
|
+
# enhancement also in broadcast mode.
|
786
|
+
#
|
787
|
+
# This is only needed for the 'mc' discovery method, other methods might change
|
788
|
+
# the concept of identity to mean something else so we should pass the full
|
789
|
+
# identity filter to them
|
790
|
+
def identity_filter_discovery_optimization
|
791
|
+
if options[:filter]["identity"].size > 0 && @discovery_method == "mc"
|
792
|
+
regex_filters = options[:filter]["identity"].select{|i| i.match("^\/")}.size
|
793
|
+
|
794
|
+
if regex_filters == 0
|
795
|
+
@discovered_agents = options[:filter]["identity"].clone
|
796
|
+
@force_direct_request = true if Config.instance.direct_addressing
|
797
|
+
end
|
798
|
+
end
|
799
|
+
end
|
800
|
+
|
801
|
+
# Calls an agent in a way very similar to call_agent but it supports batching
|
802
|
+
# the queries to the network.
|
803
|
+
#
|
804
|
+
# The result sets, stats, block handling etc is all exactly like you would expect
|
805
|
+
# from normal call_agent.
|
806
|
+
#
|
807
|
+
# This is used by method_missing and works only with direct addressing mode
|
808
|
+
def call_agent_batched(action, args, opts, batch_size, sleep_time, &block)
|
809
|
+
raise "Batched requests requires direct addressing" unless Config.instance.direct_addressing
|
810
|
+
raise "Cannot bypass result processing for batched requests" if args[:process_results] == false
|
811
|
+
validate_batch_size(batch_size)
|
812
|
+
|
813
|
+
sleep_time = Float(sleep_time)
|
814
|
+
|
815
|
+
Log.debug("Calling #{agent}##{action} in batches of #{batch_size} with sleep time of #{sleep_time}")
|
816
|
+
|
817
|
+
@force_direct_request = true
|
818
|
+
|
819
|
+
discovered = discover
|
820
|
+
results = []
|
821
|
+
respcount = 0
|
822
|
+
|
823
|
+
if discovered.size > 0
|
824
|
+
req = new_request(action.to_s, args)
|
825
|
+
|
826
|
+
aggregate = load_aggregate_functions(action, @ddl)
|
827
|
+
|
828
|
+
if @progress && !block_given?
|
829
|
+
twirl = Progress.new
|
830
|
+
@stdout.puts
|
831
|
+
@stdout.print twirl.twirl(respcount, discovered.size)
|
832
|
+
end
|
833
|
+
|
834
|
+
if (batch_size =~ /^(\d+)%$/)
|
835
|
+
# determine batch_size as a percentage of the discovered array's size
|
836
|
+
batch_size = (discovered.size / 100.0 * Integer($1)).ceil
|
837
|
+
else
|
838
|
+
batch_size = Integer(batch_size)
|
839
|
+
end
|
840
|
+
|
841
|
+
@stats.requestid = nil
|
842
|
+
processed_nodes = 0
|
843
|
+
|
844
|
+
discovered.in_groups_of(batch_size) do |hosts|
|
845
|
+
message = Message.new(req, nil, {:agent => @agent,
|
846
|
+
:type => :direct_request,
|
847
|
+
:collective => @collective,
|
848
|
+
:filter => opts[:filter],
|
849
|
+
:options => opts})
|
850
|
+
|
851
|
+
# first time round we let the Message object create a request id
|
852
|
+
# we then re-use it for future requests to keep auditing sane etc
|
853
|
+
@stats.requestid = message.create_reqid unless @stats.requestid
|
854
|
+
message.requestid = @stats.requestid
|
855
|
+
|
856
|
+
message.discovered_hosts = hosts.clone.compact
|
857
|
+
|
858
|
+
@client.req(message) do |resp|
|
859
|
+
respcount += 1
|
860
|
+
|
861
|
+
if block_given?
|
862
|
+
aggregate = process_results_with_block(action, resp, block, aggregate)
|
863
|
+
else
|
864
|
+
@stdout.print twirl.twirl(respcount, discovered.size) if @progress
|
865
|
+
|
866
|
+
result, aggregate = process_results_without_block(resp, action, aggregate)
|
867
|
+
|
868
|
+
results << result
|
869
|
+
end
|
870
|
+
end
|
871
|
+
|
872
|
+
if @initial_options[:sort]
|
873
|
+
results.sort!
|
874
|
+
end
|
875
|
+
|
876
|
+
@stats.noresponsefrom.concat @client.stats[:noresponsefrom]
|
877
|
+
@stats.unexpectedresponsefrom.concat @client.stats[:unexpectedresponsefrom]
|
878
|
+
@stats.responses += @client.stats[:responses]
|
879
|
+
@stats.blocktime += @client.stats[:blocktime] + sleep_time
|
880
|
+
@stats.totaltime += @client.stats[:totaltime]
|
881
|
+
@stats.discoverytime += @client.stats[:discoverytime]
|
882
|
+
|
883
|
+
processed_nodes += hosts.length
|
884
|
+
if (discovered.length > processed_nodes)
|
885
|
+
sleep sleep_time
|
886
|
+
end
|
887
|
+
end
|
888
|
+
|
889
|
+
@stats.aggregate_summary = aggregate.summarize if aggregate
|
890
|
+
@stats.aggregate_failures = aggregate.failed if aggregate
|
891
|
+
else
|
892
|
+
@stderr.print("\nNo request sent, we did not discover any nodes.")
|
893
|
+
end
|
894
|
+
|
895
|
+
@stats.finish_request
|
896
|
+
|
897
|
+
RPC.stats(@stats)
|
898
|
+
|
899
|
+
@stdout.print("\n") if @progress
|
900
|
+
|
901
|
+
if block_given?
|
902
|
+
return stats
|
903
|
+
else
|
904
|
+
return [results].flatten
|
905
|
+
end
|
906
|
+
end
|
907
|
+
|
908
|
+
# Handles traditional calls to the remote agents with full stats
|
909
|
+
# blocks, non blocks and everything else supported.
|
910
|
+
#
|
911
|
+
# Other methods of calling the nodes can reuse this code by
|
912
|
+
# for example specifying custom options and discovery data
|
913
|
+
def call_agent(action, args, opts, disc=:auto, &block)
|
914
|
+
# Handle fire and forget requests and make sure
|
915
|
+
# the :process_results value is set appropriately
|
916
|
+
#
|
917
|
+
# specific reply-to requests should be treated like
|
918
|
+
# fire and forget since the client will never get
|
919
|
+
# the responses
|
920
|
+
if args[:process_results] == false || @reply_to
|
921
|
+
return fire_and_forget_request(action, args)
|
922
|
+
else
|
923
|
+
args[:process_results] = true
|
924
|
+
end
|
925
|
+
|
926
|
+
# Do discovery when no specific discovery array is given
|
927
|
+
#
|
928
|
+
# If an array is given set the force_direct_request hint that
|
929
|
+
# will tell the message object to be a direct request one
|
930
|
+
if disc == :auto
|
931
|
+
discovered = discover
|
932
|
+
else
|
933
|
+
@force_direct_request = true if Config.instance.direct_addressing
|
934
|
+
discovered = disc
|
935
|
+
end
|
936
|
+
|
937
|
+
req = new_request(action.to_s, args)
|
938
|
+
|
939
|
+
message = Message.new(req, nil, {:agent => @agent, :type => :request, :collective => @collective, :filter => opts[:filter], :options => opts})
|
940
|
+
message.discovered_hosts = discovered.clone
|
941
|
+
|
942
|
+
results = []
|
943
|
+
respcount = 0
|
944
|
+
|
945
|
+
if discovered.size > 0
|
946
|
+
message.type = :direct_request if @force_direct_request
|
947
|
+
|
948
|
+
if @progress && !block_given?
|
949
|
+
twirl = Progress.new
|
950
|
+
@stdout.puts
|
951
|
+
@stdout.print twirl.twirl(respcount, discovered.size)
|
952
|
+
end
|
953
|
+
|
954
|
+
aggregate = load_aggregate_functions(action, @ddl)
|
955
|
+
|
956
|
+
@client.req(message) do |resp|
|
957
|
+
respcount += 1
|
958
|
+
|
959
|
+
if block_given?
|
960
|
+
aggregate = process_results_with_block(action, resp, block, aggregate)
|
961
|
+
else
|
962
|
+
@stdout.print twirl.twirl(respcount, discovered.size) if @progress
|
963
|
+
|
964
|
+
result, aggregate = process_results_without_block(resp, action, aggregate)
|
965
|
+
|
966
|
+
results << result
|
967
|
+
end
|
968
|
+
end
|
969
|
+
|
970
|
+
if @initial_options[:sort]
|
971
|
+
results.sort!
|
972
|
+
end
|
973
|
+
|
974
|
+
@stats.aggregate_summary = aggregate.summarize if aggregate
|
975
|
+
@stats.aggregate_failures = aggregate.failed if aggregate
|
976
|
+
@stats.client_stats = @client.stats
|
977
|
+
else
|
978
|
+
@stderr.print("\nNo request sent, we did not discover any nodes.")
|
979
|
+
end
|
980
|
+
|
981
|
+
@stats.finish_request
|
982
|
+
|
983
|
+
RPC.stats(@stats)
|
984
|
+
|
985
|
+
@stdout.print("\n\n") if @progress
|
986
|
+
|
987
|
+
if block_given?
|
988
|
+
return stats
|
989
|
+
else
|
990
|
+
return [results].flatten
|
991
|
+
end
|
992
|
+
end
|
993
|
+
|
994
|
+
# Handles result sets that has no block associated, sets fails and ok
|
995
|
+
# in the stats object and return a hash of the response to send to the
|
996
|
+
# caller
|
997
|
+
def process_results_without_block(resp, action, aggregate)
|
998
|
+
@stats.node_responded(resp[:senderid])
|
999
|
+
|
1000
|
+
result = rpc_result_from_reply(@agent, action, resp)
|
1001
|
+
aggregate = aggregate_reply(result, aggregate) if aggregate
|
1002
|
+
|
1003
|
+
if result[:statuscode] == 0 || result[:statuscode] == 1
|
1004
|
+
@stats.ok if result[:statuscode] == 0
|
1005
|
+
@stats.fail if result[:statuscode] == 1
|
1006
|
+
else
|
1007
|
+
@stats.fail
|
1008
|
+
end
|
1009
|
+
|
1010
|
+
[result, aggregate]
|
1011
|
+
end
|
1012
|
+
|
1013
|
+
# process client requests by calling a block on each result
|
1014
|
+
# in this mode we do not do anything fancy with the result
|
1015
|
+
# objects and we raise exceptions if there are problems with
|
1016
|
+
# the data
|
1017
|
+
def process_results_with_block(action, resp, block, aggregate)
|
1018
|
+
@stats.node_responded(resp[:senderid])
|
1019
|
+
|
1020
|
+
result = rpc_result_from_reply(@agent, action, resp)
|
1021
|
+
aggregate = aggregate_reply(result, aggregate) if aggregate
|
1022
|
+
|
1023
|
+
@stats.ok if result[:statuscode] == 0
|
1024
|
+
@stats.fail if result[:statuscode] != 0
|
1025
|
+
@stats.time_block_execution :start
|
1026
|
+
|
1027
|
+
case block.arity
|
1028
|
+
when 1
|
1029
|
+
block.call(resp)
|
1030
|
+
when 2
|
1031
|
+
block.call(resp, result)
|
1032
|
+
end
|
1033
|
+
|
1034
|
+
@stats.time_block_execution :end
|
1035
|
+
|
1036
|
+
return aggregate
|
1037
|
+
end
|
1038
|
+
|
1039
|
+
private
|
1040
|
+
|
1041
|
+
def determine_batch_mode(batch_size)
|
1042
|
+
if (batch_size != 0 && batch_size != "0")
|
1043
|
+
return true
|
1044
|
+
end
|
1045
|
+
|
1046
|
+
return false
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
# Validate the bach_size based on the following criteria
|
1050
|
+
# batch_size is percentage string and it's more than 0 percent
|
1051
|
+
# batch_size is a string of digits
|
1052
|
+
# batch_size is of type Integer
|
1053
|
+
def validate_batch_size(batch_size)
|
1054
|
+
if (batch_size.is_a?(Integer))
|
1055
|
+
return
|
1056
|
+
elsif (batch_size.is_a?(String))
|
1057
|
+
if ((batch_size =~ /^(\d+)%$/ && Integer($1) != 0) || batch_size =~ /^(\d+)$/)
|
1058
|
+
return
|
1059
|
+
end
|
1060
|
+
end
|
1061
|
+
|
1062
|
+
raise("batch_size must be an integer or match a percentage string (e.g. '24%'")
|
1063
|
+
end
|
1064
|
+
end
|
1065
|
+
end
|
1066
|
+
end
|