minicron 0.2 → 0.3
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 +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
|