f2a65d502c07c69a69c6906db4b5d5c016d3a70b
[packages/precise/mcollective.git] / lib / mcollective / rpc / actionrunner.rb
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