shell_command_agent.rb 4.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. module Agents
  2. class ShellCommandAgent < Agent
  3. default_schedule "never"
  4. can_dry_run!
  5. def self.should_run?
  6. ENV['ENABLE_INSECURE_AGENTS'] == "true"
  7. end
  8. description <<-MD
  9. The Shell Command Agent will execute commands on your local system, returning the output.
  10. `command` specifies the command (either a shell command line string or an array of command line arguments) to be executed, and `path` will tell ShellCommandAgent in what directory to run this command. The content of `stdin` will be fed to the command via the standard input.
  11. `expected_update_period_in_days` is used to determine if the Agent is working.
  12. ShellCommandAgent can also act upon received events. When receiving an event, this Agent's options can interpolate values from the incoming event.
  13. For example, your command could be defined as `{{cmd}}`, in which case the event's `cmd` property would be used.
  14. 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.
  15. If `suppress_on_failure` is set to true, no event is emitted when `exit_status` is not zero.
  16. If `suppress_on_empty_output` is set to true, no event is emitted when `output` is empty.
  17. *Warning*: This type of Agent runs arbitrary commands on your system, #{Agents::ShellCommandAgent.should_run? ? "but is **currently enabled**" : "and is **currently disabled**"}.
  18. Only enable this Agent if you trust everyone using your Huginn installation.
  19. You can enable this Agent in your .env file by setting `ENABLE_INSECURE_AGENTS` to `true`.
  20. MD
  21. event_description <<-MD
  22. Events look like this:
  23. {
  24. "command": "pwd",
  25. "path": "/home/Huginn",
  26. "exit_status": 0,
  27. "errors": "",
  28. "output": "/home/Huginn"
  29. }
  30. MD
  31. def default_options
  32. {
  33. 'path' => "/",
  34. 'command' => "pwd",
  35. 'suppress_on_failure' => false,
  36. 'suppress_on_empty_output' => false,
  37. 'expected_update_period_in_days' => 1
  38. }
  39. end
  40. def validate_options
  41. unless options['path'].present? && options['command'].present? && options['expected_update_period_in_days'].present?
  42. errors.add(:base, "The path, command, and expected_update_period_in_days fields are all required.")
  43. end
  44. case options['stdin']
  45. when String, nil
  46. else
  47. errors.add(:base, "stdin must be a string.")
  48. end
  49. unless Array(options['command']).all? { |o| o.is_a?(String) }
  50. errors.add(:base, "command must be a shell command line string or an array of command line arguments.")
  51. end
  52. unless File.directory?(options['path'])
  53. errors.add(:base, "#{options['path']} is not a real directory.")
  54. end
  55. end
  56. def working?
  57. Agents::ShellCommandAgent.should_run? && event_created_within?(interpolated['expected_update_period_in_days']) && !recent_error_logs?
  58. end
  59. def receive(incoming_events)
  60. incoming_events.each do |event|
  61. handle(interpolated(event), event)
  62. end
  63. end
  64. def check
  65. handle(interpolated)
  66. end
  67. private
  68. def handle(opts, event = nil)
  69. if Agents::ShellCommandAgent.should_run?
  70. command = opts['command']
  71. path = opts['path']
  72. stdin = opts['stdin']
  73. result, errors, exit_status = run_command(path, command, stdin)
  74. payload = {
  75. 'command' => command,
  76. 'path' => path,
  77. 'exit_status' => exit_status,
  78. 'errors' => errors,
  79. 'output' => result,
  80. }
  81. unless suppress_event?(payload)
  82. created_event = create_event payload: payload
  83. end
  84. log("Ran '#{command}' under '#{path}'", outbound_event: created_event, inbound_event: event)
  85. else
  86. log("Unable to run because insecure agents are not enabled. Edit ENABLE_INSECURE_AGENTS in the Huginn .env configuration.")
  87. end
  88. end
  89. def run_command(path, command, stdin)
  90. begin
  91. rout, wout = IO.pipe
  92. rerr, werr = IO.pipe
  93. rin, win = IO.pipe
  94. pid = spawn(*command, chdir: path, out: wout, err: werr, in: rin)
  95. wout.close
  96. werr.close
  97. rin.close
  98. if stdin
  99. win.write stdin
  100. win.close
  101. end
  102. (result = rout.read).strip!
  103. (errors = rerr.read).strip!
  104. _, status = Process.wait2(pid)
  105. exit_status = status.exitstatus
  106. rescue => e
  107. errors = e.to_s
  108. result = ''.freeze
  109. exit_status = nil
  110. end
  111. [result, errors, exit_status]
  112. end
  113. def suppress_event?(payload)
  114. (boolify(interpolated['suppress_on_failure']) && payload['exit_status'].nonzero?) ||
  115. (boolify(interpolated['suppress_on_empty_output']) && payload['output'].empty?)
  116. end
  117. end
  118. end