choria-mcorpc-support 0.0.1
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.
- 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
|