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.

Files changed (103) hide show
  1. data/bin/mc-call-agent +54 -0
  2. data/bin/mco +27 -0
  3. data/lib/mcollective.rb +70 -0
  4. data/lib/mcollective/agents.rb +160 -0
  5. data/lib/mcollective/application.rb +354 -0
  6. data/lib/mcollective/applications.rb +145 -0
  7. data/lib/mcollective/client.rb +292 -0
  8. data/lib/mcollective/config.rb +202 -0
  9. data/lib/mcollective/connector.rb +18 -0
  10. data/lib/mcollective/connector/base.rb +24 -0
  11. data/lib/mcollective/facts.rb +39 -0
  12. data/lib/mcollective/facts/base.rb +86 -0
  13. data/lib/mcollective/log.rb +103 -0
  14. data/lib/mcollective/logger.rb +5 -0
  15. data/lib/mcollective/logger/base.rb +73 -0
  16. data/lib/mcollective/logger/console_logger.rb +61 -0
  17. data/lib/mcollective/logger/file_logger.rb +46 -0
  18. data/lib/mcollective/logger/syslog_logger.rb +53 -0
  19. data/lib/mcollective/matcher.rb +16 -0
  20. data/lib/mcollective/matcher/parser.rb +93 -0
  21. data/lib/mcollective/matcher/scanner.rb +123 -0
  22. data/lib/mcollective/message.rb +201 -0
  23. data/lib/mcollective/monkey_patches.rb +104 -0
  24. data/lib/mcollective/optionparser.rb +164 -0
  25. data/lib/mcollective/pluginmanager.rb +180 -0
  26. data/lib/mcollective/pluginpackager.rb +26 -0
  27. data/lib/mcollective/pluginpackager/agent_definition.rb +79 -0
  28. data/lib/mcollective/pluginpackager/standard_definition.rb +59 -0
  29. data/lib/mcollective/registration.rb +16 -0
  30. data/lib/mcollective/registration/base.rb +75 -0
  31. data/lib/mcollective/rpc.rb +188 -0
  32. data/lib/mcollective/rpc/actionrunner.rb +142 -0
  33. data/lib/mcollective/rpc/agent.rb +441 -0
  34. data/lib/mcollective/rpc/audit.rb +38 -0
  35. data/lib/mcollective/rpc/client.rb +793 -0
  36. data/lib/mcollective/rpc/ddl.rb +258 -0
  37. data/lib/mcollective/rpc/helpers.rb +339 -0
  38. data/lib/mcollective/rpc/progress.rb +63 -0
  39. data/lib/mcollective/rpc/reply.rb +61 -0
  40. data/lib/mcollective/rpc/request.rb +51 -0
  41. data/lib/mcollective/rpc/result.rb +41 -0
  42. data/lib/mcollective/rpc/stats.rb +185 -0
  43. data/lib/mcollective/runnerstats.rb +90 -0
  44. data/lib/mcollective/security.rb +26 -0
  45. data/lib/mcollective/security/base.rb +237 -0
  46. data/lib/mcollective/shell.rb +87 -0
  47. data/lib/mcollective/ssl.rb +246 -0
  48. data/lib/mcollective/unix_daemon.rb +37 -0
  49. data/lib/mcollective/util.rb +274 -0
  50. data/lib/mcollective/vendor.rb +41 -0
  51. data/lib/mcollective/vendor/require_vendored.rb +2 -0
  52. data/lib/mcollective/windows_daemon.rb +25 -0
  53. data/spec/Rakefile +16 -0
  54. data/spec/fixtures/application/test.rb +7 -0
  55. data/spec/fixtures/test-cert.pem +15 -0
  56. data/spec/fixtures/test-private.pem +15 -0
  57. data/spec/fixtures/test-public.pem +6 -0
  58. data/spec/monkey_patches/instance_variable_defined.rb +7 -0
  59. data/spec/spec.opts +1 -0
  60. data/spec/spec_helper.rb +25 -0
  61. data/spec/unit/agents_spec.rb +280 -0
  62. data/spec/unit/application_spec.rb +636 -0
  63. data/spec/unit/applications_spec.rb +155 -0
  64. data/spec/unit/array.rb +30 -0
  65. data/spec/unit/config_spec.rb +148 -0
  66. data/spec/unit/facts/base_spec.rb +118 -0
  67. data/spec/unit/facts_spec.rb +39 -0
  68. data/spec/unit/log_spec.rb +71 -0
  69. data/spec/unit/logger/base_spec.rb +110 -0
  70. data/spec/unit/logger/syslog_logger_spec.rb +86 -0
  71. data/spec/unit/matcher/parser_spec.rb +106 -0
  72. data/spec/unit/matcher/scanner_spec.rb +71 -0
  73. data/spec/unit/message_spec.rb +401 -0
  74. data/spec/unit/optionparser_spec.rb +113 -0
  75. data/spec/unit/pluginmanager_spec.rb +173 -0
  76. data/spec/unit/pluginpackager/agent_definition_spec.rb +130 -0
  77. data/spec/unit/pluginpackager/standard_definition_spec.rb +75 -0
  78. data/spec/unit/plugins/mcollective/connector/activemq_spec.rb +533 -0
  79. data/spec/unit/plugins/mcollective/connector/stomp/eventlogger_spec.rb +34 -0
  80. data/spec/unit/plugins/mcollective/connector/stomp_spec.rb +417 -0
  81. data/spec/unit/plugins/mcollective/packagers/ospackage_spec.rb +229 -0
  82. data/spec/unit/plugins/mcollective/security/psk_spec.rb +156 -0
  83. data/spec/unit/registration/base_spec.rb +77 -0
  84. data/spec/unit/rpc/actionrunner_spec.rb +213 -0
  85. data/spec/unit/rpc/agent_spec.rb +155 -0
  86. data/spec/unit/rpc/client_spec.rb +523 -0
  87. data/spec/unit/rpc/ddl_spec.rb +388 -0
  88. data/spec/unit/rpc/helpers_spec.rb +55 -0
  89. data/spec/unit/rpc/reply_spec.rb +143 -0
  90. data/spec/unit/rpc/request_spec.rb +115 -0
  91. data/spec/unit/rpc/result_spec.rb +66 -0
  92. data/spec/unit/rpc/stats_spec.rb +288 -0
  93. data/spec/unit/runnerstats_spec.rb +40 -0
  94. data/spec/unit/security/base_spec.rb +279 -0
  95. data/spec/unit/shell_spec.rb +144 -0
  96. data/spec/unit/ssl_spec.rb +244 -0
  97. data/spec/unit/symbol.rb +11 -0
  98. data/spec/unit/unix_daemon.rb +41 -0
  99. data/spec/unit/util_spec.rb +342 -0
  100. data/spec/unit/vendor_spec.rb +34 -0
  101. data/spec/unit/windows_daemon.rb +43 -0
  102. data/spec/windows_spec.opts +1 -0
  103. 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