mcollective-client 1.3.3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of mcollective-client might be problematic. Click here for more details.

Files changed (103) hide show
  1. data/bin/mc-call-agent +54 -0
  2. data/bin/mco +27 -0
  3. data/lib/mcollective.rb +70 -0
  4. data/lib/mcollective/agents.rb +160 -0
  5. data/lib/mcollective/application.rb +354 -0
  6. data/lib/mcollective/applications.rb +145 -0
  7. data/lib/mcollective/client.rb +292 -0
  8. data/lib/mcollective/config.rb +202 -0
  9. data/lib/mcollective/connector.rb +18 -0
  10. data/lib/mcollective/connector/base.rb +24 -0
  11. data/lib/mcollective/facts.rb +39 -0
  12. data/lib/mcollective/facts/base.rb +86 -0
  13. data/lib/mcollective/log.rb +103 -0
  14. data/lib/mcollective/logger.rb +5 -0
  15. data/lib/mcollective/logger/base.rb +73 -0
  16. data/lib/mcollective/logger/console_logger.rb +61 -0
  17. data/lib/mcollective/logger/file_logger.rb +46 -0
  18. data/lib/mcollective/logger/syslog_logger.rb +53 -0
  19. data/lib/mcollective/matcher.rb +16 -0
  20. data/lib/mcollective/matcher/parser.rb +93 -0
  21. data/lib/mcollective/matcher/scanner.rb +123 -0
  22. data/lib/mcollective/message.rb +201 -0
  23. data/lib/mcollective/monkey_patches.rb +104 -0
  24. data/lib/mcollective/optionparser.rb +164 -0
  25. data/lib/mcollective/pluginmanager.rb +180 -0
  26. data/lib/mcollective/pluginpackager.rb +26 -0
  27. data/lib/mcollective/pluginpackager/agent_definition.rb +79 -0
  28. data/lib/mcollective/pluginpackager/standard_definition.rb +59 -0
  29. data/lib/mcollective/registration.rb +16 -0
  30. data/lib/mcollective/registration/base.rb +75 -0
  31. data/lib/mcollective/rpc.rb +188 -0
  32. data/lib/mcollective/rpc/actionrunner.rb +142 -0
  33. data/lib/mcollective/rpc/agent.rb +441 -0
  34. data/lib/mcollective/rpc/audit.rb +38 -0
  35. data/lib/mcollective/rpc/client.rb +793 -0
  36. data/lib/mcollective/rpc/ddl.rb +258 -0
  37. data/lib/mcollective/rpc/helpers.rb +339 -0
  38. data/lib/mcollective/rpc/progress.rb +63 -0
  39. data/lib/mcollective/rpc/reply.rb +61 -0
  40. data/lib/mcollective/rpc/request.rb +51 -0
  41. data/lib/mcollective/rpc/result.rb +41 -0
  42. data/lib/mcollective/rpc/stats.rb +185 -0
  43. data/lib/mcollective/runnerstats.rb +90 -0
  44. data/lib/mcollective/security.rb +26 -0
  45. data/lib/mcollective/security/base.rb +237 -0
  46. data/lib/mcollective/shell.rb +87 -0
  47. data/lib/mcollective/ssl.rb +246 -0
  48. data/lib/mcollective/unix_daemon.rb +37 -0
  49. data/lib/mcollective/util.rb +274 -0
  50. data/lib/mcollective/vendor.rb +41 -0
  51. data/lib/mcollective/vendor/require_vendored.rb +2 -0
  52. data/lib/mcollective/windows_daemon.rb +25 -0
  53. data/spec/Rakefile +16 -0
  54. data/spec/fixtures/application/test.rb +7 -0
  55. data/spec/fixtures/test-cert.pem +15 -0
  56. data/spec/fixtures/test-private.pem +15 -0
  57. data/spec/fixtures/test-public.pem +6 -0
  58. data/spec/monkey_patches/instance_variable_defined.rb +7 -0
  59. data/spec/spec.opts +1 -0
  60. data/spec/spec_helper.rb +25 -0
  61. data/spec/unit/agents_spec.rb +280 -0
  62. data/spec/unit/application_spec.rb +636 -0
  63. data/spec/unit/applications_spec.rb +155 -0
  64. data/spec/unit/array.rb +30 -0
  65. data/spec/unit/config_spec.rb +148 -0
  66. data/spec/unit/facts/base_spec.rb +118 -0
  67. data/spec/unit/facts_spec.rb +39 -0
  68. data/spec/unit/log_spec.rb +71 -0
  69. data/spec/unit/logger/base_spec.rb +110 -0
  70. data/spec/unit/logger/syslog_logger_spec.rb +86 -0
  71. data/spec/unit/matcher/parser_spec.rb +106 -0
  72. data/spec/unit/matcher/scanner_spec.rb +71 -0
  73. data/spec/unit/message_spec.rb +401 -0
  74. data/spec/unit/optionparser_spec.rb +113 -0
  75. data/spec/unit/pluginmanager_spec.rb +173 -0
  76. data/spec/unit/pluginpackager/agent_definition_spec.rb +130 -0
  77. data/spec/unit/pluginpackager/standard_definition_spec.rb +75 -0
  78. data/spec/unit/plugins/mcollective/connector/activemq_spec.rb +533 -0
  79. data/spec/unit/plugins/mcollective/connector/stomp/eventlogger_spec.rb +34 -0
  80. data/spec/unit/plugins/mcollective/connector/stomp_spec.rb +417 -0
  81. data/spec/unit/plugins/mcollective/packagers/ospackage_spec.rb +229 -0
  82. data/spec/unit/plugins/mcollective/security/psk_spec.rb +156 -0
  83. data/spec/unit/registration/base_spec.rb +77 -0
  84. data/spec/unit/rpc/actionrunner_spec.rb +213 -0
  85. data/spec/unit/rpc/agent_spec.rb +155 -0
  86. data/spec/unit/rpc/client_spec.rb +523 -0
  87. data/spec/unit/rpc/ddl_spec.rb +388 -0
  88. data/spec/unit/rpc/helpers_spec.rb +55 -0
  89. data/spec/unit/rpc/reply_spec.rb +143 -0
  90. data/spec/unit/rpc/request_spec.rb +115 -0
  91. data/spec/unit/rpc/result_spec.rb +66 -0
  92. data/spec/unit/rpc/stats_spec.rb +288 -0
  93. data/spec/unit/runnerstats_spec.rb +40 -0
  94. data/spec/unit/security/base_spec.rb +279 -0
  95. data/spec/unit/shell_spec.rb +144 -0
  96. data/spec/unit/ssl_spec.rb +244 -0
  97. data/spec/unit/symbol.rb +11 -0
  98. data/spec/unit/unix_daemon.rb +41 -0
  99. data/spec/unit/util_spec.rb +342 -0
  100. data/spec/unit/vendor_spec.rb +34 -0
  101. data/spec/unit/windows_daemon.rb +43 -0
  102. data/spec/windows_spec.opts +1 -0
  103. metadata +242 -0
