Update version according to OSCI-856
[packages/precise/mcollective.git] / lib / mcollective / rpc / agent.rb
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     #          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       include Translatable
35       extend Translatable
36
37       attr_accessor :reply, :request, :agent_name
38       attr_reader :logger, :config, :timeout, :ddl, :meta
39
40       def initialize
41         @agent_name = self.class.to_s.split("::").last.downcase
42
43         load_ddl
44
45         @logger = Log.instance
46         @config = Config.instance
47
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
51
52         startup_hook
53       end
54
55       def load_ddl
56         @ddl = DDL.new(@agent_name, :agent)
57         @meta = @ddl.meta
58         @timeout = @meta[:timeout] || 10
59
60       rescue Exception => e
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)
62       end
63
64       def handlemsg(msg, connection)
65         @request = RPC::Request.new(msg, @ddl)
66         @reply = RPC::Reply.new(@request.action, @ddl)
67
68         begin
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
73           @request.validate!
74
75           # Calls the authorization plugin if any is defined
76           # if this raises an exception we wil just skip processing this
77           # message
78           authorization_hook(@request) if respond_to?("authorization_hook")
79
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)
84
85           before_processing_hook(msg, connection)
86
87           if respond_to?("#{@request.action}_action")
88             send("#{@request.action}_action")
89           else
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}'"
92           end
93         rescue RPCAborted => e
94           @reply.fail e.to_s, 1
95
96         rescue UnknownRPCAction => e
97           @reply.fail e.to_s, 2
98
99         rescue MissingRPCData => e
100           @reply.fail e.to_s, 3
101
102         rescue InvalidRPCData, DDLValidationError => e
103           @reply.fail e.to_s, 4
104
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
109
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
114
115         end
116
117         after_processing_hook
118
119         if @request.should_respond?
120           return @reply.to_hash
121         else
122           log_code(:PLMC35, "Client did not request a response, surpressing reply", :debug)
123           return nil
124         end
125       end
126
127       # By default RPC Agents support a toggle in the configuration that
128       # can enable and disable them based on the agent name
129       #
130       # Example an agent called Foo can have:
131       #
132       # plugin.foo.activate_agent = false
133       #
134       # and this will prevent the agent from loading on this particular
135       # machine.
136       #
137       # Agents can use the activate_when helper to override this for example:
138       #
139       # activate_when do
140       #    File.exist?("/usr/bin/puppet")
141       # end
142       def self.activate?
143         agent_name = self.to_s.split("::").last.downcase
144
145         log_code(:PLMC37, "Starting default activation checks for the '%{agent}' agent", :debug, :agent => agent_name)
146
147         should_activate = Util.str_to_bool(Config.instance.pluginconf.fetch("#{agent_name}.activate_agent", true))
148
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)
151         end
152
153         return should_activate
154       end
155
156       # Returns an array of actions this agent support
157       def self.actions
158         public_instance_methods.sort.grep(/_action$/).map do |method|
159           $1 if method =~ /(.+)_action$/
160         end
161       end
162
163       private
164       # Runs a command via the MC::Shell wrapper, options are as per MC::Shell
165       #
166       # The simplest use is:
167       #
168       #   out = ""
169       #   err = ""
170       #   status = run("echo 1", :stdout => out, :stderr => err)
171       #
172       #   reply[:out] = out
173       #   reply[:error] = err
174       #   reply[:exitstatus] = status
175       #
176       # This can be simplified as:
177       #
178       #   reply[:exitstatus] = run("echo 1", :stdout => :out, :stderr => :error)
179       #
180       # You can set a command specific environment and cwd:
181       #
182       #   run("echo 1", :cwd => "/tmp", :environment => {"FOO" => "BAR"})
183       #
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:
186       #
187       #   run("echo 1", :cwd => "/tmp", :environment => nil)
188       #
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
191       # to the client.
192       #
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={})
196         shellopts = {}
197
198         # force stderr and stdout to be strings as the library
199         # will append data to them if given using the << method.
200         #
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] ]
208             else
209               if options[k].respond_to?("<<")
210                 shellopts[k] = options[k]
211               else
212                 reply.fail! "#{k} should support << while calling run(#{command})"
213               end
214             end
215           end
216         end
217
218         [:stdin, :cwd, :environment].each do |k|
219           if options.include?(k)
220             shellopts[k] = options[k]
221           end
222         end
223
224         shell = Shell.new(command, shellopts)
225
226         shell.runcommand
227
228         if options[:chomp]
229           shellopts[:stdout].chomp! if shellopts[:stdout].is_a?(String)
230           shellopts[:stderr].chomp! if shellopts[:stderr].is_a?(String)
231         end
232
233         shell.status.exitstatus rescue -1
234       end
235
236       # Registers meta data for the introspection hash
237       def self.metadata(data)
238         agent = File.basename(caller.first).split(":").first
239
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)
241       end
242
243       # Creates the needed activate? class in a manner similar to the other
244       # helpers like action, authorized_by etc
245       #
246       # activate_when do
247       #    File.exist?("/usr/bin/puppet")
248       # end
249       def self.activate_when(&block)
250         (class << self; self; end).instance_eval do
251           define_method("activate?", &block)
252         end
253       end
254
255       # Creates a new action with the block passed and sets some defaults
256       #
257       # action "status" do
258       #    # logic here to restart service
259       # end
260       def self.action(name, &block)
261         raise "Need to pass a body for the action" unless block_given?
262
263         self.module_eval { define_method("#{name}_action", &block) }
264       end
265
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
270
271         # turns foo_bar into FooBar
272         plugin = plugin.to_s.split("_").map {|v| v.capitalize}.join
273         pluginname = "MCollective::Util::#{plugin}"
274
275         PluginManager.loadclass(pluginname) unless MCollective::Util.constants.include?(plugin)
276
277         class_eval("
278                       def authorization_hook(request)
279                    #{pluginname}.authorize(request)
280                       end
281                    ")
282       end
283
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:
286       #
287       # validate :msg, String
288       # validate :msg, /^[\w\s]+$/
289       #
290       # There are also some special helper validators:
291       #
292       # validate :command, :shellsafe
293       # validate :command, :ipv6address
294       # validate :command, :ipv4address
295       # validate :command, :boolean
296       # validate :command, ["start", "stop"]
297       #
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)
301
302         Validator.validate(@request[key], validation)
303       rescue ValidatorError => e
304         raise InvalidRPCData, "Input %s did not pass validation: %s" % [ key, e.message ]
305       end
306
307       # convenience wrapper around Util#shellescape
308       def shellescape(str)
309         Util.shellescape(str)
310       end
311
312       # handles external actions
313       def implemented_by(command, type=:json)
314         runner = ActionRunner.new(command, request, type)
315
316         res = runner.run
317
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)
320
321         reply.data.merge!(res[:data])
322
323         if res[:exitstatus] > 0
324           reply.fail "Failed to run #{command}: #{res[:stderr]}", res[:exitstatus]
325         end
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}"
329       end
330
331       # Called at the end of the RPC::Agent standard initialize method
332       # use this to adjust meta parameters, timeouts and any setup you
333       # need to do.
334       #
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.
338       def startup_hook
339       end
340
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)
346       end
347
348       # Called at the end of processing just before the response gets sent
349       # to the middleware.
350       #
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
357       end
358
359       # Gets called right after a request was received and calls audit plugins
360       #
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)
368       end
369     end
370   end
371 end