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