Merge pull request #237 from cantino/ZirconCode-master

ShellCommandAgent

Andrew Cantino 11 years ago
parent
commit
f001638943
3 changed files with 214 additions and 0 deletions
  1. 4 0
      .env.example
  2. 111 0
      app/models/agents/shell_command_agent.rb
  3. 99 0
      spec/models/agents/shell_command_agent_spec.rb

+ 4 - 0
.env.example

@@ -86,6 +86,10 @@ AWS_SANDBOX=false
86 86
 # You should not allow this on a shared Huginn box because it is not secure.
87 87
 ALLOW_JSONPATH_EVAL=false
88 88
 
89
+# Enable this setting to allow insecure Agents like the ShellCommandAgent.  Only do this
90
+# when you trust everyone using your Huginn installation.
91
+ENABLE_INSECURE_AGENTS=false
92
+
89 93
 # Use Graphviz for generating diagrams instead of using Google Chart
90 94
 # Tools.  Specify a dot(1) command path built with SVG support
91 95
 # enabled.

+ 111 - 0
app/models/agents/shell_command_agent.rb

@@ -0,0 +1,111 @@
1
+require 'open3'
2
+
3
+module Agents
4
+  class ShellCommandAgent < Agent
5
+    default_schedule "never"
6
+
7
+    def self.should_run?
8
+      ENV['ENABLE_INSECURE_AGENTS'] == "true"
9
+    end
10
+
11
+    description <<-MD
12
+      The ShellCommandAgent can execute commands on your local system, returning the output.
13
+
14
+      `command` specifies the command to be executed, and `path` will tell ShellCommandAgent in what directory to run this command.
15
+
16
+      `expected_update_period_in_days` is used to determine if the Agent is working.
17
+
18
+      ShellCommandAgent can also act upon received events. These events may contain their own `path` and `command` values. If they do not, ShellCommandAgent will use the configured options. For this reason, please specify defaults even if you are planning to have this Agent to respond to events.
19
+
20
+      The resulting event will contain the `command` which was executed, the `path` it was executed under, the `exit_status` of the command, the `errors`, and the actual `output`. ShellCommandAgent will not log an error if the result implies that something went wrong.
21
+
22
+      *Warning*: This type of Agent runs arbitrary commands on your system, #{Agents::ShellCommandAgent.should_run? ? "but is **currently enabled**" : "and is **currently disabled**"}.
23
+      Only enable this Agent if you trust everyone using your Huginn installation.
24
+      You can enable this Agent in your .env file by setting `ENABLE_INSECURE_AGENTS` to `true`.
25
+    MD
26
+
27
+    event_description <<-MD
28
+    Events look like this:
29
+
30
+      {
31
+        'command' => 'pwd',
32
+        'path' => '/home/Huginn',
33
+        'exit_status' => '0',
34
+        'errors' => '',
35
+        'output' => '/home/Huginn' 
36
+      }
37
+    MD
38
+
39
+    def default_options
40
+      {
41
+          'path' => "/",
42
+          'command' => "pwd",
43
+          'expected_update_period_in_days' => 1
44
+      }
45
+    end
46
+
47
+    def validate_options
48
+      unless options['path'].present? && options['command'].present? && options['expected_update_period_in_days'].present?
49
+        errors.add(:base, "The path, command, and expected_update_period_in_days fields are all required.")
50
+      end
51
+
52
+      unless File.directory?(options['path'])
53
+        errors.add(:base, "#{options['path']} is not a real directory.")
54
+      end
55
+    end
56
+
57
+    def working?
58
+      Agents::ShellCommandAgent.should_run? && event_created_within?(options['expected_update_period_in_days']) && !recent_error_logs?
59
+    end
60
+
61
+    def receive(incoming_events)
62
+      incoming_events.each do |event|
63
+        handle(event.payload, event)
64
+      end
65
+    end
66
+
67
+    def check
68
+      handle(options)
69
+    end
70
+
71
+    private
72
+
73
+    def handle(opts = options, event = nil)
74
+      if Agents::ShellCommandAgent.should_run?
75
+        command = opts['command'] || options['command']
76
+        path = opts['path'] || options['path']
77
+
78
+        result, errors, exit_status = run_command(path, command)
79
+
80
+        vals = {"command" => command, "path" => path, "exit_status" => exit_status, "errors" => errors, "output" => result}
81
+        created_event = create_event :payload => vals
82
+
83
+        log("Ran '#{command}' under '#{path}'", :outbound_event => created_event, :inbound_event => event)
84
+      else
85
+        log("Unable to run because insecure agents are not enabled.  Edit ENABLE_INSECURE_AGENTS in the Huginn .env configuration.")
86
+      end
87
+    end
88
+
89
+    def run_command(path, command)
90
+      result = nil
91
+      errors = nil
92
+      exit_status = nil
93
+
94
+      Dir.chdir(path){
95
+        begin
96
+          stdin, stdout, stderr, wait_thr = Open3.popen3(command)
97
+          exit_status = wait_thr.value.to_i
98
+          result = stdout.gets(nil)
99
+          errors = stderr.gets(nil)
100
+        rescue Exception => e
101
+          errors = e.to_s
102
+        end
103
+      }
104
+
105
+      result = result.to_s.strip
106
+      errors = errors.to_s.strip
107
+
108
+      [result, errors, exit_status]
109
+    end
110
+  end
111
+end

