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.
Files changed (133) hide show
  1. checksums.yaml +7 -0
  2. data/bin/mco +64 -0
  3. data/lib/mcollective.rb +63 -0
  4. data/lib/mcollective/agent.rb +5 -0
  5. data/lib/mcollective/agents.rb +149 -0
  6. data/lib/mcollective/aggregate.rb +85 -0
  7. data/lib/mcollective/aggregate/average.ddl +33 -0
  8. data/lib/mcollective/aggregate/average.rb +29 -0
  9. data/lib/mcollective/aggregate/base.rb +40 -0
  10. data/lib/mcollective/aggregate/result.rb +9 -0
  11. data/lib/mcollective/aggregate/result/base.rb +25 -0
  12. data/lib/mcollective/aggregate/result/collection_result.rb +19 -0
  13. data/lib/mcollective/aggregate/result/numeric_result.rb +13 -0
  14. data/lib/mcollective/aggregate/sum.ddl +33 -0
  15. data/lib/mcollective/aggregate/sum.rb +18 -0
  16. data/lib/mcollective/aggregate/summary.ddl +33 -0
  17. data/lib/mcollective/aggregate/summary.rb +53 -0
  18. data/lib/mcollective/application.rb +365 -0
  19. data/lib/mcollective/application/completion.rb +104 -0
  20. data/lib/mcollective/application/describe_filter.rb +87 -0
  21. data/lib/mcollective/application/facts.rb +62 -0
  22. data/lib/mcollective/application/find.rb +23 -0
  23. data/lib/mcollective/application/help.rb +28 -0
  24. data/lib/mcollective/application/inventory.rb +344 -0
  25. data/lib/mcollective/application/ping.rb +82 -0
  26. data/lib/mcollective/application/plugin.rb +369 -0
  27. data/lib/mcollective/application/rpc.rb +111 -0
  28. data/lib/mcollective/applications.rb +134 -0
  29. data/lib/mcollective/cache.rb +145 -0
  30. data/lib/mcollective/client.rb +353 -0
  31. data/lib/mcollective/config.rb +245 -0
  32. data/lib/mcollective/connector.rb +18 -0
  33. data/lib/mcollective/connector/base.rb +26 -0
  34. data/lib/mcollective/data.rb +91 -0
  35. data/lib/mcollective/data/agent_data.ddl +22 -0
  36. data/lib/mcollective/data/agent_data.rb +17 -0
  37. data/lib/mcollective/data/base.rb +67 -0
  38. data/lib/mcollective/data/collective_data.ddl +20 -0
  39. data/lib/mcollective/data/collective_data.rb +9 -0
  40. data/lib/mcollective/data/fact_data.ddl +28 -0
  41. data/lib/mcollective/data/fact_data.rb +55 -0
  42. data/lib/mcollective/data/fstat_data.ddl +89 -0
  43. data/lib/mcollective/data/fstat_data.rb +56 -0
  44. data/lib/mcollective/data/result.rb +45 -0
  45. data/lib/mcollective/ddl.rb +113 -0
  46. data/lib/mcollective/ddl/agentddl.rb +253 -0
  47. data/lib/mcollective/ddl/base.rb +217 -0
  48. data/lib/mcollective/ddl/dataddl.rb +56 -0
  49. data/lib/mcollective/ddl/discoveryddl.rb +52 -0
  50. data/lib/mcollective/ddl/validatorddl.rb +6 -0
  51. data/lib/mcollective/discovery.rb +143 -0
  52. data/lib/mcollective/discovery/flatfile.ddl +11 -0
  53. data/lib/mcollective/discovery/flatfile.rb +48 -0
  54. data/lib/mcollective/discovery/mc.ddl +11 -0
  55. data/lib/mcollective/discovery/mc.rb +30 -0
  56. data/lib/mcollective/discovery/stdin.ddl +11 -0
  57. data/lib/mcollective/discovery/stdin.rb +68 -0
  58. data/lib/mcollective/exceptions.rb +28 -0
  59. data/lib/mcollective/facts.rb +39 -0
  60. data/lib/mcollective/facts/base.rb +100 -0
  61. data/lib/mcollective/facts/yaml_facts.rb +65 -0
  62. data/lib/mcollective/generators.rb +7 -0
  63. data/lib/mcollective/generators/agent_generator.rb +51 -0
  64. data/lib/mcollective/generators/base.rb +46 -0
  65. data/lib/mcollective/generators/data_generator.rb +51 -0
  66. data/lib/mcollective/generators/templates/action_snippet.erb +13 -0
  67. data/lib/mcollective/generators/templates/data_input_snippet.erb +7 -0
  68. data/lib/mcollective/generators/templates/ddl.erb +8 -0
  69. data/lib/mcollective/generators/templates/plugin.erb +7 -0
  70. data/lib/mcollective/log.rb +118 -0
  71. data/lib/mcollective/logger.rb +5 -0
  72. data/lib/mcollective/logger/base.rb +77 -0
  73. data/lib/mcollective/logger/console_logger.rb +61 -0
  74. data/lib/mcollective/logger/file_logger.rb +53 -0
  75. data/lib/mcollective/logger/syslog_logger.rb +53 -0
  76. data/lib/mcollective/matcher.rb +224 -0
  77. data/lib/mcollective/matcher/parser.rb +128 -0
  78. data/lib/mcollective/matcher/scanner.rb +241 -0
  79. data/lib/mcollective/message.rb +248 -0
  80. data/lib/mcollective/monkey_patches.rb +152 -0
  81. data/lib/mcollective/optionparser.rb +197 -0
  82. data/lib/mcollective/pluginmanager.rb +180 -0
  83. data/lib/mcollective/pluginpackager.rb +98 -0
  84. data/lib/mcollective/pluginpackager/agent_definition.rb +94 -0
  85. data/lib/mcollective/pluginpackager/debpackage_packager.rb +237 -0
  86. data/lib/mcollective/pluginpackager/modulepackage_packager.rb +127 -0
  87. data/lib/mcollective/pluginpackager/ospackage_packager.rb +59 -0
  88. data/lib/mcollective/pluginpackager/rpmpackage_packager.rb +180 -0
  89. data/lib/mcollective/pluginpackager/standard_definition.rb +69 -0
  90. data/lib/mcollective/pluginpackager/templates/debian/Makefile.erb +7 -0
  91. data/lib/mcollective/pluginpackager/templates/debian/changelog.erb +5 -0
  92. data/lib/mcollective/pluginpackager/templates/debian/compat.erb +1 -0
  93. data/lib/mcollective/pluginpackager/templates/debian/control.erb +15 -0
  94. data/lib/mcollective/pluginpackager/templates/debian/copyright.erb +8 -0
  95. data/lib/mcollective/pluginpackager/templates/debian/rules.erb +6 -0
  96. data/lib/mcollective/pluginpackager/templates/module/Modulefile.erb +5 -0
  97. data/lib/mcollective/pluginpackager/templates/module/README.md.erb +37 -0
  98. data/lib/mcollective/pluginpackager/templates/module/_manifest.pp.erb +9 -0
  99. data/lib/mcollective/pluginpackager/templates/redhat/rpm_spec.erb +63 -0
  100. data/lib/mcollective/registration/base.rb +91 -0
  101. data/lib/mcollective/rpc.rb +182 -0
  102. data/lib/mcollective/rpc/actionrunner.rb +158 -0
  103. data/lib/mcollective/rpc/agent.rb +374 -0
  104. data/lib/mcollective/rpc/audit.rb +38 -0
  105. data/lib/mcollective/rpc/client.rb +1066 -0
  106. data/lib/mcollective/rpc/helpers.rb +321 -0
  107. data/lib/mcollective/rpc/progress.rb +63 -0
  108. data/lib/mcollective/rpc/reply.rb +87 -0
  109. data/lib/mcollective/rpc/request.rb +86 -0
  110. data/lib/mcollective/rpc/result.rb +90 -0
  111. data/lib/mcollective/rpc/stats.rb +294 -0
  112. data/lib/mcollective/runnerstats.rb +90 -0
  113. data/lib/mcollective/security.rb +26 -0
  114. data/lib/mcollective/security/base.rb +244 -0
  115. data/lib/mcollective/shell.rb +126 -0
  116. data/lib/mcollective/ssl.rb +285 -0
  117. data/lib/mcollective/util.rb +579 -0
  118. data/lib/mcollective/validator.rb +85 -0
  119. data/lib/mcollective/validator/array_validator.ddl +7 -0
  120. data/lib/mcollective/validator/array_validator.rb +9 -0
  121. data/lib/mcollective/validator/ipv4address_validator.ddl +7 -0
  122. data/lib/mcollective/validator/ipv4address_validator.rb +16 -0
  123. data/lib/mcollective/validator/ipv6address_validator.ddl +7 -0
  124. data/lib/mcollective/validator/ipv6address_validator.rb +16 -0
  125. data/lib/mcollective/validator/length_validator.ddl +7 -0
  126. data/lib/mcollective/validator/length_validator.rb +11 -0
  127. data/lib/mcollective/validator/regex_validator.ddl +7 -0
  128. data/lib/mcollective/validator/regex_validator.rb +9 -0
  129. data/lib/mcollective/validator/shellsafe_validator.ddl +7 -0
  130. data/lib/mcollective/validator/shellsafe_validator.rb +13 -0
  131. data/lib/mcollective/validator/typecheck_validator.ddl +7 -0
  132. data/lib/mcollective/validator/typecheck_validator.rb +28 -0
  133. 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