minicron 0.2 → 0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/bin/minicron +17 -2
- data/lib/minicron.rb +30 -32
- data/lib/minicron/alert/pagerduty.rb +1 -1
- data/lib/minicron/cli.rb +19 -208
- data/lib/minicron/cli/commands.rb +194 -0
- data/lib/minicron/constants.rb +1 -1
- data/lib/minicron/cron.rb +10 -15
- data/lib/minicron/hub/app.rb +2 -2
- data/lib/minicron/hub/controllers/api/hosts.rb +2 -3
- data/lib/minicron/hub/db/schema.rb +65 -70
- data/lib/minicron/hub/db/schema.sql +0 -14
- data/lib/minicron/monitor.rb +1 -1
- data/lib/minicron/transport/client.rb +4 -3
- data/lib/minicron/transport/faye/client.rb +1 -1
- data/lib/minicron/transport/faye/server.rb +6 -6
- data/lib/minicron/transport/server.rb +14 -10
- data/spec/minicron/alert/pagerduty_spec.rb +66 -0
- data/spec/minicron/alert/sms_spec.rb +69 -0
- data/spec/minicron/cli_spec.rb +35 -20
- data/spec/minicron/transport/client_spec.rb +70 -1
- data/spec/minicron/transport/faye/client_spec.rb +41 -27
- data/spec/minicron/transport/server_spec.rb +7 -11
- data/spec/minicron_spec.rb +45 -5
- data/spec/spec_helper.rb +1 -0
- data/spec/valid_config.toml +0 -1
- metadata +110 -92
- data/lib/minicron/hub/assets/js/auth/ember-auth-9.0.7.min.js +0 -2
- data/lib/minicron/hub/assets/js/auth/ember-auth-request-jquery-1.0.3.min.js +0 -1
@@ -0,0 +1,194 @@
|
|
1
|
+
require 'insidious'
|
2
|
+
require 'minicron'
|
3
|
+
require 'minicron/transport/client'
|
4
|
+
|
5
|
+
module Minicron
|
6
|
+
module CLI
|
7
|
+
class Commands
|
8
|
+
# Add the `minicron db` command
|
9
|
+
def self.add_db_cli_command(cli)
|
10
|
+
cli.command :db do |c|
|
11
|
+
c.syntax = 'minicron db [setup]'
|
12
|
+
c.description = 'Sets up the minicron database schema.'
|
13
|
+
|
14
|
+
c.action do |args, opts|
|
15
|
+
# Check that exactly one argument has been passed
|
16
|
+
if args.length != 1
|
17
|
+
fail ArgumentError, 'A valid command to run is required! See `minicron help db`'
|
18
|
+
end
|
19
|
+
|
20
|
+
# Parse the file and cli config options
|
21
|
+
Minicron::CLI.parse_config(opts)
|
22
|
+
|
23
|
+
# These are inlined as we only need them in this use case
|
24
|
+
require 'rake'
|
25
|
+
require 'minicron/hub/app'
|
26
|
+
require 'sinatra/activerecord/rake'
|
27
|
+
|
28
|
+
# Setup the db
|
29
|
+
Minicron::Hub::App.setup_db
|
30
|
+
|
31
|
+
# Tell activerecord where the db folder is, it assumes it is in db/
|
32
|
+
Sinatra::ActiveRecordTasks.db_dir = Minicron::HUB_PATH + '/db'
|
33
|
+
|
34
|
+
# Adjust the task name
|
35
|
+
task = args.first == 'setup' ? 'load' : args.first
|
36
|
+
|
37
|
+
# Run the task
|
38
|
+
Rake.application['db:schema:' + task].invoke
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Add the `minicron server` command
|
44
|
+
def self.add_server_cli_command(cli)
|
45
|
+
cli.command :server do |c|
|
46
|
+
c.syntax = 'minicron server [start|stop|status]'
|
47
|
+
c.description = 'Controls the minicron server.'
|
48
|
+
c.option '--host STRING', String, "The host for the server to listen on. Default: #{Minicron.config['server']['host']}"
|
49
|
+
c.option '--port STRING', Integer, "How port for the server to listed on. Default: #{Minicron.config['server']['port']}"
|
50
|
+
c.option '--path STRING', String, "The path on the host. Default: #{Minicron.config['server']['path']}"
|
51
|
+
c.option '--debug', "Enable debug mode. Default: #{Minicron.config['server']['debug']}"
|
52
|
+
|
53
|
+
c.action do |args, opts|
|
54
|
+
# Parse the file and cli config options
|
55
|
+
Minicron::CLI.parse_config(opts)
|
56
|
+
|
57
|
+
# If we get no arguments then default the action to start
|
58
|
+
action = args.first.nil? ? 'start' : args.first
|
59
|
+
|
60
|
+
# Get an instance of insidious and set the pid file
|
61
|
+
insidious = Insidious.new(
|
62
|
+
:pid_file => '/tmp/minicron.pid',
|
63
|
+
:daemonize => Minicron.config['server']['debug'] == false
|
64
|
+
)
|
65
|
+
|
66
|
+
case action
|
67
|
+
when 'start'
|
68
|
+
insidious.start! do
|
69
|
+
# Run the execution monitor (this runs in a separate thread)
|
70
|
+
monitor = Minicron::Monitor.new
|
71
|
+
monitor.start!
|
72
|
+
|
73
|
+
# Start the server!
|
74
|
+
Minicron::Transport::Server.start!(
|
75
|
+
Minicron.config['server']['host'],
|
76
|
+
Minicron.config['server']['port'],
|
77
|
+
Minicron.config['server']['path']
|
78
|
+
)
|
79
|
+
end
|
80
|
+
when 'stop'
|
81
|
+
insidious.stop!
|
82
|
+
when 'status'
|
83
|
+
if insidious.running?
|
84
|
+
puts 'minicron is running'
|
85
|
+
else
|
86
|
+
puts 'minicron is not running'
|
87
|
+
end
|
88
|
+
else
|
89
|
+
fail ArgumentError, 'Invalid action, expected [start|stop|status]. See `minicron help server`'
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Add the `minicron run [command]` command
|
96
|
+
# @yieldparam output [String] output from the cli
|
97
|
+
def self.add_run_cli_command(cli)
|
98
|
+
# Add the run command to the cli
|
99
|
+
cli.command :run do |c|
|
100
|
+
c.syntax = "minicron run 'command -option value'"
|
101
|
+
c.description = 'Runs the command passed as an argument.'
|
102
|
+
c.option '--mode STRING', String, "How to capture the command output, each 'line' or each 'char'? Default: #{Minicron.config['cli']['mode']}"
|
103
|
+
c.option '--dry-run', "Run the command without sending the output to the server. Default: #{Minicron.config['cli']['dry_run']}"
|
104
|
+
|
105
|
+
c.action do |args, opts|
|
106
|
+
# Check that exactly one argument has been passed
|
107
|
+
if args.length != 1
|
108
|
+
fail ArgumentError, 'A valid command to run is required! See `minicron help run`'
|
109
|
+
end
|
110
|
+
|
111
|
+
# Parse the file and cli config options
|
112
|
+
Minicron::CLI.parse_config(opts)
|
113
|
+
|
114
|
+
begin
|
115
|
+
# Set up the job and get the job and execution ids
|
116
|
+
unless Minicron.config['cli']['dry_run']
|
117
|
+
# Get a faye instance so we can send data about the job
|
118
|
+
faye = Minicron::Transport::Client.new(
|
119
|
+
Minicron.config['client']['scheme'],
|
120
|
+
Minicron.config['client']['host'],
|
121
|
+
Minicron.config['client']['port'],
|
122
|
+
Minicron.config['client']['path']
|
123
|
+
)
|
124
|
+
|
125
|
+
# Set up the job and get the jexecution and job ids back from the server
|
126
|
+
ids = setup_job(args.first, faye)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Execute the command and yield the output
|
130
|
+
Minicron::CLI.run_command(args.first, :mode => Minicron.config['cli']['mode'], :verbose => Minicron.config['verbose']) do |output|
|
131
|
+
# We need to handle the yielded output differently based on it's type
|
132
|
+
case output[:type]
|
133
|
+
when :status
|
134
|
+
unless Minicron.config['cli']['dry_run']
|
135
|
+
faye.send(:job_id => ids[:job_id], :execution_id => ids[:execution_id], :type => :status, :message => output[:output])
|
136
|
+
end
|
137
|
+
when :command
|
138
|
+
unless Minicron.config['cli']['dry_run']
|
139
|
+
faye.send(:job_id => ids[:job_id], :execution_id => ids[:execution_id], :type => :output, :message => output[:output])
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
yield output[:output] unless output[:type] == :status
|
144
|
+
end
|
145
|
+
rescue Exception => e
|
146
|
+
# Send the exception message to the server and yield it
|
147
|
+
unless Minicron.config['cli']['dry_run']
|
148
|
+
faye.send(:job_id => ids[:job_id], :execution_id => ids[:execution_id], :type => :output, :message => e.message)
|
149
|
+
end
|
150
|
+
|
151
|
+
raise Exception, e
|
152
|
+
ensure
|
153
|
+
# Ensure that all messages are delivered and that we
|
154
|
+
unless Minicron.config['cli']['dry_run']
|
155
|
+
faye.ensure_delivery
|
156
|
+
faye.tidy_up
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
# Setup a job by sending the SETUP command to the server
|
166
|
+
#
|
167
|
+
# @param command [String] the job command
|
168
|
+
# @param faye a faye client instance
|
169
|
+
# @return [Hash] the job_id and execution_id
|
170
|
+
def self.setup_job(command, faye)
|
171
|
+
# Get the fully qualified domain name of the currnet host
|
172
|
+
fqdn = Minicron.get_fqdn
|
173
|
+
|
174
|
+
# Get the short hostname of the current host
|
175
|
+
hostname = Minicron.get_hostname
|
176
|
+
|
177
|
+
# Get the md5 hash for the job
|
178
|
+
job_hash = Minicron::Transport.get_job_hash(command, fqdn)
|
179
|
+
|
180
|
+
# Fire up eventmachine
|
181
|
+
faye.ensure_em_running
|
182
|
+
|
183
|
+
# Setup the job on the server
|
184
|
+
ids = faye.setup(job_hash, command, fqdn, hostname)
|
185
|
+
|
186
|
+
# Wait until we get the execution id
|
187
|
+
faye.ensure_delivery
|
188
|
+
|
189
|
+
# Return the ids
|
190
|
+
ids
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
data/lib/minicron/constants.rb
CHANGED
data/lib/minicron/cron.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'shellwords'
|
2
|
+
require 'escape'
|
2
3
|
|
3
4
|
module Minicron
|
4
5
|
# Used to interact with the crontab on hosts over an ssh connection
|
@@ -13,24 +14,17 @@ module Minicron
|
|
13
14
|
@ssh = ssh
|
14
15
|
end
|
15
16
|
|
16
|
-
# Escape the quotes in a command
|
17
|
-
#
|
18
|
-
# @param command [String]
|
19
|
-
# @return [String]
|
20
|
-
def escape_command(command)
|
21
|
-
command.gsub(/\\|'/) { |c| "\\#{c}" }
|
22
|
-
end
|
23
|
-
|
24
17
|
# Build the minicron command to be used in the crontab
|
25
18
|
#
|
26
19
|
# @param command [String]
|
27
20
|
# @param schedule [String]
|
28
21
|
# @return [String]
|
29
22
|
def build_minicron_command(command, schedule)
|
30
|
-
# Escape
|
31
|
-
command =
|
23
|
+
# Escape the command so it will work in bourne shells
|
24
|
+
command = Escape.shell_command(['minicron', 'run', command])
|
25
|
+
cron_command =Escape.shell_command(['/bin/bash', '-l', '-c', command])
|
32
26
|
|
33
|
-
"#{schedule} root
|
27
|
+
"#{schedule} root #{cron_command}"
|
34
28
|
end
|
35
29
|
|
36
30
|
# Used to find a string and replace it with another in the crontab by
|
@@ -63,7 +57,7 @@ module Minicron
|
|
63
57
|
# If it's a delete
|
64
58
|
if replace == ''
|
65
59
|
# Check the original line is no longer there
|
66
|
-
grep = conn.exec!("grep -F
|
60
|
+
grep = conn.exec!("grep -F #{find.shellescape} /etc/crontab.tmp").to_s.strip
|
67
61
|
|
68
62
|
# Throw an exception if we can't see our new line at the end of the file
|
69
63
|
if grep != replace
|
@@ -71,7 +65,7 @@ module Minicron
|
|
71
65
|
end
|
72
66
|
else
|
73
67
|
# Check the updated line is there
|
74
|
-
grep = conn.exec!("grep -F
|
68
|
+
grep = conn.exec!("grep -F #{replace.shellescape} /etc/crontab.tmp").to_s.strip
|
75
69
|
|
76
70
|
# Throw an exception if we can't see our new line at the end of the file
|
77
71
|
if grep != replace
|
@@ -98,14 +92,15 @@ module Minicron
|
|
98
92
|
|
99
93
|
# Prepare the line we are going to write to the crontab
|
100
94
|
line = build_minicron_command(job.command, schedule)
|
101
|
-
|
95
|
+
escaped_line = line.shellescape
|
96
|
+
echo_line = "echo #{escaped_line} >> /etc/crontab && echo 'y' || echo 'n'"
|
102
97
|
|
103
98
|
# Append it to the end of the crontab
|
104
99
|
write = conn.exec!(echo_line).strip
|
105
100
|
|
106
101
|
# Throw an exception if it failed
|
107
102
|
if write != 'y'
|
108
|
-
fail Exception, "Unable to
|
103
|
+
fail Exception, "Unable to echo #{escaped_line} to the crontab"
|
109
104
|
end
|
110
105
|
|
111
106
|
# Check the line is there
|
data/lib/minicron/hub/app.rb
CHANGED
@@ -104,7 +104,7 @@ module Minicron::Hub
|
|
104
104
|
end
|
105
105
|
|
106
106
|
def handle_exception(env, e, status)
|
107
|
-
if Minicron.config['
|
107
|
+
if Minicron.config['trace']
|
108
108
|
env['rack.errors'].puts(e)
|
109
109
|
env['rack.errors'].puts(e.backtrace.join("\n"))
|
110
110
|
env['rack.errors'].flush
|
@@ -114,7 +114,7 @@ module Minicron::Hub
|
|
114
114
|
hash = { :error => e.to_s }
|
115
115
|
|
116
116
|
# Display the full trace if tracing is enabled
|
117
|
-
hash[:trace] = e.backtrace if Minicron.config['
|
117
|
+
hash[:trace] = e.backtrace if Minicron.config['trace']
|
118
118
|
|
119
119
|
[status, { 'Content-Type' => 'application/json' }, [hash.to_json]]
|
120
120
|
end
|
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'minicron'
|
2
1
|
require 'minicron/transport/ssh'
|
3
2
|
|
4
3
|
class Minicron::Hub::App
|
@@ -109,8 +108,8 @@ class Minicron::Hub::App
|
|
109
108
|
ssh.close
|
110
109
|
|
111
110
|
# Delete the pub/priv key pair
|
112
|
-
private_key_path =
|
113
|
-
public_key_path =
|
111
|
+
private_key_path = File.expand_path("~/.ssh/minicron_host_#{host.id}_rsa")
|
112
|
+
public_key_path = File.expand_path("~/.ssh/minicron_host_#{host.id}_rsa.pub")
|
114
113
|
File.delete(private_key_path)
|
115
114
|
File.delete(public_key_path)
|
116
115
|
|
@@ -12,92 +12,87 @@
|
|
12
12
|
|
13
13
|
ActiveRecord::Schema.define(version: 0) do
|
14
14
|
|
15
|
-
create_table
|
16
|
-
t.integer
|
17
|
-
t.integer
|
18
|
-
t.string
|
19
|
-
t.datetime
|
20
|
-
t.string
|
21
|
-
t.datetime
|
15
|
+
create_table "alerts", force: true do |t|
|
16
|
+
t.integer "schedule_id"
|
17
|
+
t.integer "execution_id"
|
18
|
+
t.string "kind", limit: 4, default: "", null: false
|
19
|
+
t.datetime "expected_at"
|
20
|
+
t.string "medium", limit: 9, default: "", null: false
|
21
|
+
t.datetime "sent_at", null: false
|
22
22
|
end
|
23
23
|
|
24
|
-
add_index
|
25
|
-
add_index
|
26
|
-
add_index
|
27
|
-
add_index
|
28
|
-
add_index
|
24
|
+
add_index "alerts", ["execution_id"], name: "execution_id", using: :btree
|
25
|
+
add_index "alerts", ["expected_at"], name: "expected_at", using: :btree
|
26
|
+
add_index "alerts", ["kind"], name: "kind", using: :btree
|
27
|
+
add_index "alerts", ["medium"], name: "medium", using: :btree
|
28
|
+
add_index "alerts", ["schedule_id"], name: "schedule_id", using: :btree
|
29
29
|
|
30
|
-
create_table
|
31
|
-
t.integer
|
32
|
-
t.datetime
|
33
|
-
t.datetime
|
34
|
-
t.datetime
|
35
|
-
t.integer
|
30
|
+
create_table "executions", force: true do |t|
|
31
|
+
t.integer "job_id", null: false
|
32
|
+
t.datetime "created_at", null: false
|
33
|
+
t.datetime "started_at"
|
34
|
+
t.datetime "finished_at"
|
35
|
+
t.integer "exit_status"
|
36
36
|
end
|
37
37
|
|
38
|
-
add_index
|
39
|
-
add_index
|
40
|
-
add_index
|
41
|
-
add_index
|
38
|
+
add_index "executions", ["created_at"], name: "created_at", using: :btree
|
39
|
+
add_index "executions", ["finished_at"], name: "finished_at", using: :btree
|
40
|
+
add_index "executions", ["job_id"], name: "job_id", using: :btree
|
41
|
+
add_index "executions", ["started_at"], name: "started_at", using: :btree
|
42
42
|
|
43
|
-
create_table
|
44
|
-
t.string
|
45
|
-
t.string
|
46
|
-
t.string
|
47
|
-
t.integer
|
48
|
-
t.text
|
49
|
-
t.datetime
|
50
|
-
t.datetime
|
43
|
+
create_table "hosts", force: true do |t|
|
44
|
+
t.string "name"
|
45
|
+
t.string "fqdn", default: "", null: false
|
46
|
+
t.string "host", default: "", null: false
|
47
|
+
t.integer "port", null: false
|
48
|
+
t.text "public_key"
|
49
|
+
t.datetime "created_at", null: false
|
50
|
+
t.datetime "updated_at", null: false
|
51
51
|
end
|
52
52
|
|
53
|
-
add_index
|
53
|
+
add_index "hosts", ["fqdn"], name: "hostname", using: :btree
|
54
54
|
|
55
|
-
create_table
|
56
|
-
t.integer
|
57
|
-
t.integer
|
58
|
-
t.text
|
59
|
-
t.datetime
|
55
|
+
create_table "job_execution_outputs", force: true do |t|
|
56
|
+
t.integer "execution_id", null: false
|
57
|
+
t.integer "seq", null: false
|
58
|
+
t.text "output", null: false
|
59
|
+
t.datetime "timestamp", null: false
|
60
60
|
end
|
61
61
|
|
62
|
-
add_index
|
63
|
-
add_index
|
62
|
+
add_index "job_execution_outputs", ["execution_id"], name: "execution_id", using: :btree
|
63
|
+
add_index "job_execution_outputs", ["seq"], name: "seq", using: :btree
|
64
64
|
|
65
|
-
create_table
|
66
|
-
t.string
|
67
|
-
t.string
|
68
|
-
t.text
|
69
|
-
t.integer
|
70
|
-
t.datetime
|
71
|
-
t.datetime
|
65
|
+
create_table "jobs", force: true do |t|
|
66
|
+
t.string "job_hash", limit: 32, default: "", null: false
|
67
|
+
t.string "name"
|
68
|
+
t.text "command", null: false
|
69
|
+
t.integer "host_id", null: false
|
70
|
+
t.datetime "created_at", null: false
|
71
|
+
t.datetime "updated_at", null: false
|
72
72
|
end
|
73
73
|
|
74
|
-
add_index
|
75
|
-
add_index
|
76
|
-
add_index
|
74
|
+
add_index "jobs", ["created_at"], name: "created_at", using: :btree
|
75
|
+
add_index "jobs", ["host_id"], name: "host_id", using: :btree
|
76
|
+
add_index "jobs", ["job_hash"], name: "job_hash", unique: true, using: :btree
|
77
77
|
|
78
|
-
create_table
|
79
|
-
t.integer
|
80
|
-
t.string
|
81
|
-
t.string
|
82
|
-
t.string
|
83
|
-
t.string
|
84
|
-
t.string
|
85
|
-
t.string
|
86
|
-
t.datetime
|
87
|
-
t.datetime
|
78
|
+
create_table "schedules", force: true do |t|
|
79
|
+
t.integer "job_id", null: false
|
80
|
+
t.string "minute", limit: 179
|
81
|
+
t.string "hour", limit: 71
|
82
|
+
t.string "day_of_the_month", limit: 92
|
83
|
+
t.string "month", limit: 25
|
84
|
+
t.string "day_of_the_week", limit: 20
|
85
|
+
t.string "special", limit: 9
|
86
|
+
t.datetime "created_at", null: false
|
87
|
+
t.datetime "updated_at", null: false
|
88
88
|
end
|
89
89
|
|
90
|
-
add_index
|
91
|
-
add_index
|
92
|
-
add_index
|
93
|
-
add_index
|
94
|
-
add_index
|
95
|
-
add_index
|
96
|
-
add_index
|
97
|
-
|
98
|
-
create_table 'users', force: true do |t|
|
99
|
-
t.integer 'email', null: false
|
100
|
-
t.integer 'password', null: false
|
101
|
-
end
|
90
|
+
add_index "schedules", ["day_of_the_month"], name: "day_of_the_month", using: :btree
|
91
|
+
add_index "schedules", ["day_of_the_week"], name: "day_of_the_week", using: :btree
|
92
|
+
add_index "schedules", ["hour"], name: "hour", using: :btree
|
93
|
+
add_index "schedules", ["job_id"], name: "job_id", using: :btree
|
94
|
+
add_index "schedules", ["minute"], name: "minute", using: :btree
|
95
|
+
add_index "schedules", ["month"], name: "month", using: :btree
|
96
|
+
add_index "schedules", ["special"], name: "special", using: :btree
|
102
97
|
|
103
98
|
end
|