mcollective-client 1.3.3
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of mcollective-client might be problematic. Click here for more details.
- data/bin/mc-call-agent +54 -0
- data/bin/mco +27 -0
- data/lib/mcollective.rb +70 -0
- data/lib/mcollective/agents.rb +160 -0
- data/lib/mcollective/application.rb +354 -0
- data/lib/mcollective/applications.rb +145 -0
- data/lib/mcollective/client.rb +292 -0
- data/lib/mcollective/config.rb +202 -0
- data/lib/mcollective/connector.rb +18 -0
- data/lib/mcollective/connector/base.rb +24 -0
- data/lib/mcollective/facts.rb +39 -0
- data/lib/mcollective/facts/base.rb +86 -0
- data/lib/mcollective/log.rb +103 -0
- data/lib/mcollective/logger.rb +5 -0
- data/lib/mcollective/logger/base.rb +73 -0
- data/lib/mcollective/logger/console_logger.rb +61 -0
- data/lib/mcollective/logger/file_logger.rb +46 -0
- data/lib/mcollective/logger/syslog_logger.rb +53 -0
- data/lib/mcollective/matcher.rb +16 -0
- data/lib/mcollective/matcher/parser.rb +93 -0
- data/lib/mcollective/matcher/scanner.rb +123 -0
- data/lib/mcollective/message.rb +201 -0
- data/lib/mcollective/monkey_patches.rb +104 -0
- data/lib/mcollective/optionparser.rb +164 -0
- data/lib/mcollective/pluginmanager.rb +180 -0
- data/lib/mcollective/pluginpackager.rb +26 -0
- data/lib/mcollective/pluginpackager/agent_definition.rb +79 -0
- data/lib/mcollective/pluginpackager/standard_definition.rb +59 -0
- data/lib/mcollective/registration.rb +16 -0
- data/lib/mcollective/registration/base.rb +75 -0
- data/lib/mcollective/rpc.rb +188 -0
- data/lib/mcollective/rpc/actionrunner.rb +142 -0
- data/lib/mcollective/rpc/agent.rb +441 -0
- data/lib/mcollective/rpc/audit.rb +38 -0
- data/lib/mcollective/rpc/client.rb +793 -0
- data/lib/mcollective/rpc/ddl.rb +258 -0
- data/lib/mcollective/rpc/helpers.rb +339 -0
- data/lib/mcollective/rpc/progress.rb +63 -0
- data/lib/mcollective/rpc/reply.rb +61 -0
- data/lib/mcollective/rpc/request.rb +51 -0
- data/lib/mcollective/rpc/result.rb +41 -0
- data/lib/mcollective/rpc/stats.rb +185 -0
- data/lib/mcollective/runnerstats.rb +90 -0
- data/lib/mcollective/security.rb +26 -0
- data/lib/mcollective/security/base.rb +237 -0
- data/lib/mcollective/shell.rb +87 -0
- data/lib/mcollective/ssl.rb +246 -0
- data/lib/mcollective/unix_daemon.rb +37 -0
- data/lib/mcollective/util.rb +274 -0
- data/lib/mcollective/vendor.rb +41 -0
- data/lib/mcollective/vendor/require_vendored.rb +2 -0
- data/lib/mcollective/windows_daemon.rb +25 -0
- data/spec/Rakefile +16 -0
- data/spec/fixtures/application/test.rb +7 -0
- data/spec/fixtures/test-cert.pem +15 -0
- data/spec/fixtures/test-private.pem +15 -0
- data/spec/fixtures/test-public.pem +6 -0
- data/spec/monkey_patches/instance_variable_defined.rb +7 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/unit/agents_spec.rb +280 -0
- data/spec/unit/application_spec.rb +636 -0
- data/spec/unit/applications_spec.rb +155 -0
- data/spec/unit/array.rb +30 -0
- data/spec/unit/config_spec.rb +148 -0
- data/spec/unit/facts/base_spec.rb +118 -0
- data/spec/unit/facts_spec.rb +39 -0
- data/spec/unit/log_spec.rb +71 -0
- data/spec/unit/logger/base_spec.rb +110 -0
- data/spec/unit/logger/syslog_logger_spec.rb +86 -0
- data/spec/unit/matcher/parser_spec.rb +106 -0
- data/spec/unit/matcher/scanner_spec.rb +71 -0
- data/spec/unit/message_spec.rb +401 -0
- data/spec/unit/optionparser_spec.rb +113 -0
- data/spec/unit/pluginmanager_spec.rb +173 -0
- data/spec/unit/pluginpackager/agent_definition_spec.rb +130 -0
- data/spec/unit/pluginpackager/standard_definition_spec.rb +75 -0
- data/spec/unit/plugins/mcollective/connector/activemq_spec.rb +533 -0
- data/spec/unit/plugins/mcollective/connector/stomp/eventlogger_spec.rb +34 -0
- data/spec/unit/plugins/mcollective/connector/stomp_spec.rb +417 -0
- data/spec/unit/plugins/mcollective/packagers/ospackage_spec.rb +229 -0
- data/spec/unit/plugins/mcollective/security/psk_spec.rb +156 -0
- data/spec/unit/registration/base_spec.rb +77 -0
- data/spec/unit/rpc/actionrunner_spec.rb +213 -0
- data/spec/unit/rpc/agent_spec.rb +155 -0
- data/spec/unit/rpc/client_spec.rb +523 -0
- data/spec/unit/rpc/ddl_spec.rb +388 -0
- data/spec/unit/rpc/helpers_spec.rb +55 -0
- data/spec/unit/rpc/reply_spec.rb +143 -0
- data/spec/unit/rpc/request_spec.rb +115 -0
- data/spec/unit/rpc/result_spec.rb +66 -0
- data/spec/unit/rpc/stats_spec.rb +288 -0
- data/spec/unit/runnerstats_spec.rb +40 -0
- data/spec/unit/security/base_spec.rb +279 -0
- data/spec/unit/shell_spec.rb +144 -0
- data/spec/unit/ssl_spec.rb +244 -0
- data/spec/unit/symbol.rb +11 -0
- data/spec/unit/unix_daemon.rb +41 -0
- data/spec/unit/util_spec.rb +342 -0
- data/spec/unit/vendor_spec.rb +34 -0
- data/spec/unit/windows_daemon.rb +43 -0
- data/spec/windows_spec.opts +1 -0
- metadata +242 -0
@@ -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,793 @@
|
|
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 :discovery_timeout, :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
|
+
|
9
|
+
@@initial_options = nil
|
10
|
+
|
11
|
+
# Creates a stub for a remote agent, you can pass in an options array in the flags
|
12
|
+
# which will then be used else it will just create a default options array with
|
13
|
+
# filtering enabled based on the standard command line use.
|
14
|
+
#
|
15
|
+
# rpc = RPC::Client.new("rpctest", :configfile => "client.cfg", :options => options)
|
16
|
+
#
|
17
|
+
# You typically would not call this directly you'd use MCollective::RPC#rpcclient instead
|
18
|
+
# which is a wrapper around this that can be used as a Mixin
|
19
|
+
def initialize(agent, flags = {})
|
20
|
+
if flags.include?(:options)
|
21
|
+
initial_options = flags[:options]
|
22
|
+
|
23
|
+
elsif @@initial_options
|
24
|
+
initial_options = Marshal.load(@@initial_options)
|
25
|
+
|
26
|
+
else
|
27
|
+
oparser = MCollective::Optionparser.new({:verbose => false, :progress_bar => true, :mcollective_limit_targets => false, :batch_size => nil, :batch_sleep_time => 1}, "filter")
|
28
|
+
|
29
|
+
initial_options = oparser.parse do |parser, opts|
|
30
|
+
if block_given?
|
31
|
+
yield(parser, opts)
|
32
|
+
end
|
33
|
+
|
34
|
+
Helpers.add_simplerpc_options(parser, opts)
|
35
|
+
end
|
36
|
+
|
37
|
+
@@initial_options = Marshal.dump(initial_options)
|
38
|
+
end
|
39
|
+
|
40
|
+
@stats = Stats.new
|
41
|
+
@agent = agent
|
42
|
+
@discovery_timeout = initial_options[:disctimeout]
|
43
|
+
@timeout = initial_options[:timeout]
|
44
|
+
@verbose = initial_options[:verbose]
|
45
|
+
@filter = initial_options[:filter]
|
46
|
+
@config = initial_options[:config]
|
47
|
+
@discovered_agents = nil
|
48
|
+
@progress = initial_options[:progress_bar]
|
49
|
+
@limit_targets = initial_options[:mcollective_limit_targets]
|
50
|
+
@limit_method = Config.instance.rpclimitmethod
|
51
|
+
@output_format = initial_options[:output_format] || :console
|
52
|
+
@force_direct_request = false
|
53
|
+
@reply_to = initial_options[:reply_to]
|
54
|
+
|
55
|
+
@batch_size = Integer(initial_options[:batch_size] || 0)
|
56
|
+
@batch_sleep_time = Float(initial_options[:batch_sleep_time] || 1)
|
57
|
+
@batch_mode = @batch_size > 0
|
58
|
+
|
59
|
+
agent_filter agent
|
60
|
+
|
61
|
+
@client = MCollective::Client.new(@config)
|
62
|
+
@client.options = initial_options
|
63
|
+
|
64
|
+
@collective = @client.collective
|
65
|
+
@ttl = initial_options[:ttl] || Config.instance.ttl
|
66
|
+
|
67
|
+
# if we can find a DDL for the service override
|
68
|
+
# the timeout of the client so we always magically
|
69
|
+
# wait appropriate amounts of time.
|
70
|
+
#
|
71
|
+
# We add the discovery timeout to the ddl supplied
|
72
|
+
# timeout as the discovery timeout tends to be tuned
|
73
|
+
# for local network conditions and fact source speed
|
74
|
+
# which would other wise not be accounted for and
|
75
|
+
# some results might get missed.
|
76
|
+
#
|
77
|
+
# We do this only if the timeout is the default 5
|
78
|
+
# seconds, so that users cli overrides will still
|
79
|
+
# get applied
|
80
|
+
begin
|
81
|
+
@ddl = DDL.new(agent)
|
82
|
+
@timeout = @ddl.meta[:timeout] + @discovery_timeout if @timeout == 5
|
83
|
+
rescue Exception => e
|
84
|
+
Log.debug("Could not find DDL: #{e}")
|
85
|
+
@ddl = nil
|
86
|
+
end
|
87
|
+
|
88
|
+
# allows stderr and stdout to be overridden for testing
|
89
|
+
# but also for web apps that might not want a bunch of stuff
|
90
|
+
# generated to actual file handles
|
91
|
+
if initial_options[:stderr]
|
92
|
+
@stderr = initial_options[:stderr]
|
93
|
+
else
|
94
|
+
@stderr = STDERR
|
95
|
+
@stderr.sync = true
|
96
|
+
end
|
97
|
+
|
98
|
+
if initial_options[:stdout]
|
99
|
+
@stdout = initial_options[:stdout]
|
100
|
+
else
|
101
|
+
@stdout = STDOUT
|
102
|
+
@stdout.sync = true
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Disconnects cleanly from the middleware
|
107
|
+
def disconnect
|
108
|
+
@client.disconnect
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns help for an agent if a DDL was found
|
112
|
+
def help(template)
|
113
|
+
if @ddl
|
114
|
+
@ddl.help(template)
|
115
|
+
else
|
116
|
+
return "Can't find DDL for agent '#{@agent}'"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Creates a suitable request hash for the SimpleRPC agent.
|
121
|
+
#
|
122
|
+
# You'd use this if you ever wanted to take care of sending
|
123
|
+
# requests on your own - perhaps via Client#sendreq if you
|
124
|
+
# didn't care for responses.
|
125
|
+
#
|
126
|
+
# In that case you can just do:
|
127
|
+
#
|
128
|
+
# msg = your_rpc.new_request("some_action", :foo => :bar)
|
129
|
+
# filter = your_rpc.filter
|
130
|
+
#
|
131
|
+
# your_rpc.client.sendreq(msg, msg[:agent], filter)
|
132
|
+
#
|
133
|
+
# This will send a SimpleRPC request to the action some_action
|
134
|
+
# with arguments :foo = :bar, it will return immediately and
|
135
|
+
# you will have no indication at all if the request was receieved or not
|
136
|
+
#
|
137
|
+
# Clearly the use of this technique should be limited and done only
|
138
|
+
# if your code requires such a thing
|
139
|
+
def new_request(action, data)
|
140
|
+
callerid = PluginManager["security_plugin"].callerid
|
141
|
+
|
142
|
+
raise 'callerid received from security plugin is not valid' unless PluginManager["security_plugin"].valid_callerid?(callerid)
|
143
|
+
|
144
|
+
{:agent => @agent,
|
145
|
+
:action => action,
|
146
|
+
:caller => callerid,
|
147
|
+
:data => data}
|
148
|
+
end
|
149
|
+
|
150
|
+
# Magic handler to invoke remote methods
|
151
|
+
#
|
152
|
+
# Once the stub is created using the constructor or the RPC#rpcclient helper you can
|
153
|
+
# call remote actions easily:
|
154
|
+
#
|
155
|
+
# ret = rpc.echo(:msg => "hello world")
|
156
|
+
#
|
157
|
+
# This will call the 'echo' action of the 'rpctest' agent and return the result as an array,
|
158
|
+
# the array will be a simplified result set from the usual full MCollective::Client#req with
|
159
|
+
# additional error codes and error text:
|
160
|
+
#
|
161
|
+
# {
|
162
|
+
# :sender => "remote.box.com",
|
163
|
+
# :statuscode => 0,
|
164
|
+
# :statusmsg => "OK",
|
165
|
+
# :data => "hello world"
|
166
|
+
# }
|
167
|
+
#
|
168
|
+
# If :statuscode is 0 then everything went find, if it's 1 then you supplied the correct arguments etc
|
169
|
+
# but the request could not be completed, you'll find a human parsable reason in :statusmsg then.
|
170
|
+
#
|
171
|
+
# Codes 2 to 5 maps directly to UnknownRPCAction, MissingRPCData, InvalidRPCData and UnknownRPCError
|
172
|
+
# see below for a description of those, in each case :statusmsg would be the reason for failure.
|
173
|
+
#
|
174
|
+
# To get access to the full result of the MCollective::Client#req calls you can pass in a block:
|
175
|
+
#
|
176
|
+
# rpc.echo(:msg => "hello world") do |resp|
|
177
|
+
# pp resp
|
178
|
+
# end
|
179
|
+
#
|
180
|
+
# In this case resp will the result from MCollective::Client#req. Instead of returning simple
|
181
|
+
# text and codes as above you'll also need to handle the following exceptions:
|
182
|
+
#
|
183
|
+
# UnknownRPCAction - There is no matching action on the agent
|
184
|
+
# MissingRPCData - You did not supply all the needed parameters for the action
|
185
|
+
# InvalidRPCData - The data you did supply did not pass validation
|
186
|
+
# UnknownRPCError - Some other error prevented the agent from running
|
187
|
+
#
|
188
|
+
# During calls a progress indicator will be shown of how many results we've received against
|
189
|
+
# how many nodes were discovered, you can disable this by setting progress to false:
|
190
|
+
#
|
191
|
+
# rpc.progress = false
|
192
|
+
#
|
193
|
+
# This supports a 2nd mode where it will send the SimpleRPC request and never handle the
|
194
|
+
# responses. It's a bit like UDP, it sends the request with the filter attached and you
|
195
|
+
# only get back the requestid, you have no indication about results.
|
196
|
+
#
|
197
|
+
# You can invoke this using:
|
198
|
+
#
|
199
|
+
# puts rpc.echo(:process_results => false)
|
200
|
+
#
|
201
|
+
# This will output just the request id.
|
202
|
+
#
|
203
|
+
# Batched processing is supported:
|
204
|
+
#
|
205
|
+
# printrpc rpc.ping(:batch_size => 5)
|
206
|
+
#
|
207
|
+
# This will do everything exactly as normal but communicate to only 5
|
208
|
+
# agents at a time
|
209
|
+
def method_missing(method_name, *args, &block)
|
210
|
+
# set args to an empty hash if nothings given
|
211
|
+
args = args[0]
|
212
|
+
args = {} if args.nil?
|
213
|
+
|
214
|
+
action = method_name.to_s
|
215
|
+
|
216
|
+
@stats.reset
|
217
|
+
|
218
|
+
@ddl.validate_request(action, args) if @ddl
|
219
|
+
|
220
|
+
# if a global batch size is set just use that else set it
|
221
|
+
# in the case that it was passed as an argument
|
222
|
+
batch_mode = args.include?(:batch_size) || @batch_mode
|
223
|
+
batch_size = args.delete(:batch_size) || @batch_size
|
224
|
+
batch_sleep_time = args.delete(:batch_sleep_time) || @batch_sleep_time
|
225
|
+
|
226
|
+
# if we were given a batch_size argument thats 0 and batch_mode was
|
227
|
+
# determined to be on via global options etc this will allow a batch_size
|
228
|
+
# of 0 to disable or batch_mode for this call only
|
229
|
+
batch_mode = (batch_mode && Integer(batch_size) > 0)
|
230
|
+
|
231
|
+
# Handle single target requests by doing discovery and picking
|
232
|
+
# a random node. Then do a custom request specifying a filter
|
233
|
+
# that will only match the one node.
|
234
|
+
if @limit_targets
|
235
|
+
target_nodes = pick_nodes_from_discovered(@limit_targets)
|
236
|
+
Log.debug("Picked #{target_nodes.join(',')} as limited target(s)")
|
237
|
+
|
238
|
+
custom_request(action, args, target_nodes, {"identity" => /^(#{target_nodes.join('|')})$/}, &block)
|
239
|
+
elsif batch_mode
|
240
|
+
call_agent_batched(action, args, options, batch_size, batch_sleep_time, &block)
|
241
|
+
else
|
242
|
+
call_agent(action, args, options, :auto, &block)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Constructs custom requests with custom filters and discovery data
|
247
|
+
# the idea is that this would be used in web applications where you
|
248
|
+
# might be using a cached copy of data provided by a registration agent
|
249
|
+
# to figure out on your own what nodes will be responding and what your
|
250
|
+
# filter would be.
|
251
|
+
#
|
252
|
+
# This will help you essentially short circuit the traditional cycle of:
|
253
|
+
#
|
254
|
+
# mc discover / call / wait for discovered nodes
|
255
|
+
#
|
256
|
+
# by doing discovery however you like, contructing a filter and a list of
|
257
|
+
# nodes you expect responses from.
|
258
|
+
#
|
259
|
+
# Other than that it will work exactly like a normal call, blocks will behave
|
260
|
+
# the same way, stats will be handled the same way etcetc
|
261
|
+
#
|
262
|
+
# If you just wanted to contact one machine for example with a client that
|
263
|
+
# already has other filter options setup you can do:
|
264
|
+
#
|
265
|
+
# puppet.custom_request("runonce", {}, ["your.box.com"], {:identity => "your.box.com"})
|
266
|
+
#
|
267
|
+
# This will do runonce action on just 'your.box.com', no discovery will be
|
268
|
+
# done and after receiving just one response it will stop waiting for responses
|
269
|
+
#
|
270
|
+
# If direct_addressing is enabled in the config file you can provide an empty
|
271
|
+
# hash as a filter, this will force that request to be a directly addressed
|
272
|
+
# request which technically does not need filters. If you try to use this
|
273
|
+
# mode with direct addressing disabled an exception will be raise
|
274
|
+
def custom_request(action, args, expected_agents, filter = {}, &block)
|
275
|
+
@ddl.validate_request(action, args) if @ddl
|
276
|
+
|
277
|
+
if filter == {} && !Config.instance.direct_addressing
|
278
|
+
raise "Attempted to do a filterless custom_request without direct_addressing enabled, preventing unexpected call to all nodes"
|
279
|
+
end
|
280
|
+
|
281
|
+
@stats.reset
|
282
|
+
|
283
|
+
custom_filter = Util.empty_filter
|
284
|
+
custom_options = options.clone
|
285
|
+
|
286
|
+
# merge the supplied filter with the standard empty one
|
287
|
+
# we could just use the merge method but I want to be sure
|
288
|
+
# we dont merge in stuff that isnt actually valid
|
289
|
+
["identity", "fact", "agent", "cf_class", "compound"].each do |ftype|
|
290
|
+
if filter.include?(ftype)
|
291
|
+
custom_filter[ftype] = [filter[ftype], custom_filter[ftype]].flatten
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# ensure that all filters at least restrict the call to the agent we're a proxy for
|
296
|
+
custom_filter["agent"] << @agent unless custom_filter["agent"].include?(@agent)
|
297
|
+
custom_options[:filter] = custom_filter
|
298
|
+
|
299
|
+
# Fake out the stats discovery would have put there
|
300
|
+
@stats.discovered_agents([expected_agents].flatten)
|
301
|
+
|
302
|
+
# Handle fire and forget requests
|
303
|
+
#
|
304
|
+
# If a specific reply-to was set then from the client perspective this should
|
305
|
+
# be a fire and forget request too since no response will ever reach us - it
|
306
|
+
# will go to the reply-to destination
|
307
|
+
if args[:process_results] == false || @reply_to
|
308
|
+
return fire_and_forget_request(action, args, custom_filter)
|
309
|
+
end
|
310
|
+
|
311
|
+
# Now do a call pretty much exactly like in method_missing except with our own
|
312
|
+
# options and discovery magic
|
313
|
+
if block_given?
|
314
|
+
call_agent(action, args, custom_options, [expected_agents].flatten) do |r|
|
315
|
+
block.call(r)
|
316
|
+
end
|
317
|
+
else
|
318
|
+
call_agent(action, args, custom_options, [expected_agents].flatten)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
# Sets the class filter
|
323
|
+
def class_filter(klass)
|
324
|
+
@filter["cf_class"] << klass
|
325
|
+
@filter["cf_class"].compact!
|
326
|
+
reset
|
327
|
+
end
|
328
|
+
|
329
|
+
# Sets the fact filter
|
330
|
+
def fact_filter(fact, value=nil, operator="=")
|
331
|
+
return if fact.nil?
|
332
|
+
return if fact == false
|
333
|
+
|
334
|
+
if value.nil?
|
335
|
+
parsed = Util.parse_fact_string(fact)
|
336
|
+
@filter["fact"] << parsed unless parsed == false
|
337
|
+
else
|
338
|
+
parsed = Util.parse_fact_string("#{fact}#{operator}#{value}")
|
339
|
+
@filter["fact"] << parsed unless parsed == false
|
340
|
+
end
|
341
|
+
|
342
|
+
@filter["fact"].compact!
|
343
|
+
reset
|
344
|
+
end
|
345
|
+
|
346
|
+
# Sets the agent filter
|
347
|
+
def agent_filter(agent)
|
348
|
+
@filter["agent"] << agent
|
349
|
+
@filter["agent"].compact!
|
350
|
+
reset
|
351
|
+
end
|
352
|
+
|
353
|
+
# Sets the identity filter
|
354
|
+
def identity_filter(identity)
|
355
|
+
@filter["identity"] << identity
|
356
|
+
@filter["identity"].compact!
|
357
|
+
reset
|
358
|
+
end
|
359
|
+
|
360
|
+
# Set a compound filter
|
361
|
+
def compound_filter(filter)
|
362
|
+
@filter["compound"] = Matcher::Parser.new(filter).execution_stack
|
363
|
+
reset
|
364
|
+
end
|
365
|
+
|
366
|
+
# Resets various internal parts of the class, most importantly it clears
|
367
|
+
# out the cached discovery
|
368
|
+
def reset
|
369
|
+
@discovered_agents = nil
|
370
|
+
end
|
371
|
+
|
372
|
+
# Reet the filter to an empty one
|
373
|
+
def reset_filter
|
374
|
+
@filter = Util.empty_filter
|
375
|
+
agent_filter @agent
|
376
|
+
end
|
377
|
+
|
378
|
+
# Does discovery based on the filters set, if a discovery was
|
379
|
+
# previously done return that else do a new discovery.
|
380
|
+
#
|
381
|
+
# Alternatively if identity filters are given and none of them are
|
382
|
+
# regular expressions then just use the provided data as discovered
|
383
|
+
# data, avoiding discovery
|
384
|
+
#
|
385
|
+
# Discovery can be forced if direct_addressing is enabled by passing
|
386
|
+
# in an array of nodes with :nodes or JSON data like those produced
|
387
|
+
# by mcollective RPC JSON output using :json
|
388
|
+
#
|
389
|
+
# Will show a message indicating its doing discovery if running
|
390
|
+
# verbose or if the :verbose flag is passed in.
|
391
|
+
#
|
392
|
+
# Use reset to force a new discovery
|
393
|
+
def discover(flags={})
|
394
|
+
flags.keys.each do |key|
|
395
|
+
raise "Unknown option #{key} passed to discover" unless [:verbose, :hosts, :nodes, :json].include?(key)
|
396
|
+
end
|
397
|
+
|
398
|
+
flags.include?(:verbose) ? verbose = flags[:verbose] : verbose = @verbose
|
399
|
+
|
400
|
+
verbose = false unless @output_format == :console
|
401
|
+
|
402
|
+
# flags[:nodes] and flags[:hosts] are the same thing, we should never have
|
403
|
+
# allowed :hosts as that was inconsistent with the established terminology
|
404
|
+
flags[:nodes] = flags.delete(:hosts) if flags.include?(:hosts)
|
405
|
+
|
406
|
+
reset if flags[:nodes] || flags[:json]
|
407
|
+
|
408
|
+
unless @discovered_agents
|
409
|
+
# if either hosts or JSON is supplied try to figure out discovery data from there
|
410
|
+
# if direct_addressing is not enabled this is a critical error as the user might
|
411
|
+
# not have supplied filters so raise an exception
|
412
|
+
if flags[:nodes] || flags[:json]
|
413
|
+
raise "Can only supply discovery data if direct_addressing is enabled" unless Config.instance.direct_addressing
|
414
|
+
|
415
|
+
hosts = []
|
416
|
+
|
417
|
+
if flags[:nodes]
|
418
|
+
hosts = Helpers.extract_hosts_from_array(flags[:nodes])
|
419
|
+
elsif flags[:json]
|
420
|
+
hosts = Helpers.extract_hosts_from_json(flags[:json])
|
421
|
+
end
|
422
|
+
|
423
|
+
raise "Could not find any hosts in discovery data provided" if hosts.empty?
|
424
|
+
|
425
|
+
@discovered_agents = hosts
|
426
|
+
@force_direct_request = true
|
427
|
+
|
428
|
+
# if an identity filter is supplied and it is all strings no regex we can use that
|
429
|
+
# as discovery data, technically the identity filter is then redundant if we are
|
430
|
+
# in direct addressing mode and we could empty it out but this use case should
|
431
|
+
# only really be for a few -I's on the CLI
|
432
|
+
#
|
433
|
+
# For safety we leave the filter in place for now, that way we can support this
|
434
|
+
# enhancement also in broadcast mode
|
435
|
+
elsif options[:filter]["identity"].size > 0
|
436
|
+
regex_filters = options[:filter]["identity"].select{|i| i.match("^\/")}.size
|
437
|
+
|
438
|
+
if regex_filters == 0
|
439
|
+
@discovered_agents = options[:filter]["identity"].clone
|
440
|
+
@force_direct_request = true if Config.instance.direct_addressing
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
# All else fails we do it the hard way using a traditional broadcast
|
446
|
+
unless @discovered_agents
|
447
|
+
@stats.time_discovery :start
|
448
|
+
|
449
|
+
@stderr.print("Determining the amount of hosts matching filter for #{discovery_timeout} seconds .... ") if verbose
|
450
|
+
|
451
|
+
# if the requested limit is a pure number and not a percent
|
452
|
+
# and if we're configured to use the first found hosts as the
|
453
|
+
# limit method then pass in the limit thus minimizing the amount
|
454
|
+
# of work we do in the discover phase and speeding it up significantly
|
455
|
+
if @limit_method == :first and @limit_targets.is_a?(Fixnum)
|
456
|
+
@discovered_agents = @client.discover(@filter, @discovery_timeout, @limit_targets)
|
457
|
+
else
|
458
|
+
@discovered_agents = @client.discover(@filter, @discovery_timeout)
|
459
|
+
end
|
460
|
+
|
461
|
+
@force_direct_request = false
|
462
|
+
@stderr.puts(@discovered_agents.size) if verbose
|
463
|
+
|
464
|
+
@stats.time_discovery :end
|
465
|
+
end
|
466
|
+
|
467
|
+
@stats.discovered_agents(@discovered_agents)
|
468
|
+
RPC.discovered(@discovered_agents)
|
469
|
+
|
470
|
+
@discovered_agents
|
471
|
+
end
|
472
|
+
|
473
|
+
# Provides a normal options hash like you would get from
|
474
|
+
# Optionparser
|
475
|
+
def options
|
476
|
+
{:disctimeout => @discovery_timeout,
|
477
|
+
:timeout => @timeout,
|
478
|
+
:verbose => @verbose,
|
479
|
+
:filter => @filter,
|
480
|
+
:collective => @collective,
|
481
|
+
:output_format => @output_format,
|
482
|
+
:ttl => @ttl,
|
483
|
+
:config => @config}
|
484
|
+
end
|
485
|
+
|
486
|
+
# Sets the collective we are communicating with
|
487
|
+
def collective=(c)
|
488
|
+
@collective = c
|
489
|
+
@client.options[:collective] = c
|
490
|
+
end
|
491
|
+
|
492
|
+
# Sets and sanity checks the limit_targets variable
|
493
|
+
# used to restrict how many nodes we'll target
|
494
|
+
def limit_targets=(limit)
|
495
|
+
if limit.is_a?(String)
|
496
|
+
raise "Invalid limit specified: #{limit} valid limits are /^\d+%*$/" unless limit =~ /^\d+%*$/
|
497
|
+
|
498
|
+
begin
|
499
|
+
@limit_targets = Integer(limit)
|
500
|
+
rescue
|
501
|
+
@limit_targets = limit
|
502
|
+
end
|
503
|
+
else
|
504
|
+
@limit_targets = Integer(limit)
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
# Sets and sanity check the limit_method variable
|
509
|
+
# used to determine how to limit targets if limit_targets is set
|
510
|
+
def limit_method=(method)
|
511
|
+
method = method.to_sym unless method.is_a?(Symbol)
|
512
|
+
|
513
|
+
raise "Unknown limit method #{method} must be :random or :first" unless [:random, :first].include?(method)
|
514
|
+
|
515
|
+
@limit_method = method
|
516
|
+
end
|
517
|
+
|
518
|
+
# Sets the batch size, if the size is set to 0 that will disable batch mode
|
519
|
+
def batch_size=(limit)
|
520
|
+
raise "Can only set batch size if direct addressing is supported" unless Config.instance.direct_addressing
|
521
|
+
|
522
|
+
@batch_size = Integer(limit)
|
523
|
+
@batch_mode = @batch_size > 0
|
524
|
+
end
|
525
|
+
|
526
|
+
def batch_sleep_time=(time)
|
527
|
+
raise "Can only set batch sleep time if direct addressing is supported" unless Config.instance.direct_addressing
|
528
|
+
|
529
|
+
@batch_sleep_time = Float(time)
|
530
|
+
end
|
531
|
+
|
532
|
+
private
|
533
|
+
# Pick a number of nodes from the discovered nodes
|
534
|
+
#
|
535
|
+
# The count should be a string that can be either
|
536
|
+
# just a number or a percentage like 10%
|
537
|
+
#
|
538
|
+
# It will select nodes from the discovered list based
|
539
|
+
# on the rpclimitmethod configuration option which can
|
540
|
+
# be either :first or anything else
|
541
|
+
#
|
542
|
+
# - :first would be a simple way to do a distance based
|
543
|
+
# selection
|
544
|
+
# - anything else will just pick one at random
|
545
|
+
def pick_nodes_from_discovered(count)
|
546
|
+
if count =~ /%$/
|
547
|
+
pct = (discover.size * (count.to_f / 100)).to_i
|
548
|
+
pct == 0 ? count = 1 : count = pct
|
549
|
+
else
|
550
|
+
count = count.to_i
|
551
|
+
end
|
552
|
+
|
553
|
+
return discover if discover.size <= count
|
554
|
+
|
555
|
+
result = []
|
556
|
+
|
557
|
+
if @limit_method == :first
|
558
|
+
return discover[0, count]
|
559
|
+
else
|
560
|
+
count.times do
|
561
|
+
rnd = rand(discover.size)
|
562
|
+
result << discover[rnd]
|
563
|
+
discover.delete_at(rnd)
|
564
|
+
end
|
565
|
+
end
|
566
|
+
|
567
|
+
[result].flatten
|
568
|
+
end
|
569
|
+
|
570
|
+
# for requests that do not care for results just
|
571
|
+
# return the request id and don't do any of the
|
572
|
+
# response processing.
|
573
|
+
#
|
574
|
+
# We send the :process_results flag with to the
|
575
|
+
# nodes so they can make decisions based on that.
|
576
|
+
#
|
577
|
+
# Should only be called via method_missing
|
578
|
+
def fire_and_forget_request(action, args, filter=nil)
|
579
|
+
@ddl.validate_request(action, args) if @ddl
|
580
|
+
|
581
|
+
req = new_request(action.to_s, args)
|
582
|
+
|
583
|
+
filter = options[:filter] unless filter
|
584
|
+
|
585
|
+
message = Message.new(req, nil, {:agent => @agent, :type => :request, :collective => @collective, :filter => filter, :options => options})
|
586
|
+
message.reply_to = @reply_to if @reply_to
|
587
|
+
|
588
|
+
return @client.sendreq(message, nil)
|
589
|
+
end
|
590
|
+
|
591
|
+
# Calls an agent in a way very similar to call_agent but it supports batching
|
592
|
+
# the queries to the network.
|
593
|
+
#
|
594
|
+
# The result sets, stats, block handling etc is all exactly like you would expect
|
595
|
+
# from normal call_agent.
|
596
|
+
#
|
597
|
+
# This is used by method_missing and works only with direct addressing mode
|
598
|
+
def call_agent_batched(action, args, opts, batch_size, sleep_time, &block)
|
599
|
+
raise "Batched requests requires direct addressing" unless Config.instance.direct_addressing
|
600
|
+
raise "Cannot bypass result processing for batched requests" if args[:process_results] == false
|
601
|
+
|
602
|
+
batch_size = Integer(batch_size)
|
603
|
+
sleep_time = Float(sleep_time)
|
604
|
+
|
605
|
+
Log.debug("Calling #{agent}##{action} in batches of #{batch_size} with sleep time of #{sleep_time}")
|
606
|
+
|
607
|
+
@force_direct_request = true
|
608
|
+
|
609
|
+
discovered = discover
|
610
|
+
result = []
|
611
|
+
respcount = 0
|
612
|
+
|
613
|
+
if discovered.size > 0
|
614
|
+
req = new_request(action.to_s, args)
|
615
|
+
|
616
|
+
if @progress && !block_given?
|
617
|
+
twirl = Progress.new
|
618
|
+
@stdout.puts
|
619
|
+
@stdout.print twirl.twirl(respcount, discovered.size)
|
620
|
+
end
|
621
|
+
|
622
|
+
discovered.in_groups_of(batch_size) do |hosts, last_batch|
|
623
|
+
message = Message.new(req, nil, {:agent => @agent, :type => :direct_request, :collective => @collective, :filter => opts[:filter], :options => opts})
|
624
|
+
message.discovered_hosts = hosts.clone.compact
|
625
|
+
|
626
|
+
@client.req(message) do |resp|
|
627
|
+
respcount += 1
|
628
|
+
|
629
|
+
if block_given?
|
630
|
+
process_results_with_block(action, resp, block)
|
631
|
+
else
|
632
|
+
@stdout.print twirl.twirl(respcount, discovered.size) if @progress
|
633
|
+
|
634
|
+
result << process_results_without_block(resp, action)
|
635
|
+
end
|
636
|
+
end
|
637
|
+
|
638
|
+
@stats.noresponsefrom.concat @client.stats[:noresponsefrom]
|
639
|
+
@stats.responses += @client.stats[:responses]
|
640
|
+
@stats.blocktime += @client.stats[:blocktime] + sleep_time
|
641
|
+
@stats.totaltime += @client.stats[:totaltime]
|
642
|
+
@stats.discoverytime += @client.stats[:discoverytime]
|
643
|
+
|
644
|
+
sleep sleep_time unless last_batch
|
645
|
+
end
|
646
|
+
else
|
647
|
+
@stderr.print("\nNo request sent, we did not discover any nodes.")
|
648
|
+
end
|
649
|
+
|
650
|
+
@stats.finish_request
|
651
|
+
|
652
|
+
RPC.stats(@stats)
|
653
|
+
|
654
|
+
@stdout.print("\n") if @progress
|
655
|
+
|
656
|
+
if block_given?
|
657
|
+
return stats
|
658
|
+
else
|
659
|
+
return [result].flatten
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
663
|
+
# Handles traditional calls to the remote agents with full stats
|
664
|
+
# blocks, non blocks and everything else supported.
|
665
|
+
#
|
666
|
+
# Other methods of calling the nodes can reuse this code by
|
667
|
+
# for example specifying custom options and discovery data
|
668
|
+
def call_agent(action, args, opts, disc=:auto, &block)
|
669
|
+
# Handle fire and forget requests and make sure
|
670
|
+
# the :process_results value is set appropriately
|
671
|
+
#
|
672
|
+
# specific reply-to requests should be treated like
|
673
|
+
# fire and forget since the client will never get
|
674
|
+
# the responses
|
675
|
+
if args[:process_results] == false || @reply_to
|
676
|
+
return fire_and_forget_request(action, args)
|
677
|
+
else
|
678
|
+
args[:process_results] = true
|
679
|
+
end
|
680
|
+
|
681
|
+
# Do discovery when no specific discovery array is given
|
682
|
+
#
|
683
|
+
# If an array is given set the force_direct_request hint that
|
684
|
+
# will tell the message object to be a direct request one
|
685
|
+
if disc == :auto
|
686
|
+
discovered = discover
|
687
|
+
else
|
688
|
+
@force_direct_request = true if Config.instance.direct_addressing
|
689
|
+
discovered = disc
|
690
|
+
end
|
691
|
+
|
692
|
+
req = new_request(action.to_s, args)
|
693
|
+
|
694
|
+
message = Message.new(req, nil, {:agent => @agent, :type => :request, :collective => @collective, :filter => opts[:filter], :options => opts})
|
695
|
+
message.discovered_hosts = discovered.clone
|
696
|
+
message.type = :direct_request if @force_direct_request
|
697
|
+
|
698
|
+
result = []
|
699
|
+
respcount = 0
|
700
|
+
|
701
|
+
if discovered.size > 0
|
702
|
+
if @progress && !block_given?
|
703
|
+
twirl = Progress.new
|
704
|
+
@stdout.puts
|
705
|
+
@stdout.print twirl.twirl(respcount, discovered.size)
|
706
|
+
end
|
707
|
+
|
708
|
+
@client.req(message) do |resp|
|
709
|
+
respcount += 1
|
710
|
+
|
711
|
+
if block_given?
|
712
|
+
process_results_with_block(action, resp, block)
|
713
|
+
else
|
714
|
+
@stdout.print twirl.twirl(respcount, discovered.size) if @progress
|
715
|
+
|
716
|
+
result << process_results_without_block(resp, action)
|
717
|
+
end
|
718
|
+
end
|
719
|
+
|
720
|
+
@stats.client_stats = @client.stats
|
721
|
+
else
|
722
|
+
@stderr.print("\nNo request sent, we did not discover any nodes.")
|
723
|
+
end
|
724
|
+
|
725
|
+
@stats.finish_request
|
726
|
+
|
727
|
+
RPC.stats(@stats)
|
728
|
+
|
729
|
+
@stdout.print("\n\n") if @progress
|
730
|
+
|
731
|
+
if block_given?
|
732
|
+
return stats
|
733
|
+
else
|
734
|
+
return [result].flatten
|
735
|
+
end
|
736
|
+
end
|
737
|
+
|
738
|
+
# Handles result sets that has no block associated, sets fails and ok
|
739
|
+
# in the stats object and return a hash of the response to send to the
|
740
|
+
# caller
|
741
|
+
def process_results_without_block(resp, action)
|
742
|
+
@stats.node_responded(resp[:senderid])
|
743
|
+
|
744
|
+
if resp[:body][:statuscode] == 0 || resp[:body][:statuscode] == 1
|
745
|
+
@stats.ok if resp[:body][:statuscode] == 0
|
746
|
+
@stats.fail if resp[:body][:statuscode] == 1
|
747
|
+
|
748
|
+
return Result.new(@agent, action, {:sender => resp[:senderid], :statuscode => resp[:body][:statuscode],
|
749
|
+
:statusmsg => resp[:body][:statusmsg], :data => resp[:body][:data]})
|
750
|
+
else
|
751
|
+
@stats.fail
|
752
|
+
|
753
|
+
return Result.new(@agent, action, {:sender => resp[:senderid], :statuscode => resp[:body][:statuscode],
|
754
|
+
:statusmsg => resp[:body][:statusmsg], :data => nil})
|
755
|
+
end
|
756
|
+
end
|
757
|
+
|
758
|
+
# process client requests by calling a block on each result
|
759
|
+
# in this mode we do not do anything fancy with the result
|
760
|
+
# objects and we raise exceptions if there are problems with
|
761
|
+
# the data
|
762
|
+
def process_results_with_block(action, resp, block)
|
763
|
+
@stats.node_responded(resp[:senderid])
|
764
|
+
|
765
|
+
if resp[:body][:statuscode] == 0 || resp[:body][:statuscode] == 1
|
766
|
+
@stats.time_block_execution :start
|
767
|
+
|
768
|
+
case block.arity
|
769
|
+
when 1
|
770
|
+
block.call(resp)
|
771
|
+
when 2
|
772
|
+
rpcresp = Result.new(@agent, action, {:sender => resp[:senderid], :statuscode => resp[:body][:statuscode],
|
773
|
+
:statusmsg => resp[:body][:statusmsg], :data => resp[:body][:data]})
|
774
|
+
block.call(resp, rpcresp)
|
775
|
+
end
|
776
|
+
|
777
|
+
@stats.time_block_execution :end
|
778
|
+
else
|
779
|
+
case resp[:body][:statuscode]
|
780
|
+
when 2
|
781
|
+
raise UnknownRPCAction, resp[:body][:statusmsg]
|
782
|
+
when 3
|
783
|
+
raise MissingRPCData, resp[:body][:statusmsg]
|
784
|
+
when 4
|
785
|
+
raise InvalidRPCData, resp[:body][:statusmsg]
|
786
|
+
when 5
|
787
|
+
raise UnknownRPCError, resp[:body][:statusmsg]
|
788
|
+
end
|
789
|
+
end
|
790
|
+
end
|
791
|
+
end
|
792
|
+
end
|
793
|
+
end
|