executo 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +30 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +181 -0
- data/LICENSE.txt +21 -0
- data/README.md +66 -0
- data/Rakefile +19 -0
- data/bin/console +15 -0
- data/bin/executo +9 -0
- data/bin/setup +8 -0
- data/bin/sidekiq +29 -0
- data/bin/sidekiqctl +29 -0
- data/boot.rb +19 -0
- data/boot_activejob.rb +30 -0
- data/executo.gemspec +44 -0
- data/lib/executo/cli.rb +63 -0
- data/lib/executo/command.rb +56 -0
- data/lib/executo/command_dsl.rb +36 -0
- data/lib/executo/commands/imapsync_test.rb +52 -0
- data/lib/executo/configuration.rb +21 -0
- data/lib/executo/encrypted_worker.rb +22 -0
- data/lib/executo/feedback_process_job.rb +15 -0
- data/lib/executo/feedback_process_service.rb +41 -0
- data/lib/executo/scheduler_worker.rb +33 -0
- data/lib/executo/tagged_logger.rb +19 -0
- data/lib/executo/version.rb +5 -0
- data/lib/executo/worker.rb +102 -0
- data/lib/executo.rb +106 -0
- data/sidekiq.yml +16 -0
- data/support/Gemfile +8 -0
- data/support/boot.rb +41 -0
- data/support/executo.conf +21 -0
- data/support/executo.service +26 -0
- data/support/sidekiq.yml +17 -0
- data/test.rb +20 -0
- data/uptime.rb +4 -0
- metadata +246 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Executo
|
|
4
|
+
class Command
|
|
5
|
+
include CommandDsl
|
|
6
|
+
include TaggedLogger
|
|
7
|
+
|
|
8
|
+
attr_reader :executo_id, :parameter_values, :status, :stdout, :stderr, :exitstatus
|
|
9
|
+
|
|
10
|
+
def initialize(*args)
|
|
11
|
+
@executo_id = args.first&.delete(:id) || SecureRandom.uuid
|
|
12
|
+
@errors = ActiveModel::Errors.new(self)
|
|
13
|
+
@parameter_values = args.first&.delete(:parameter_values) || {}
|
|
14
|
+
super(*args)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
raise 'missing target' unless target
|
|
19
|
+
|
|
20
|
+
perform
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def process_results(results)
|
|
24
|
+
state = results[:state]
|
|
25
|
+
logger.debug("Processing #{state} results")
|
|
26
|
+
public_send(state.to_sym, results) if respond_to?(state.to_sym)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def setup_logger(id)
|
|
30
|
+
logger_add_tag(self.class.name)
|
|
31
|
+
logger_add_tag(id)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def perform
|
|
37
|
+
Executo.publish(target: target, command: command, parameters: safe_parameters, feedback: { service: self.class.name, id: executo_id, arguments: attributes.to_h })
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def safe_parameters
|
|
41
|
+
local_parameter_values = {}
|
|
42
|
+
local_parameter_values = implicit_parameter_values if respond_to?(:implicit_parameter_values)
|
|
43
|
+
local_parameter_values = local_parameter_values.merge(parameter_values)
|
|
44
|
+
|
|
45
|
+
parameters.split.map { |parameter| parameter % local_parameter_values }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
def process_feedback(feedback, results)
|
|
50
|
+
cmd = new(feedback['arguments'].merge(id: feedback['id']))
|
|
51
|
+
cmd.setup_logger(feedback['id'])
|
|
52
|
+
cmd.process_results(results.symbolize_keys)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Executo
|
|
4
|
+
module CommandDsl
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
include ActiveAttr::Model
|
|
7
|
+
|
|
8
|
+
delegate :target, :command, :parameters, :feedback_interval, to: :class
|
|
9
|
+
|
|
10
|
+
class_methods do
|
|
11
|
+
def call(*args)
|
|
12
|
+
new(*args).call
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def target(value = nil)
|
|
16
|
+
@target = value if value.present?
|
|
17
|
+
@target
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def command(value = nil)
|
|
21
|
+
@command = value if value.present?
|
|
22
|
+
@command
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def parameters(value = nil)
|
|
26
|
+
@parameters = value if value.present?
|
|
27
|
+
@parameters || ''
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def feedback_interval(value = nil)
|
|
31
|
+
@feedback_interval = value if value.present?
|
|
32
|
+
@feedback_interval || 10
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Call with:
|
|
4
|
+
# ImapSyncTest.call(mailbox_id: 'abc', parameter_values: {host1: 1, user1: 1, password1: 1, host2: 2, user2: 2, password2: 2})
|
|
5
|
+
|
|
6
|
+
class ImapSyncTest < Executo::Command
|
|
7
|
+
target 'localhost'
|
|
8
|
+
command '/usr/local/bin/imapsync'
|
|
9
|
+
parameters '--dry --host1 %<host1>s --user1 %<user1>s --password1 %<password1>s --host2 %<host2>s --user2 %<user2>s --password2 %<password2>s --logfile %<logfile>s --delete2'
|
|
10
|
+
attribute :mailbox_id
|
|
11
|
+
feedback_interval 10
|
|
12
|
+
|
|
13
|
+
# Callback for 'started' messages. results contain stdout/stderr, atribute mailbox_id will already be set.
|
|
14
|
+
def started(results)
|
|
15
|
+
logger.info "Started #{mailbox_id}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Process intermediate output from the command. results contain stdout/stderr/runtime_seconds
|
|
19
|
+
def output(results)
|
|
20
|
+
find_current_status(results[:stdout])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Process completed output from the command. results contain stdout/stderr/exitstatus/runtime_seconds
|
|
24
|
+
def completed(results)
|
|
25
|
+
binding.pry
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Process failed output from the command. results contain stdout/stderr/exitstatus/runtime_seconds
|
|
29
|
+
def failed(results)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Process final output from the command. results contain runtime_seconds
|
|
33
|
+
def finished(results)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def find_current_status(lines)
|
|
39
|
+
line = lines.reverse.find { |l| l =~ %r[\d+/\d+ msgs left] }
|
|
40
|
+
return unless line
|
|
41
|
+
|
|
42
|
+
match = line.match(%r[(?<current>\d+)/(?<total>\d+) msgs left])
|
|
43
|
+
|
|
44
|
+
logger.info "Processing #{mailbox_id} - #{match[:current]}/#{match[:total]} (#{(match[:current].to_i.to_f / match[:total].to_i).to_i }%)"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def implicit_parameter_values
|
|
48
|
+
{
|
|
49
|
+
logfile: "/tmp/imapsync_#{executo_id}.log"
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Executo
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :redis
|
|
6
|
+
attr_accessor :active_job_redis
|
|
7
|
+
attr_writer :logger
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@redis = {}
|
|
11
|
+
@active_job_redis = {}
|
|
12
|
+
@logger = ActiveSupport::TaggedLogging.new(Logger.new($stdout))
|
|
13
|
+
@logger.level = Logger::INFO
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# logger [Object].
|
|
17
|
+
def logger
|
|
18
|
+
@logger.is_a?(Proc) ? instance_exec(&@logger) : @logger
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'executo/worker'
|
|
4
|
+
|
|
5
|
+
module Executo
|
|
6
|
+
class EncryptedWorker < Worker
|
|
7
|
+
# @param [String] encrypted_command
|
|
8
|
+
# @param [Array] encrypted_params
|
|
9
|
+
# @param [Hash] encrypted_options
|
|
10
|
+
def perform(encrypted_command, encrypted_params = [], encrypted_options = {})
|
|
11
|
+
Executo.config.logger.debug "encrypted_command: #{encrypted_command}"
|
|
12
|
+
Executo.config.logger.debug "encrypted_params: #{encrypted_params}"
|
|
13
|
+
Executo.config.logger.debug "encrypted_options: #{encrypted_options}"
|
|
14
|
+
|
|
15
|
+
command = Executo.decrypt(encrypted_command)
|
|
16
|
+
params = Executo.decrypt(encrypted_params)
|
|
17
|
+
options = Executo.decrypt(encrypted_options)
|
|
18
|
+
|
|
19
|
+
super(command, params, options)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Executo
|
|
4
|
+
class FeedbackProcessJob < ActiveJob::Base
|
|
5
|
+
def perform(feedback, results)
|
|
6
|
+
feedback_service_class = feedback['service']&.safe_constantize
|
|
7
|
+
unless feedback_service_class
|
|
8
|
+
Executo.logger.error("Feedback service #{feedback['service']} not found")
|
|
9
|
+
return
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
feedback_service_class.process_feedback(feedback, results)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Executo
|
|
4
|
+
class FeedbackProcessService
|
|
5
|
+
include Executo::TaggedLogger
|
|
6
|
+
|
|
7
|
+
attr_reader :id, :state, :exitstatus, :stdout, :stderr
|
|
8
|
+
attr_writer :arguments
|
|
9
|
+
|
|
10
|
+
def initialize(feedback, results)
|
|
11
|
+
@id = feedback['id']
|
|
12
|
+
@state = results['state']
|
|
13
|
+
@exitstatus = results['exitstatus']
|
|
14
|
+
@stdout = results['stdout'] || []
|
|
15
|
+
@stderr = results['stderr'] || []
|
|
16
|
+
@arguments = feedback['arguments'] || {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
logger_add_tag(self.class.name)
|
|
21
|
+
logger_add_tag(id)
|
|
22
|
+
perform
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def perform; end
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
def arguments(*names)
|
|
31
|
+
names.each do |name|
|
|
32
|
+
define_method(name) { instance_variable_get('@arguments')[name.to_s] }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def process_feedback(feedback, results)
|
|
37
|
+
new(feedback, results).call
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# https://gist.github.com/jordinl83/08ad9afd8f5046ddd9d38bcebf373e74
|
|
4
|
+
|
|
5
|
+
module Executo
|
|
6
|
+
class SchedulerWorker
|
|
7
|
+
include Sidekiq::Worker
|
|
8
|
+
sidekiq_options queue: 'critical'
|
|
9
|
+
|
|
10
|
+
def perform
|
|
11
|
+
execution_time = Time.now.utc
|
|
12
|
+
execution_time -= execution_time.sec
|
|
13
|
+
|
|
14
|
+
self.class.perform_at(execution_time + 60) unless scheduled?
|
|
15
|
+
|
|
16
|
+
# SCHEDULE.each do |(worker_class, schedule_lambda)|
|
|
17
|
+
# worker_class.perform_async if !scheduled?(worker_class) && schedule_lambda.call(execution_time)
|
|
18
|
+
# end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def scheduled?(worker_class = self.class)
|
|
22
|
+
scheduled_workers[worker_class.name]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def scheduled_workers
|
|
28
|
+
@scheduled_workers ||= Sidekiq::ScheduledSet.new.entries.each_with_object({}) do |item, hash|
|
|
29
|
+
hash[item['class']] = true
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Executo
|
|
4
|
+
module TaggedLogger
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
def logger_add_tag(tag)
|
|
9
|
+
return unless tag.present?
|
|
10
|
+
|
|
11
|
+
@logger = logger.tagged(tag)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def logger
|
|
15
|
+
@logger ||= Executo.logger
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Executo
|
|
4
|
+
class Worker
|
|
5
|
+
include Sidekiq::Worker
|
|
6
|
+
include Executo::TaggedLogger
|
|
7
|
+
|
|
8
|
+
attr_reader :options, :output, :flushable_output
|
|
9
|
+
|
|
10
|
+
# @param [String] command
|
|
11
|
+
# @param [Array] params
|
|
12
|
+
# @param [Hash] options
|
|
13
|
+
def perform(command, params = [], options = {})
|
|
14
|
+
@options = options
|
|
15
|
+
@output ||= Hash.new([])
|
|
16
|
+
@flushable_output ||= Hash.new([])
|
|
17
|
+
@started_at = Time.current
|
|
18
|
+
@flushed_at = Time.current
|
|
19
|
+
|
|
20
|
+
logger_add_tag('Worker')
|
|
21
|
+
logger_add_tag(options.dig('feedback', 'id'))
|
|
22
|
+
logger_add_tag(command)
|
|
23
|
+
|
|
24
|
+
logger.debug "params: #{params}"
|
|
25
|
+
logger.debug "options: #{options}"
|
|
26
|
+
|
|
27
|
+
send_feedback(state: 'started')
|
|
28
|
+
|
|
29
|
+
status = execute(command, params, options)
|
|
30
|
+
send_feedback(
|
|
31
|
+
state: status.success? ? 'completed' : 'failed',
|
|
32
|
+
exitstatus: status.exitstatus.to_i,
|
|
33
|
+
stdout: output[:stdout],
|
|
34
|
+
stderr: output[:stderr],
|
|
35
|
+
pid: status.pid
|
|
36
|
+
)
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
logger.error "Exception: #{e.class} - #{e.message}"
|
|
39
|
+
logger.error e.backtrace.join("\n")
|
|
40
|
+
send_feedback(state: 'failed')
|
|
41
|
+
ensure
|
|
42
|
+
send_feedback(state: 'finished')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def execute(command, params = [], options = {})
|
|
48
|
+
argument_list = [command] + params
|
|
49
|
+
stdin_content = options['stdin_content'] || []
|
|
50
|
+
stdin_newlines = options.key?('stdin_newlines') ? options['stdin_newlines'] : true
|
|
51
|
+
shell_escape = options.key?('shell_escape') ? options['shell_escape'] : true
|
|
52
|
+
|
|
53
|
+
CLI.run(argument_list, stdout: ->(line) { register_output(:stdout, line) }, stderr: ->(line) { register_output(:stderr, line) }, stdin_content: stdin_content, stdin_newlines: stdin_newlines, shell_escape: shell_escape)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def register_output(channel, value)
|
|
57
|
+
return unless value
|
|
58
|
+
|
|
59
|
+
value = value.split("\n").map { |line| line.chomp.force_encoding('utf-8') }
|
|
60
|
+
output[channel] += value
|
|
61
|
+
flushable_output[channel] += value
|
|
62
|
+
logger.debug "Output: #{channel}: #{value}"
|
|
63
|
+
flush_if_needed
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def flush_if_needed
|
|
67
|
+
return if time_since_flushed < (options.dig('feedback', 'flush_interval') || 10)
|
|
68
|
+
return if flushable_output[:stdout].blank? && flushable_output[:stderr].blank?
|
|
69
|
+
|
|
70
|
+
flush_stdout = flushable_output[:stdout].dup
|
|
71
|
+
flushable_output[:stdout].clear
|
|
72
|
+
flush_stderr = flushable_output[:stderr].dup
|
|
73
|
+
flushable_output[:stderr].clear
|
|
74
|
+
|
|
75
|
+
send_feedback(state: 'output', stdout: flush_stdout, stderr: flush_stderr)
|
|
76
|
+
@flushed_at = Time.current
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def time_since_flushed
|
|
80
|
+
(Time.current - @flushed_at).to_i
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def send_feedback(results)
|
|
84
|
+
results.merge!(runtime_seconds: (Time.current - @started_at).to_i)
|
|
85
|
+
logger.info "Command #{results[:state]} after #{results[:runtime_seconds]} seconds"
|
|
86
|
+
Sidekiq::Client.new(Executo.active_job_connection_pool).push(
|
|
87
|
+
'class' => ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper,
|
|
88
|
+
'queue' => 'default',
|
|
89
|
+
'wrapped' => 'Executo::FeedbackProcessJob',
|
|
90
|
+
'args' => [
|
|
91
|
+
{
|
|
92
|
+
'job_class' => 'Executo::FeedbackProcessJob',
|
|
93
|
+
'arguments' => [
|
|
94
|
+
options['feedback'],
|
|
95
|
+
results.deep_stringify_keys
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
data/lib/executo.rb
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/message_encryptor'
|
|
4
|
+
require 'active_support/tagged_logging'
|
|
5
|
+
require 'sidekiq'
|
|
6
|
+
require 'active_job'
|
|
7
|
+
require 'securerandom'
|
|
8
|
+
require 'active_attr'
|
|
9
|
+
|
|
10
|
+
require 'executo/cli'
|
|
11
|
+
require 'executo/tagged_logger'
|
|
12
|
+
require 'executo/configuration'
|
|
13
|
+
require 'executo/version'
|
|
14
|
+
require 'executo/encrypted_worker'
|
|
15
|
+
require 'executo/scheduler_worker'
|
|
16
|
+
require 'executo/worker'
|
|
17
|
+
require 'executo/feedback_process_job'
|
|
18
|
+
require 'executo/feedback_process_service'
|
|
19
|
+
require 'executo/command_dsl'
|
|
20
|
+
require 'executo/command'
|
|
21
|
+
|
|
22
|
+
require 'executo/commands/imapsync_test'
|
|
23
|
+
|
|
24
|
+
module Executo
|
|
25
|
+
class Error < StandardError; end
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
attr_reader :config
|
|
29
|
+
|
|
30
|
+
delegate :logger, to: :config
|
|
31
|
+
|
|
32
|
+
def setup
|
|
33
|
+
@config = Configuration.new
|
|
34
|
+
yield config
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def cryptor
|
|
38
|
+
@cryptor ||= ActiveSupport::MessageEncryptor.new(ENV['EXECUTO_KEY'])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def encrypt(obj)
|
|
42
|
+
cryptor.encrypt_and_sign(obj)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def decrypt(string)
|
|
46
|
+
cryptor.decrypt_and_verify(string)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
##
|
|
50
|
+
# Publishes a command to be executed on a target
|
|
51
|
+
#
|
|
52
|
+
# @param [String] target a server or a role
|
|
53
|
+
# @param [String] command command to be executed
|
|
54
|
+
# @param [Array] params params for the command
|
|
55
|
+
# @param [Boolean] encrypt whether to encrypt all parameters
|
|
56
|
+
# @param [Hash] options options for the worker
|
|
57
|
+
# @param [Hash] job_options, options for sidekiq, valid options are:
|
|
58
|
+
# queue - the named queue to use, default 'default'
|
|
59
|
+
# class - the worker class to call, required
|
|
60
|
+
# args - an array of simple arguments to the perform method, must be JSON-serializable
|
|
61
|
+
# at - timestamp to schedule the job (optional), must be Numeric (e.g. Time.now.to_f)
|
|
62
|
+
# retry - whether to retry this job if it fails, default true or an integer number of retries
|
|
63
|
+
# backtrace - whether to save any error backtrace, default false
|
|
64
|
+
def publish(target:, command:, parameters: [], encrypt: false, options: {}, job_options: {}, feedback: {})
|
|
65
|
+
options['feedback'] = feedback&.stringify_keys
|
|
66
|
+
options['feedback']['id'] ||= SecureRandom.uuid
|
|
67
|
+
|
|
68
|
+
args = [command, parameters, options.deep_stringify_keys]
|
|
69
|
+
args = args.map { |a| encrypt(a) } if encrypt
|
|
70
|
+
|
|
71
|
+
sidekiq_options = { 'retry' => 0 }.merge(job_options).merge(
|
|
72
|
+
'queue' => target,
|
|
73
|
+
'class' => encrypt ? 'Executo::EncryptedWorker' : 'Executo::Worker',
|
|
74
|
+
'args' => args
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if defined?(Rails) && Rails.env.test?
|
|
78
|
+
$executo_jobs ||= {}
|
|
79
|
+
$executo_jobs[options.dig('feedback', 'id')] = sidekiq_options
|
|
80
|
+
else
|
|
81
|
+
Sidekiq::Client.new(connection_pool).push(sidekiq_options)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
logger.info("Published #{command} to #{target} with id #{options['feedback']['id']}")
|
|
85
|
+
options.dig('feedback', 'id')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def schedule(target, list)
|
|
89
|
+
options = {
|
|
90
|
+
'retry' => 0,
|
|
91
|
+
'queue' => target,
|
|
92
|
+
'class' => 'Executo::SetScheduleWorker',
|
|
93
|
+
'args' => list
|
|
94
|
+
}
|
|
95
|
+
Sidekiq::Client.new(connection_pool).push(options)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def connection_pool
|
|
99
|
+
@connection_pool ||= ConnectionPool.new(size: 5, timeout: 5) { Redis.new(config.redis) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def active_job_connection_pool
|
|
103
|
+
@active_job_connection_pool ||= ConnectionPool.new(size: 5, timeout: 5) { Redis.new(config.active_job_redis) }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
data/sidekiq.yml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Sample configuration file for Sidekiq.
|
|
2
|
+
# Options here can still be overridden by cmd line args.
|
|
3
|
+
# Place this file at config/sidekiq.yml and Sidekiq will
|
|
4
|
+
# pick it up automatically.
|
|
5
|
+
---
|
|
6
|
+
:verbose: false
|
|
7
|
+
:concurrency: 2
|
|
8
|
+
|
|
9
|
+
# Set timeout to 8 on Heroku, longer if you manage your own systems.
|
|
10
|
+
:timeout: 30
|
|
11
|
+
|
|
12
|
+
# Sidekiq will run this file through ERB when reading it so you can
|
|
13
|
+
# even put in dynamic logic, like a host-specific queue.
|
|
14
|
+
# http://www.mikeperham.com/2013/11/13/advanced-sidekiq-host-specific-queues/
|
|
15
|
+
:queues:
|
|
16
|
+
- localhost
|
data/support/Gemfile
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
|
+
|
|
5
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
|
6
|
+
git_source(:entdec) { |repo_name| "git@code.entropydecelerator.com:#{repo_name}.git" }
|
|
7
|
+
|
|
8
|
+
gem 'executo', entdec: 'components/executo', branch: :master
|
data/support/boot.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'executo'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
Executo.setup do |config|
|
|
8
|
+
config.redis = { url: 'redis://localhost:6379/1' }
|
|
9
|
+
config.callback = lambda do |state, exitstatus, stdout, stderr, context|
|
|
10
|
+
callback_url = context['options'].delete('callback_url')
|
|
11
|
+
break unless callback_url
|
|
12
|
+
|
|
13
|
+
data = {
|
|
14
|
+
state: state,
|
|
15
|
+
command: context['command'],
|
|
16
|
+
params: context['params'],
|
|
17
|
+
options: context['options']
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
data[:exitstatus] = exitstatus.to_i if exitstatus
|
|
21
|
+
data[:stdout] = stdout.force_encoding('utf-8') if stdout
|
|
22
|
+
data[:stderr] = stderr.force_encoding('utf-8') if stderr
|
|
23
|
+
data[:pid] = context['pid'].to_i if context['pid']
|
|
24
|
+
|
|
25
|
+
uri = URI.parse(callback_url)
|
|
26
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
27
|
+
http.use_ssl = uri.scheme == 'https'
|
|
28
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
29
|
+
http.start do
|
|
30
|
+
http.request_post(
|
|
31
|
+
uri.path,
|
|
32
|
+
data.to_json,
|
|
33
|
+
'Content-Type': 'application/json'
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Sidekiq.configure_server do |config|
|
|
40
|
+
config.redis = Executo.config.redis
|
|
41
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
description "Executo"
|
|
2
|
+
|
|
3
|
+
start on runlevel [2345]
|
|
4
|
+
stop on runlevel [06]
|
|
5
|
+
|
|
6
|
+
env MALLOC_ARENA_MAX=2
|
|
7
|
+
|
|
8
|
+
respawn
|
|
9
|
+
respawn limit 5 10
|
|
10
|
+
|
|
11
|
+
normal exit 0 TERM
|
|
12
|
+
reload signal TSTP
|
|
13
|
+
|
|
14
|
+
script
|
|
15
|
+
exec /bin/bash <<'EOT'
|
|
16
|
+
# Logs to /var/log/upstart/executo.log
|
|
17
|
+
cd /data/executo/
|
|
18
|
+
exec bundle exec sidekiq -r /data/executo/boot.rb -C /data/executo/sidekiq.yml
|
|
19
|
+
EOT
|
|
20
|
+
|
|
21
|
+
end script
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=executo
|
|
3
|
+
Wants=redis-server.service
|
|
4
|
+
After=multi-user.target redis-server.service
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
Type=simple
|
|
8
|
+
WorkingDirectory=/data/executo
|
|
9
|
+
ExecStart=/usr/local/bin/bundle exec sidekiq -r /data/executo/boot.rb -C /var/apps/our-project/config/sidekiq.yml
|
|
10
|
+
User=pi
|
|
11
|
+
Group=pi
|
|
12
|
+
|
|
13
|
+
# if we crash, restart
|
|
14
|
+
RestartSec=1
|
|
15
|
+
Restart=always
|
|
16
|
+
|
|
17
|
+
# Logs go to syslog
|
|
18
|
+
# View with "sudo journalctl -u executo"
|
|
19
|
+
StandardOutput=syslog
|
|
20
|
+
StandardError=syslog
|
|
21
|
+
|
|
22
|
+
# This will default to "bundler" if we don't specify it
|
|
23
|
+
SyslogIdentifier=executo
|
|
24
|
+
|
|
25
|
+
[Install]
|
|
26
|
+
WantedBy=multi-user.target
|
data/support/sidekiq.yml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Sample configuration file for Sidekiq.
|
|
2
|
+
# Options here can still be overridden by cmd line args.
|
|
3
|
+
# Place this file at config/sidekiq.yml and Sidekiq will
|
|
4
|
+
# pick it up automatically.
|
|
5
|
+
---
|
|
6
|
+
:verbose: true
|
|
7
|
+
:concurrency: 2
|
|
8
|
+
|
|
9
|
+
# Set timeout to 8 on Heroku, longer if you manage your own systems.
|
|
10
|
+
:timeout: 30
|
|
11
|
+
|
|
12
|
+
# Sidekiq will run this file through ERB when reading it so you can
|
|
13
|
+
# even put in dynamic logic, like a host-specific queue.
|
|
14
|
+
# http://www.mikeperham.com/2013/11/13/advanced-sidekiq-host-specific-queues/
|
|
15
|
+
:queues:
|
|
16
|
+
- g02sj01
|
|
17
|
+
|
data/test.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
require 'executo'
|
|
5
|
+
require 'active_support/core_ext/hash'
|
|
6
|
+
require 'pry'
|
|
7
|
+
|
|
8
|
+
require 'sidekiq'
|
|
9
|
+
require 'active_job'
|
|
10
|
+
|
|
11
|
+
ActiveJob::Base.queue_adapter = :sidekiq
|
|
12
|
+
|
|
13
|
+
Executo.setup do |config|
|
|
14
|
+
config.redis = { url: 'redis://localhost:6379/1' }
|
|
15
|
+
config.active_job_redis = { url: 'redis://localhost:6379/0' }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
Executo.publish('localhost', 'ls', ['-al'], feedback: { service: 'LsProcessService', args: { now: Time.now } })
|
|
19
|
+
|
|
20
|
+
puts 'Done'
|
data/uptime.rb
ADDED