shell_command_agent.rb 4.9KB

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