minicron 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -1,6 +1,6 @@
1
1
  # The minicron module
2
2
  module Minicron
3
- VERSION = '0.2'
3
+ VERSION = '0.3'
4
4
  DEFAULT_CONFIG_FILE = '/etc/minicron.toml'
5
5
  BASE_PATH = File.expand_path('../../../', __FILE__)
6
6
  LIB_PATH = File.expand_path('../../', __FILE__)
@@ -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 any quotes in the command
31
- command = escape_command(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 minicron run '#{command}'"
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 \"#{find}\" /etc/crontab.tmp").to_s.strip
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 \"#{replace}\" /etc/crontab.tmp").to_s.strip
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
- echo_line = "echo \"#{line}\" >> /etc/crontab && echo 'y' || echo 'n'"
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 write '#{line}' to the crontab"
103
+ fail Exception, "Unable to echo #{escaped_line} to the crontab"
109
104
  end
110
105
 
111
106
  # Check the line is there
@@ -104,7 +104,7 @@ module Minicron::Hub
104
104
  end
105
105
 
106
106
  def handle_exception(env, e, status)
107
- if Minicron.config['global']['trace']
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['global']['trace']
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 = Minicron.sanitize_filename(File.expand_path("~/.ssh/minicron_host_#{host.id}_rsa"))
113
- public_key_path = Minicron.sanitize_filename(File.expand_path("~/.ssh/minicron_host_#{host.id}_rsa.pub"))
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 '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
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 '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
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 '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'
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 '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
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 '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
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 'hosts', ['fqdn'], name: 'hostname', using: :btree
53
+ add_index "hosts", ["fqdn"], name: "hostname", using: :btree
54
54
 
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
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 'job_execution_outputs', ['execution_id'], name: 'execution_id', using: :btree
63
- add_index 'job_execution_outputs', ['seq'], name: 'seq', using: :btree
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 '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
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 '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
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 '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
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 '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
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