choria-mcorpc-support 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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