+ 99 - 0
spec/models/agents/shell_command_agent_spec.rb

@@ -0,0 +1,99 @@
1
+require 'spec_helper'
2
+
3
+describe Agents::ShellCommandAgent do
4
+  before do
5
+    @valid_path = Dir.pwd
6
+
7
+    @valid_params = {
8
+        :path  => @valid_path,
9
+        :command  => "pwd",
10
+        :expected_update_period_in_days => "1",
11
+      }
12
+
13
+    @checker = Agents::ShellCommandAgent.new(:name => "somename", :options => @valid_params)
14
+    @checker.user = users(:jane)
15
+    @checker.save!
16
+
17
+    @event = Event.new
18
+    @event.agent = agents(:jane_weather_agent)
19
+    @event.payload = {
20
+      :command => "ls"
21
+    }
22
+    @event.save!
23
+
24
+    stub(Agents::ShellCommandAgent).should_run? { true }
25
+  end
26
+
27
+  describe "validation" do
28
+    before do
29
+      @checker.should be_valid
30
+    end
31
+
32
+    it "should validate presence of necessary fields" do
33
+      @checker.options[:command] = nil
34
+      @checker.should_not be_valid
35
+    end
36
+
37
+    it "should validate path" do
38
+      @checker.options[:path] = 'notarealpath/itreallyisnt'
39
+      @checker.should_not be_valid
40
+    end
41
+
42
+    it "should validate path" do
43
+      @checker.options[:path] = '/'
44
+      @checker.should be_valid
45
+    end
46
+  end
47
+
48
+  describe "#working?" do
49
+    it "generating events as scheduled" do
50
+      stub(@checker).run_command(@valid_path, 'pwd') { ["fake pwd output", "", 0] }
51
+
52
+      @checker.should_not be_working
53
+      @checker.check
54
+      @checker.reload.should be_working
55
+      three_days_from_now = 3.days.from_now
56
+      stub(Time).now { three_days_from_now }
57
+      @checker.should_not be_working
58
+    end
59
+  end
60
+
61
+  describe "#check" do
62
+    before do
63
+      stub(@checker).run_command(@valid_path, 'pwd') { ["fake pwd output", "", 0] }
64
+    end
65
+
66
+    it "should create an event when checking" do
67
+      expect { @checker.check }.to change { Event.count }.by(1)
68
+      Event.last.payload[:path].should == @valid_path
69
+      Event.last.payload[:command].should == 'pwd'
70
+      Event.last.payload[:output].should == "fake pwd output"
71
+    end
72
+
73
+    it "does not run when should_run? is false" do
74
+      stub(Agents::ShellCommandAgent).should_run? { false }
75
+      expect { @checker.check }.not_to change { Event.count }
76
+    end
77
+  end
78
+
79
+  describe "#receive" do
80
+    before do
81
+      stub(@checker).run_command(@valid_path, @event.payload[:command]) { ["fake ls output", "", 0] }
82
+    end
83
+
84
+    it "creates events" do
85
+      @checker.receive([@event])
86
+      Event.last.payload[:path].should == @valid_path
87
+      Event.last.payload[:command].should == @event.payload[:command]
88
+      Event.last.payload[:output].should == "fake ls output"
89
+    end
90
+
91
+    it "does not run when should_run? is false" do
92
+      stub(Agents::ShellCommandAgent).should_run? { false }
93
+
94
+      expect {
95
+        @checker.receive([@event])
96
+      }.not_to change { Event.count }
97
+    end
98
+  end
99
+end