Updated mcollective.init according to OSCI-658
[packages/precise/mcollective.git] / lib / mcollective / rpc / actionrunner.rb
diff --git a/lib/mcollective/rpc/actionrunner.rb b/lib/mcollective/rpc/actionrunner.rb
new file mode 100644 (file)
index 0000000..f2a65d5
--- /dev/null
@@ -0,0 +1,142 @@
+module MCollective
+  module RPC
+    # A helper used by RPC::Agent#implemented_by to delegate an action to
+    # an external script.  At present only JSON based serialization is
+    # supported in future ones based on key=val pairs etc will be added
+    #
+    # It serializes the request object into an input file and creates an
+    # empty output file.  It then calls the external command reading the
+    # output file at the end.
+    #
+    # any STDERR gets logged at error level and any STDOUT gets logged at
+    # info level.
+    #
+    # It will interpret the exit code from the application the same way
+    # RPC::Reply#fail! and #fail handles their codes creating a consistent
+    # interface, the message part of the fail message will come from STDERR
+    #
+    # Generally externals should just exit with code 1 on failure and print to
+    # STDERR, this is exactly what Perl die() does and translates perfectly
+    # to our model
+    #
+    # It uses the MCollective::Shell wrapper to call the external application
+    class ActionRunner
+      attr_reader :command, :agent, :action, :format, :stdout, :stderr, :request
+
+      def initialize(command, request, format=:json)
+        @agent = request.agent
+        @action = request.action
+        @format = format
+        @request = request
+        @command = path_to_command(command)
+        @stdout = ""
+        @stderr = ""
+      end
+
+      def run
+        unless canrun?(command)
+          Log.warn("Cannot run #{to_s}")
+          raise RPCAborted, "Cannot execute #{to_s}"
+        end
+
+        Log.debug("Running #{to_s}")
+
+        request_file = saverequest(request)
+        reply_file = tempfile("reply")
+        reply_file.close
+
+        runner = shell(command, request_file.path, reply_file.path)
+
+        runner.runcommand
+
+        Log.debug("#{command} exited with #{runner.status.exitstatus}")
+
+        stderr.each_line {|l| Log.error("#{to_s}: #{l.chomp}")} unless stderr.empty?
+        stdout.each_line {|l| Log.info("#{to_s}: #{l.chomp}")} unless stdout.empty?
+
+        {:exitstatus => runner.status.exitstatus,
+         :stdout     => runner.stdout,
+         :stderr     => runner.stderr,
+         :data       => load_results(reply_file.path)}
+      ensure
+        request_file.close! if request_file.respond_to?("close!")
+        reply_file.close! if reply_file.respond_to?("close")
+      end
+
+      def shell(command, infile, outfile)
+        env = {"MCOLLECTIVE_REQUEST_FILE" => infile,
+               "MCOLLECTIVE_REPLY_FILE"   => outfile}
+
+        Shell.new("#{command} #{infile} #{outfile}", :cwd => Dir.tmpdir, :stdout => stdout, :stderr => stderr, :environment => env)
+      end
+
+      def load_results(file)
+        Log.debug("Attempting to load results in #{format} format from #{file}")
+
+        data = {}
+
+        if respond_to?("load_#{format}_results")
+          tempdata = send("load_#{format}_results", file)
+
+          tempdata.each_pair do |k,v|
+            data[k.to_sym] = v
+          end
+        end
+
+        data
+      rescue Exception => e
+        {}
+      end
+
+      def load_json_results(file)
+        return {} unless File.readable?(file)
+
+        JSON.load(File.read(file)) || {}
+      rescue JSON::ParserError
+        {}
+      end
+
+      def saverequest(req)
+        Log.debug("Attempting to save request in #{format} format")
+
+        if respond_to?("save_#{format}_request")
+          data = send("save_#{format}_request", req)
+
+          request_file = tempfile("request")
+          request_file.puts data
+          request_file.close
+        end
+
+        request_file
+      end
+
+      def save_json_request(req)
+        req.to_json
+      end
+
+      def canrun?(command)
+        File.executable?(command)
+      end
+
+      def to_s
+        "%s#%s command: %s" % [ agent, action, command ]
+      end
+
+      def tempfile(prefix)
+        Tempfile.new("mcollective_#{prefix}", Dir.tmpdir)
+      end
+
+      def path_to_command(command)
+        unless command[0,1] == File::SEPARATOR
+          Config.instance.libdir.each do |libdir|
+            command_file = File.join(libdir, "agent", agent, command)
+
+            return command_file if File.exist?(command_file)
+          end
+        end
+
+        return command
+      end
+    end
+  end
+end