@@ -0,0 +1,142 @@
1
+ module MCollective
2
+ module RPC
3
+ # A helper used by RPC::Agent#implemented_by to delegate an action to
4
+ # an external script. At present only JSON based serialization is
5
+ # supported in future ones based on key=val pairs etc will be added
6
+ #
7
+ # It serializes the request object into an input file and creates an
8
+ # empty output file. It then calls the external command reading the
9
+ # output file at the end.
10
+ #
11
+ # any STDERR gets logged at error level and any STDOUT gets logged at
12
+ # info level.
13
+ #
14
+ # It will interpret the exit code from the application the same way
15
+ # RPC::Reply#fail! and #fail handles their codes creating a consistent
16
+ # interface, the message part of the fail message will come from STDERR
17
+ #
18
+ # Generally externals should just exit with code 1 on failure and print to
19
+ # STDERR, this is exactly what Perl die() does and translates perfectly
20
+ # to our model
21
+ #
22
+ # It uses the MCollective::Shell wrapper to call the external application
23
+ class ActionRunner
24
+ attr_reader :command, :agent, :action, :format, :stdout, :stderr, :request
25
+
26
+ def initialize(command, request, format=:json)
27
+ @agent = request.agent
28
+ @action = request.action
29
+ @format = format
30
+ @request = request
31
+ @command = path_to_command(command)
32
+ @stdout = ""
33
+ @stderr = ""
34
+ end
35
+
36
+ def run
37
+ unless canrun?(command)
38
+ Log.warn("Cannot run #{to_s}")
39
+ raise RPCAborted, "Cannot execute #{to_s}"
40
+ end
41
+
42
+ Log.debug("Running #{to_s}")
43
+
44
+ request_file = saverequest(request)
45
+ reply_file = tempfile("reply")
46
+ reply_file.close
47
+
48
+ runner = shell(command, request_file.path, reply_file.path)
49
+
50
+ runner.runcommand
51
+
52
+ Log.debug("#{command} exited with #{runner.status.exitstatus}")
53
+
54
+ stderr.each_line {|l| Log.error("#{to_s}: #{l.chomp}")} unless stderr.empty?
55
+ stdout.each_line {|l| Log.info("#{to_s}: #{l.chomp}")} unless stdout.empty?
56
+
57
+ {:exitstatus => runner.status.exitstatus,
58
+ :stdout => runner.stdout,
59
+ :stderr => runner.stderr,
60
+ :data => load_results(reply_file.path)}
61
+ ensure
62
+ request_file.close! if request_file.respond_to?("close!")
63
+ reply_file.close! if reply_file.respond_to?("close")
64
+ end
65
+
66
+ def shell(command, infile, outfile)
67
+ env = {"MCOLLECTIVE_REQUEST_FILE" => infile,
68
+ "MCOLLECTIVE_REPLY_FILE" => outfile}
69
+
70
+ Shell.new("#{command} #{infile} #{outfile}", :cwd => Dir.tmpdir, :stdout => stdout, :stderr => stderr, :environment => env)
71
+ end
72
+
73
+ def load_results(file)
74
+ Log.debug("Attempting to load results in #{format} format from #{file}")
75
+
76
+ data = {}
77
+
78
+ if respond_to?("load_#{format}_results")
79
+ tempdata = send("load_#{format}_results", file)
80
+
81
+ tempdata.each_pair do |k,v|
82
+ data[k.to_sym] = v
83
+ end
84
+ end
85
+
86
+ data
87
+ rescue Exception => e
88
+ {}
89
+ end
90
+
91
+ def load_json_results(file)
92
+ return {} unless File.readable?(file)
93
+
94
+ JSON.load(File.read(file))
95
+ rescue JSON::ParserError
96
+ {}
97
+ end
98
+
99
+ def saverequest(req)
100
+ Log.debug("Attempting to save request in #{format} format")
101
+
102
+ if respond_to?("save_#{format}_request")
103
+ data = send("save_#{format}_request", req)
104
+
105
+ request_file = tempfile("request")
106
+ request_file.puts data
107
+ request_file.close
108
+ end
109
+
110
+ request_file
111
+ end
112
+
113
+ def save_json_request(req)
114
+ req.to_json
115
+ end
116
+
117
+ def canrun?(command)
118
+ File.executable?(command)
119
+ end
120
+
121
+ def to_s
122
+ "%s#%s command: %s" % [ agent, action, command ]
123
+ end
124
+
125
+ def tempfile(prefix)
126
+ Tempfile.new("mcollective_#{prefix}", Dir.tmpdir)
127
+ end
128
+
129
+ def path_to_command(command)
130
+ unless command[0,1] == File::SEPARATOR
131
+ Config.instance.libdir.each do |libdir|
132
+ command_file = File.join(libdir, "agent", agent, command)
133
+
134
+ return command_file if File.exist?(command_file)
135
+ end
136
+ end
137
+
138
+ return command
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,441 @@
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 http://marionette-collective.org/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
+ # matadata :name => "Test SimpleRPC Agent",
16
+ # :description => "A simple test",
17
+ # :author => "You",
18
+ # :license => "1.1",
19
+ # :url => "http://your.com/,
20
+ # :timeout => 60
21
+ #
22
+ # action "hello" do
23
+ # reply[:msg] = "Hello #{request[:name]}"
24
+ # end
25
+ #
26
+ # action "foo" do
27
+ # implemented_by "/some/script.sh"
28
+ # end
29
+ # end
30
+ # end
31
+ # end
32
+ #
33
+ # If you wish to implement the logic for an action using an external script use the
34
+ # implemented_by method that will cause your script to be run with 2 arguments.
35
+ #
36
+ # The first argument is a file containing JSON with the request and the 2nd argument
37
+ # is where the script should save its output as a JSON hash.
38
+ #
39
+ # We also currently have the validation code in here, this will be moved to plugins soon.
40
+ class Agent
41
+ attr_accessor :meta, :reply, :request
42
+ attr_reader :logger, :config, :timeout, :ddl
43
+
44
+ def initialize
45
+ # Default meta data unset
46
+ @meta = {:timeout => 10,
47
+ :name => "Unknown",
48
+ :description => "Unknown",
49
+ :author => "Unknown",
50
+ :license => "Unknown",
51
+ :version => "Unknown",
52
+ :url => "Unknown"}
53
+
54
+ @timeout = meta[:timeout] || 10
55
+ @logger = Log.instance
56
+ @config = Config.instance
57
+ @agent_name = self.class.to_s.split("::").last.downcase
58
+
59
+ # Loads the DDL so we can later use it for validation
60
+ # and help generation
61
+ begin
62
+ @ddl = DDL.new(@agent_name)
63
+ rescue Exception => e
64
+ Log.debug("Failed to load DDL for agent: #{e.class}: #{e}")
65
+ @ddl = nil
66
+ end
67
+
68
+ # if we have a global authorization provider enable it
69
+ # plugins can still override it per plugin
70
+ self.class.authorized_by(@config.rpcauthprovider) if @config.rpcauthorization
71
+
72
+ startup_hook
73
+ end
74
+
75
+ def handlemsg(msg, connection)
76
+ @request = RPC.request(msg)
77
+ @reply = RPC.reply
78
+
79
+ begin
80
+ # Calls the authorization plugin if any is defined
81
+ # if this raises an exception we wil just skip processing this
82
+ # message
83
+ authorization_hook(@request) if respond_to?("authorization_hook")
84
+
85
+
86
+ # Audits the request, currently continues processing the message
87
+ # we should make this a configurable so that an audit failure means
88
+ # a message wont be processed by this node depending on config
89
+ audit_request(@request, connection)
90
+
91
+ before_processing_hook(msg, connection)
92
+
93
+ if respond_to?("#{@request.action}_action")
94
+ send("#{@request.action}_action")
95
+ else
96
+ raise UnknownRPCAction, "Unknown action: #{@request.action}"
97
+ end
98
+ rescue RPCAborted => e
99
+ @reply.fail e.to_s, 1
100
+
101
+ rescue UnknownRPCAction => e
102
+ @reply.fail e.to_s, 2
103
+
104
+ rescue MissingRPCData => e
105
+ @reply.fail e.to_s, 3
106
+
107
+ rescue InvalidRPCData => e
108
+ @reply.fail e.to_s, 4
109
+
110
+ rescue UnknownRPCError => e
111
+ @reply.fail e.to_s, 5
112
+
113
+ rescue Exception => e
114
+ @reply.fail e.to_s, 5
115
+
116
+ end
117
+
118
+ after_processing_hook
119
+
120
+ if @request.should_respond?
121
+ return @reply.to_hash
122
+ else
123
+ Log.debug("Client did not request a response, surpressing reply")
124
+ return nil
125
+ end
126
+ end
127
+
128
+ # By default RPC Agents support a toggle in the configuration that
129
+ # can enable and disable them based on the agent name
130
+ #
131
+ # Example an agent called Foo can have:
132
+ #
133
+ # plugin.foo.activate_agent = false
134
+ #
135
+ # and this will prevent the agent from loading on this particular
136
+ # machine.
137
+ #
138
+ # Agents can use the activate_when helper to override this for example:
139
+ #
140
+ # activate_when do
141
+ # File.exist?("/usr/bin/puppet")
142
+ # end
143
+ def self.activate?
144
+ agent_name = self.to_s.split("::").last.downcase
145
+
146
+ Log.debug("Starting default activation checks for #{agent_name}")
147
+
148
+ should_activate = Config.instance.pluginconf["#{agent_name}.activate_agent"]
149
+
150
+ if should_activate
151
+ Log.debug("Found plugin config #{agent_name}.activate_agent with value #{should_activate}")
152
+ unless should_activate =~ /^1|y|true$/
153
+ return false
154
+ end
155
+ end
156
+
157
+ return true
158
+ end
159
+
160
+ # Generates help using the template based on the data
161
+ # created with metadata and input
162
+ def self.help(template)
163
+ if @ddl
164
+ @ddl.help(template)
165
+ else
166
+ "No DDL defined"
167
+ end
168
+ end
169
+
170
+ # to auto generate help
171
+ def help
172
+ self.help("#{@config[:configdir]}/rpc-help.erb")
173
+ end
174
+
175
+ # Returns an array of actions this agent support
176
+ def self.actions
177
+ public_instance_methods.sort.grep(/_action$/).map do |method|
178
+ $1 if method =~ /(.+)_action$/
179
+ end
180
+ end
181
+
182
+
183
+ private
184
+ # Runs a command via the MC::Shell wrapper, options are as per MC::Shell
185
+ #
186
+ # The simplest use is:
187
+ #
188
+ # out = ""
189
+ # err = ""
190
+ # status = run("echo 1", :stdout => out, :stderr => err)
191
+ #
192
+ # reply[:out] = out
193
+ # reply[:error] = err
194
+ # reply[:exitstatus] = status
195
+ #
196
+ # This can be simplified as:
197
+ #
198
+ # reply[:exitstatus] = run("echo 1", :stdout => :out, :stderr => :error)
199
+ #
200
+ # You can set a command specific environment and cwd:
201
+ #
202
+ # run("echo 1", :cwd => "/tmp", :environment => {"FOO" => "BAR"})
203
+ #
204
+ # This will run 'echo 1' from /tmp with FOO=BAR in addition to a setting forcing
205
+ # LC_ALL = C. To prevent LC_ALL from being set either set it specifically or:
206
+ #
207
+ # run("echo 1", :cwd => "/tmp", :environment => nil)
208
+ #
209
+ # Exceptions here will be handled by the usual agent exception handler or any
210
+ # specific one you create, if you dont it will just fall through and be sent
211
+ # to the client.
212
+ #
213
+ # If the shell handler fails to return a Process::Status instance for exit
214
+ # status this method will return -1 as the exit status
215
+ def run(command, options={})
216
+ shellopts = {}
217
+
218
+ # force stderr and stdout to be strings as the library
219
+ # will append data to them if given using the << method.
220
+ #
221
+ # if the data pased to :stderr or :stdin is a Symbol
222
+ # add that into the reply hash with that Symbol
223
+ [:stderr, :stdout].each do |k|
224
+ if options.include?(k)
225
+ if options[k].is_a?(Symbol)
226
+ reply[ options[k] ] = ""
227
+ shellopts[k] = reply[ options[k] ]
228
+ else
229
+ if options[k].respond_to?("<<")
230
+ shellopts[k] = options[k]
231
+ else
232
+ reply.fail! "#{k} should support << while calling run(#{command})"
233
+ end
234
+ end
235
+ end
236
+ end
237
+
238
+ [:stdin, :cwd, :environment].each do |k|
239
+ if options.include?(k)
240
+ shellopts[k] = options[k]
241
+ end
242
+ end
243
+
244
+ shell = Shell.new(command, shellopts)
245
+
246
+ shell.runcommand
247
+
248
+ if options[:chomp]
249
+ shellopts[:stdout].chomp! if shellopts[:stdout].is_a?(String)
250
+ shellopts[:stderr].chomp! if shellopts[:stderr].is_a?(String)
251
+ end
252
+
253
+ shell.status.exitstatus rescue -1
254
+ end
255
+
256
+ # Registers meta data for the introspection hash
257
+ def self.metadata(data)
258
+ [:name, :description, :author, :license, :version, :url, :timeout].each do |arg|
259
+ raise "Metadata needs a :#{arg}" unless data.include?(arg)
260
+ end
261
+
262
+ # Our old style agents were able to do all sorts of things to the meta
263
+ # data during startup_hook etc, don't really want that but also want
264
+ # backward compat.
265
+ #
266
+ # Here if you're using the new metadata way this replaces the getter
267
+ # with one that always return the same data, setter will still work but
268
+ # wont actually do anything of note.
269
+ define_method("meta") {
270
+ data
271
+ }
272
+ end
273
+
274
+ # Creates the needed activate? class in a manner similar to the other
275
+ # helpers like action, authorized_by etc
276
+ #
277
+ # activate_when do
278
+ # File.exist?("/usr/bin/puppet")
279
+ # end
280
+ def self.activate_when(&block)
281
+ (class << self; self; end).instance_eval do
282
+ define_method("activate?", &block)
283
+ end
284
+ end
285
+
286
+ # Creates a new action with the block passed and sets some defaults
287
+ #
288
+ # action "status" do
289
+ # # logic here to restart service
290
+ # end
291
+ def self.action(name, &block)
292
+ raise "Need to pass a body for the action" unless block_given?
293
+
294
+ self.module_eval { define_method("#{name}_action", &block) }
295
+ end
296
+
297
+ # Helper that creates a method on the class that will call your authorization
298
+ # plugin. If your plugin raises an exception that will abort the request
299
+ def self.authorized_by(plugin)
300
+ plugin = plugin.to_s.capitalize
301
+
302
+ # turns foo_bar into FooBar
303
+ plugin = plugin.to_s.split("_").map {|v| v.capitalize}.join
304
+ pluginname = "MCollective::Util::#{plugin}"
305
+
306
+ PluginManager.loadclass(pluginname) unless MCollective::Util.constants.include?(plugin)
307
+
308
+ class_eval("
309
+ def authorization_hook(request)
310
+ #{pluginname}.authorize(request)
311
+ end
312
+ ")
313
+ end
314
+
315
+ # Validates a data member, if validation is a regex then it will try to match it
316
+ # else it supports testing object types only:
317
+ #
318
+ # validate :msg, String
319
+ # validate :msg, /^[\w\s]+$/
320
+ #
321
+ # There are also some special helper validators:
322
+ #
323
+ # validate :command, :shellsafe
324
+ # validate :command, :ipv6address
325
+ # validate :command, :ipv4address
326
+ # validate :command, :boolean
327
+ # validate :command, ["start", "stop"]
328
+ #
329
+ # It will raise appropriate exceptions that the RPC system understand
330
+ #
331
+ # TODO: this should be plugins, 1 per validatin method so users can add their own
332
+ # at the moment i have it here just to proof the point really
333
+ def validate(key, validation)
334
+ raise MissingRPCData, "please supply a #{key} argument" unless @request.include?(key)
335
+
336
+ if validation.is_a?(Regexp)
337
+ raise InvalidRPCData, "#{key} should match #{validation}" unless @request[key].match(validation)
338
+
339
+ elsif validation.is_a?(Symbol)
340
+ case validation
341
+ when :shellsafe
342
+ raise InvalidRPCData, "#{key} should be a String" unless @request[key].is_a?(String)
343
+
344
+ ['`', '$', ';', '|', '&&', '>', '<'].each do |chr|
345
+ raise InvalidRPCData, "#{key} should not have #{chr} in it" if @request[key].match(Regexp.escape(chr))
346
+ end
347
+
348
+ when :ipv6address
349
+ begin
350
+ require 'ipaddr'
351
+ ip = IPAddr.new(@request[key])
352
+ raise InvalidRPCData, "#{key} should be an ipv6 address" unless ip.ipv6?
353
+ rescue
354
+ raise InvalidRPCData, "#{key} should be an ipv6 address"
355
+ end
356
+
357
+ when :ipv4address
358
+ begin
359
+ require 'ipaddr'
360
+ ip = IPAddr.new(@request[key])
361
+ raise InvalidRPCData, "#{key} should be an ipv4 address" unless ip.ipv4?
362
+ rescue
363
+ raise InvalidRPCData, "#{key} should be an ipv4 address"
364
+ end
365
+
366
+ when :boolean
367
+ raise InvalidRPCData, "#{key} should be boolean" unless [TrueClass, FalseClass].include?(@request[key].class)
368
+ end
369
+ elsif validation.is_a?(Array)
370
+ raise InvalidRPCData, "#{key} should be one of %s" % [ validation.join(", ") ] unless validation.include?(@request[key])
371
+
372
+ else
373
+ raise InvalidRPCData, "#{key} should be a #{validation}" unless @request[key].is_a?(validation)
374
+ end
375
+ end
376
+
377
+ # convenience wrapper around Util#shellescape
378
+ def shellescape(str)
379
+ Util.shellescape(str)
380
+ end
381
+
382
+ # handles external actions
383
+ def implemented_by(command, type=:json)
384
+ runner = ActionRunner.new(command, request, type)
385
+
386
+ res = runner.run
387
+
388
+ reply.fail! "Did not receive data from #{command}" unless res.include?(:data)
389
+ reply.fail! "Reply data from #{command} is not a Hash" unless res[:data].is_a?(Hash)
390
+
391
+ reply.data.merge!(res[:data])
392
+
393
+ if res[:exitstatus] > 0
394
+ reply.fail "Failed to run #{command}: #{res[:stderr]}", res[:exitstatus]
395
+ end
396
+ rescue Exception => e
397
+ Log.warn("Unhandled #{e.class} exception during #{request.agent}##{request.action}: #{e}")
398
+ reply.fail! "Unexpected failure calling #{command}: #{e.class}: #{e}"
399
+ end
400
+
401
+ # Called at the end of the RPC::Agent standard initialize method
402
+ # use this to adjust meta parameters, timeouts and any setup you
403
+ # need to do.
404
+ #
405
+ # This will not be called right when the daemon starts up, we use
406
+ # lazy loading and initialization so it will only be called the first
407
+ # time a request for this agent arrives.
408
+ def startup_hook
409
+ end
410
+
411
+ # Called just after a message was received from the middleware before
412
+ # it gets passed to the handlers. @request and @reply will already be
413
+ # set, the msg passed is the message as received from the normal
414
+ # mcollective runner and the connection is the actual connector.
415
+ def before_processing_hook(msg, connection)
416
+ end
417
+
418
+ # Called at the end of processing just before the response gets sent
419
+ # to the middleware.
420
+ #
421
+ # This gets run outside of the main exception handling block of the agent
422
+ # so you should handle any exceptions you could raise yourself. The reason
423
+ # it is outside of the block is so you'll have access to even status codes
424
+ # set by the exception handlers. If you do raise an exception it will just
425
+ # be passed onto the runner and processing will fail.
426
+ def after_processing_hook
427
+ end
428
+
429
+ # Gets called right after a request was received and calls audit plugins
430
+ #
431
+ # Agents can disable auditing by just overriding this method with a noop one
432
+ # this might be useful for agents that gets a lot of requests or simply if you
433
+ # do not care for the auditing in a specific agent.
434
+ def audit_request(msg, connection)
435
+ PluginManager["rpcaudit_plugin"].audit_request(msg, connection) if @config.rpcaudit
436
+ rescue Exception => e
437
+ Log.warn("Audit failed - #{e} - continuing to process message")
438
+ end
439
+ end
440
+ end
441
+ end