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.
7 # See http://marionette-collective.org/simplerpc/agents.html
9 # It only really makes sense to use this with a Simple RPC client on the other end, basic
14 # class Helloworld<RPC::Agent
16 # reply[:msg] = "Hello #{request[:name]}"
20 # implemented_by "/some/script.sh"
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.
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.
32 # We also currently have the validation code in here, this will be moved to plugins soon.
37 attr_accessor :reply, :request, :agent_name
38 attr_reader :logger, :config, :timeout, :ddl, :meta
41 @agent_name = self.class.to_s.split("::").last.downcase
45 @logger = Log.instance
46 @config = Config.instance
48 # if we have a global authorization provider enable it
49 # plugins can still override it per plugin
50 self.class.authorized_by(@config.rpcauthprovider) if @config.rpcauthorization
56 @ddl = DDL.new(@agent_name, :agent)
58 @timeout = @meta[:timeout] || 10
61 DDL.validation_fail!(:PLMC24, "Failed to load DDL for the '%{agent}' agent, DDLs are required: %{error_class}: %{error}", :error, :agent => @agent_name, :error_class => e.class, :error => e.to_s)
64 def handlemsg(msg, connection)
65 @request = RPC::Request.new(msg, @ddl)
66 @reply = RPC::Reply.new(@request.action, @ddl)
69 # Incoming requests need to be validated against the DDL thus reusing
70 # all the work users put into creating DDLs and creating a consistent
71 # quality of input validation everywhere with the a simple once off
72 # investment of writing a DDL
75 # Calls the authorization plugin if any is defined
76 # if this raises an exception we wil just skip processing this
78 authorization_hook(@request) if respond_to?("authorization_hook")
80 # Audits the request, currently continues processing the message
81 # we should make this a configurable so that an audit failure means
82 # a message wont be processed by this node depending on config
83 audit_request(@request, connection)
85 before_processing_hook(msg, connection)
87 if respond_to?("#{@request.action}_action")
88 send("#{@request.action}_action")
90 log_code(:PLMC36, "Unknown action '%{action}' for agent '%{agent}'", :warn, :action => @request.action, :agent => @request.agent)
91 raise UnknownRPCAction, "Unknown action '#{@request.action}' for agent '#{@request.agent}'"
93 rescue RPCAborted => e
96 rescue UnknownRPCAction => e
99 rescue MissingRPCData => e
100 @reply.fail e.to_s, 3
102 rescue InvalidRPCData, DDLValidationError => e
103 @reply.fail e.to_s, 4
105 rescue UnknownRPCError => e
106 Log.error("%s#%s failed: %s: %s" % [@agent_name, @request.action, e.class, e.to_s])
107 Log.error(e.backtrace.join("\n\t"))
108 @reply.fail e.to_s, 5
110 rescue Exception => e
111 Log.error("%s#%s failed: %s: %s" % [@agent_name, @request.action, e.class, e.to_s])
112 Log.error(e.backtrace.join("\n\t"))
113 @reply.fail e.to_s, 5
117 after_processing_hook
119 if @request.should_respond?
120 return @reply.to_hash
122 log_code(:PLMC35, "Client did not request a response, surpressing reply", :debug)
127 # By default RPC Agents support a toggle in the configuration that
128 # can enable and disable them based on the agent name
130 # Example an agent called Foo can have:
132 # plugin.foo.activate_agent = false
134 # and this will prevent the agent from loading on this particular
137 # Agents can use the activate_when helper to override this for example:
140 # File.exist?("/usr/bin/puppet")
143 agent_name = self.to_s.split("::").last.downcase
145 log_code(:PLMC37, "Starting default activation checks for the '%{agent}' agent", :debug, :agent => agent_name)
147 should_activate = Util.str_to_bool(Config.instance.pluginconf.fetch("#{agent_name}.activate_agent", true))
149 unless should_activate
150 log_code(:PLMC38, "Found plugin configuration '%{agent}.activate_agent' with value '%{should_activate}'", :debug, :agent => agent_name, :should_activate => should_activate)
153 return should_activate
156 # Returns an array of actions this agent support
158 public_instance_methods.sort.grep(/_action$/).map do |method|
159 $1 if method =~ /(.+)_action$/
164 # Runs a command via the MC::Shell wrapper, options are as per MC::Shell
166 # The simplest use is:
170 # status = run("echo 1", :stdout => out, :stderr => err)
173 # reply[:error] = err
174 # reply[:exitstatus] = status
176 # This can be simplified as:
178 # reply[:exitstatus] = run("echo 1", :stdout => :out, :stderr => :error)
180 # You can set a command specific environment and cwd:
182 # run("echo 1", :cwd => "/tmp", :environment => {"FOO" => "BAR"})
184 # This will run 'echo 1' from /tmp with FOO=BAR in addition to a setting forcing
185 # LC_ALL = C. To prevent LC_ALL from being set either set it specifically or:
187 # run("echo 1", :cwd => "/tmp", :environment => nil)
189 # Exceptions here will be handled by the usual agent exception handler or any
190 # specific one you create, if you dont it will just fall through and be sent
193 # If the shell handler fails to return a Process::Status instance for exit
194 # status this method will return -1 as the exit status
195 def run(command, options={})
198 # force stderr and stdout to be strings as the library
199 # will append data to them if given using the << method.
201 # if the data pased to :stderr or :stdin is a Symbol
202 # add that into the reply hash with that Symbol
203 [:stderr, :stdout].each do |k|
204 if options.include?(k)
205 if options[k].is_a?(Symbol)
206 reply[ options[k] ] = ""
207 shellopts[k] = reply[ options[k] ]
209 if options[k].respond_to?("<<")
210 shellopts[k] = options[k]
212 reply.fail! "#{k} should support << while calling run(#{command})"
218 [:stdin, :cwd, :environment, :timeout].each do |k|
219 if options.include?(k)
220 shellopts[k] = options[k]
224 shell = Shell.new(command, shellopts)
229 shellopts[:stdout].chomp! if shellopts[:stdout].is_a?(String)
230 shellopts[:stderr].chomp! if shellopts[:stderr].is_a?(String)
233 shell.status.exitstatus rescue -1
236 # Registers meta data for the introspection hash
237 def self.metadata(data)
238 agent = File.basename(caller.first).split(":").first
240 log_code(:PLMC34, "setting meta data in agents have been deprecated, DDL files are now being used for this information. Please update the '%{agent}' agent", :warn, :agent => agent)
243 # Creates the needed activate? class in a manner similar to the other
244 # helpers like action, authorized_by etc
247 # File.exist?("/usr/bin/puppet")
249 def self.activate_when(&block)
250 (class << self; self; end).instance_eval do
251 define_method("activate?", &block)
255 # Creates a new action with the block passed and sets some defaults
258 # # logic here to restart service
260 def self.action(name, &block)
261 raise "Need to pass a body for the action" unless block_given?
263 self.module_eval { define_method("#{name}_action", &block) }
266 # Helper that creates a method on the class that will call your authorization
267 # plugin. If your plugin raises an exception that will abort the request
268 def self.authorized_by(plugin)
269 plugin = plugin.to_s.capitalize
271 # turns foo_bar into FooBar
272 plugin = plugin.to_s.split("_").map {|v| v.capitalize}.join
273 pluginname = "MCollective::Util::#{plugin}"
275 PluginManager.loadclass(pluginname) unless MCollective::Util.constants.include?(plugin)
278 def authorization_hook(request)
279 #{pluginname}.authorize(request)
284 # Validates a data member, if validation is a regex then it will try to match it
285 # else it supports testing object types only:
287 # validate :msg, String
288 # validate :msg, /^[\w\s]+$/
290 # There are also some special helper validators:
292 # validate :command, :shellsafe
293 # validate :command, :ipv6address
294 # validate :command, :ipv4address
295 # validate :command, :boolean
296 # validate :command, ["start", "stop"]
298 # It will raise appropriate exceptions that the RPC system understand
299 def validate(key, validation)
300 raise MissingRPCData, "please supply a #{key} argument" unless @request.include?(key)
302 Validator.validate(@request[key], validation)
303 rescue ValidatorError => e
304 raise InvalidRPCData, "Input %s did not pass validation: %s" % [ key, e.message ]
307 # convenience wrapper around Util#shellescape
309 Util.shellescape(str)
312 # handles external actions
313 def implemented_by(command, type=:json)
314 runner = ActionRunner.new(command, request, type)
318 reply.fail! "Did not receive data from #{command}" unless res.include?(:data)
319 reply.fail! "Reply data from #{command} is not a Hash" unless res[:data].is_a?(Hash)
321 reply.data.merge!(res[:data])
323 if res[:exitstatus] > 0
324 reply.fail "Failed to run #{command}: #{res[:stderr]}", res[:exitstatus]
326 rescue Exception => e
327 Log.warn("Unhandled #{e.class} exception during #{request.agent}##{request.action}: #{e}")
328 reply.fail! "Unexpected failure calling #{command}: #{e.class}: #{e}"
331 # Called at the end of the RPC::Agent standard initialize method
332 # use this to adjust meta parameters, timeouts and any setup you
335 # This will not be called right when the daemon starts up, we use
336 # lazy loading and initialization so it will only be called the first
337 # time a request for this agent arrives.
341 # Called just after a message was received from the middleware before
342 # it gets passed to the handlers. @request and @reply will already be
343 # set, the msg passed is the message as received from the normal
344 # mcollective runner and the connection is the actual connector.
345 def before_processing_hook(msg, connection)
348 # Called at the end of processing just before the response gets sent
351 # This gets run outside of the main exception handling block of the agent
352 # so you should handle any exceptions you could raise yourself. The reason
353 # it is outside of the block is so you'll have access to even status codes
354 # set by the exception handlers. If you do raise an exception it will just
355 # be passed onto the runner and processing will fail.
356 def after_processing_hook
359 # Gets called right after a request was received and calls audit plugins
361 # Agents can disable auditing by just overriding this method with a noop one
362 # this might be useful for agents that gets a lot of requests or simply if you
363 # do not care for the auditing in a specific agent.
364 def audit_request(msg, connection)
365 PluginManager["rpcaudit_plugin"].audit_request(msg, connection) if @config.rpcaudit
366 rescue Exception => e
367 logexception(:PLMC39, "Audit failed with an error, processing the request will continue.", :warn, e)