minicron 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +674 -0
  3. data/README.md +187 -0
  4. data/Rakefile +17 -0
  5. data/bin/minicron +26 -0
  6. data/lib/minicron.rb +179 -0
  7. data/lib/minicron/alert.rb +115 -0
  8. data/lib/minicron/alert/email.rb +50 -0
  9. data/lib/minicron/alert/pagerduty.rb +39 -0
  10. data/lib/minicron/alert/sms.rb +47 -0
  11. data/lib/minicron/cli.rb +367 -0
  12. data/lib/minicron/constants.rb +7 -0
  13. data/lib/minicron/cron.rb +192 -0
  14. data/lib/minicron/hub/app.rb +132 -0
  15. data/lib/minicron/hub/assets/app/application.js +151 -0
  16. data/lib/minicron/hub/assets/app/components/schedules.js +280 -0
  17. data/lib/minicron/hub/assets/app/controllers/executions.js +35 -0
  18. data/lib/minicron/hub/assets/app/controllers/hosts.js +129 -0
  19. data/lib/minicron/hub/assets/app/controllers/jobs.js +109 -0
  20. data/lib/minicron/hub/assets/app/controllers/schedules.js +80 -0
  21. data/lib/minicron/hub/assets/app/helpers.js +22 -0
  22. data/lib/minicron/hub/assets/app/models/execution.js +13 -0
  23. data/lib/minicron/hub/assets/app/models/host.js +15 -0
  24. data/lib/minicron/hub/assets/app/models/job.js +15 -0
  25. data/lib/minicron/hub/assets/app/models/job_execution_output.js +11 -0
  26. data/lib/minicron/hub/assets/app/models/schedule.js +32 -0
  27. data/lib/minicron/hub/assets/app/router.js +31 -0
  28. data/lib/minicron/hub/assets/app/routes/executions.js +36 -0
  29. data/lib/minicron/hub/assets/app/routes/hosts.js +42 -0
  30. data/lib/minicron/hub/assets/app/routes/index.js +9 -0
  31. data/lib/minicron/hub/assets/app/routes/jobs.js +52 -0
  32. data/lib/minicron/hub/assets/app/routes/schedules.js +37 -0
  33. data/lib/minicron/hub/assets/css/bootswatch.min.css +9 -0
  34. data/lib/minicron/hub/assets/css/main.scss +323 -0
  35. data/lib/minicron/hub/assets/fonts/glyphicons-halflings-regular.eot +0 -0
  36. data/lib/minicron/hub/assets/fonts/glyphicons-halflings-regular.svg +229 -0
  37. data/lib/minicron/hub/assets/fonts/glyphicons-halflings-regular.ttf +0 -0
  38. data/lib/minicron/hub/assets/fonts/glyphicons-halflings-regular.woff +0 -0
  39. data/lib/minicron/hub/assets/fonts/lato-bold-700.woff +0 -0
  40. data/lib/minicron/hub/assets/fonts/lato-italic-400.woff +0 -0
  41. data/lib/minicron/hub/assets/fonts/lato-regular-400.woff +0 -0
  42. data/lib/minicron/hub/assets/js/ansi_up-1.1.1.min.js +6 -0
  43. data/lib/minicron/hub/assets/js/auth/ember-auth-9.0.7.min.js +2 -0
  44. data/lib/minicron/hub/assets/js/auth/ember-auth-request-jquery-1.0.3.min.js +1 -0
  45. data/lib/minicron/hub/assets/js/bootstrap-3.1.1.min.js +6 -0
  46. data/lib/minicron/hub/assets/js/ember-1.4.1.min.js +18 -0
  47. data/lib/minicron/hub/assets/js/ember-data-1.0.0-beta.7.f87cba88.min.js +10 -0
  48. data/lib/minicron/hub/assets/js/faye-browser-1.0.1.min.js +2 -0
  49. data/lib/minicron/hub/assets/js/handlebars-1.3.0.min.js +29 -0
  50. data/lib/minicron/hub/assets/js/jquery-2.1.0.min.js +4 -0
  51. data/lib/minicron/hub/assets/js/moment-2.5.1.min.js +7 -0
  52. data/lib/minicron/hub/controllers/api/executions.rb +34 -0
  53. data/lib/minicron/hub/controllers/api/hosts.rb +150 -0
  54. data/lib/minicron/hub/controllers/api/job_execution_outputs.rb +30 -0
  55. data/lib/minicron/hub/controllers/api/jobs.rb +118 -0
  56. data/lib/minicron/hub/controllers/api/schedule.rb +184 -0
  57. data/lib/minicron/hub/controllers/index.rb +5 -0
  58. data/lib/minicron/hub/db/schema.rb +98 -0
  59. data/lib/minicron/hub/db/schema.sql +158 -0
  60. data/lib/minicron/hub/models/alert.rb +7 -0
  61. data/lib/minicron/hub/models/execution.rb +8 -0
  62. data/lib/minicron/hub/models/host.rb +7 -0
  63. data/lib/minicron/hub/models/job.rb +18 -0
  64. data/lib/minicron/hub/models/job_execution_output.rb +7 -0
  65. data/lib/minicron/hub/models/schedule.rb +25 -0
  66. data/lib/minicron/hub/serializers/execution.rb +75 -0
  67. data/lib/minicron/hub/serializers/host.rb +57 -0
  68. data/lib/minicron/hub/serializers/job.rb +104 -0
  69. data/lib/minicron/hub/serializers/job_execution_output.rb +48 -0
  70. data/lib/minicron/hub/serializers/schedule.rb +68 -0
  71. data/lib/minicron/hub/views/handlebars/application.erb +51 -0
  72. data/lib/minicron/hub/views/handlebars/errors.erb +29 -0
  73. data/lib/minicron/hub/views/handlebars/executions.erb +79 -0
  74. data/lib/minicron/hub/views/handlebars/hosts.erb +205 -0
  75. data/lib/minicron/hub/views/handlebars/jobs.erb +203 -0
  76. data/lib/minicron/hub/views/handlebars/loading.erb +3 -0
  77. data/lib/minicron/hub/views/handlebars/schedules.erb +354 -0
  78. data/lib/minicron/hub/views/index.erb +7 -0
  79. data/lib/minicron/hub/views/layouts/app.erb +15 -0
  80. data/lib/minicron/monitor.rb +116 -0
  81. data/lib/minicron/transport.rb +15 -0
  82. data/lib/minicron/transport/client.rb +80 -0
  83. data/lib/minicron/transport/faye/client.rb +103 -0
  84. data/lib/minicron/transport/faye/extensions/job_handler.rb +184 -0
  85. data/lib/minicron/transport/faye/server.rb +58 -0
  86. data/lib/minicron/transport/server.rb +62 -0
  87. data/lib/minicron/transport/ssh.rb +51 -0
  88. data/spec/invalid_config.toml +2 -0
  89. data/spec/minicron/cli_spec.rb +154 -0
  90. data/spec/minicron/transport/client_spec.rb +8 -0
  91. data/spec/minicron/transport/faye/client_spec.rb +53 -0
  92. data/spec/minicron/transport/server_spec.rb +70 -0
  93. data/spec/minicron/transport_spec.rb +13 -0
  94. data/spec/minicron_spec.rb +133 -0
  95. data/spec/spec_helper.rb +33 -0
  96. data/spec/valid_config.toml +48 -0
  97. metadata +577 -0
@@ -0,0 +1,15 @@
1
+ require 'digest/md5'
2
+ require 'json'
3
+
4
+ module Minicron
5
+ # The transport module deals with interactions between the server and client
6
+ module Transport
7
+ # Calculate the job hash based on the command and host
8
+ #
9
+ # @param command [String] the job command e.g 'ls -la'
10
+ # @param fqdn [String] the fqdn of the server running the job e.g `db1.example.com`
11
+ def self.get_job_hash(command, fqdn)
12
+ Digest::MD5.hexdigest(command + fqdn)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,80 @@
1
+ require 'minicron/transport/faye/client'
2
+
3
+ module Minicron
4
+ module Transport
5
+ class Client < Minicron::Transport::FayeClient
6
+ # Instantiate a new instance of the client
7
+ #
8
+ # @param host [String] The host to be communicated with
9
+ def initialize(scheme, host, port, path)
10
+ @scheme = scheme
11
+ @host = host
12
+ @path = path == '/' ? '/faye' : "#{path}/faye"
13
+ @port = port
14
+ @seq = 1
15
+ super(@scheme, @host, @port, @path)
16
+ end
17
+
18
+ # Used to set up a job on the server
19
+ #
20
+ # @param job_hash [String]
21
+ # @param command [Integer]
22
+ # @param fqdn [String]
23
+ # @param hostname [String]
24
+ # @return [Hash]
25
+ def setup(job_hash, command, fqdn, hostname)
26
+ # Send a request to set up the job
27
+ publish("/job/#{job_hash}/status", {
28
+ :action => 'SETUP',
29
+ :command => command,
30
+ :fqdn => fqdn,
31
+ :hostname => hostname
32
+ })
33
+
34
+ # Wait for the response..
35
+ ensure_delivery
36
+
37
+ # TODO: Handle errors here!
38
+ # Get the job and execution id from the response
39
+ ids = JSON.parse(responses.first[:body]).first['channel'].split('/')[3]
40
+
41
+ # Split them up
42
+ ids = ids.split('-')
43
+
44
+ # Return them as a hash
45
+ {
46
+ :job_id => ids[0],
47
+ :execution_id => ids[1]
48
+ }
49
+ end
50
+
51
+ # Helper that wraps the publish function making it quicker to use
52
+ #
53
+ # @option options [String] job_id
54
+ # @option options [String, Symbol] type status or output
55
+ # @option options [Integer] execution_id
56
+ def send(options = {})
57
+ # Publish the message to the correct channel
58
+ publish("/job/#{options[:job_id]}/#{options[:execution_id]}/#{options[:type]}", options[:message])
59
+ end
60
+
61
+ # Publishes a message on the given channel to the server
62
+ #
63
+ # @param channel [String]
64
+ # @param message [String]
65
+ def publish(channel, message)
66
+ # Set up the data to send to faye
67
+ data = {:channel => channel, :data => {
68
+ :ts => Time.now.utc.strftime("%Y-%m-%d %H:%M:%S"),
69
+ :message => message,
70
+ :seq => @seq
71
+ }}
72
+
73
+ # Increment the sequence id
74
+ @seq += 1
75
+
76
+ request({ :message => data.to_json })
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,103 @@
1
+ require 'eventmachine'
2
+ require 'em-http-request'
3
+ require 'digest/md5'
4
+
5
+ module Minicron
6
+ module Transport
7
+ class FayeClient
8
+ attr_accessor :url
9
+ attr_accessor :queue
10
+ attr_accessor :responses
11
+
12
+ # Instantiate a new instance of the client
13
+ #
14
+ # @param scheme [String] http or https
15
+ # @param host [String] The host to be communicated with
16
+ # @param port [String] The port number the server runs on
17
+ # @param path [String] The path to the server, optional
18
+ def initialize(scheme, host, port, path) # TODO: Add options hash for other options
19
+ @scheme = scheme
20
+ @host = host
21
+ @path = path
22
+ @port = port
23
+ @url = "#{scheme}://#{host}:#{port}#{path}"
24
+ @queue = {}
25
+ @responses = []
26
+ @retries = 3
27
+ @retry_counts = {}
28
+ end
29
+
30
+ # Starts EventMachine in a new thread if it isn't already running
31
+ def ensure_em_running
32
+ Thread.new { EM.run } unless EM.reactor_running?
33
+ sleep 0.1 until EM.reactor_running?
34
+ end
35
+
36
+ # Sends a request to the @url and adds it to the request queue
37
+ #
38
+ # @param body [String]
39
+ def request(body)
40
+ # Make sure eventmachine is running
41
+ ensure_em_running
42
+
43
+ # Make the request
44
+ req = EventMachine::HttpRequest.new(
45
+ @url,
46
+ :connect_timeout => Minicron.config['client']['connect_timeout'],
47
+ :inactivity_timeout => Minicron.config['client']['inactivity_timeout']
48
+ ).post(:body => body)
49
+
50
+ # Generate an id for the request
51
+ req_id = Digest::MD5.hexdigest(body.to_s)
52
+
53
+ # Put the request in the queue
54
+ queue[req_id] = req
55
+
56
+ # Set up the retry count for this request if it didn't already exist
57
+ @retry_counts[req_id] ||= 0
58
+
59
+ # Did the request succeed? If so remove it from the queue
60
+ req.callback do
61
+ @responses.push({
62
+ :status => req.response_header.status,
63
+ :header => req.response_header,
64
+ :body => req.response
65
+ })
66
+
67
+ queue.delete(req_id)
68
+ end
69
+
70
+ # If not retry the request up to @retries times
71
+ req.errback do |error|
72
+ @responses.push({
73
+ :status => req.response_header.status,
74
+ :header => req.response_header,
75
+ :body => req.response
76
+ })
77
+
78
+ if @retry_counts[req_id] < @retries
79
+ @retry_counts[req_id] += 1
80
+ request(body)
81
+ end
82
+ end
83
+ end
84
+
85
+ # Blocks until all messages in the sending queue have completed
86
+ def ensure_delivery
87
+ # Keep waiting until the queue is empty but only if we need to
88
+ if queue.length > 0
89
+ until queue.length == 0
90
+ sleep 0.05
91
+ end
92
+ end
93
+
94
+ true
95
+ end
96
+
97
+ # Tidy up after we are done with the client
98
+ def tidy_up
99
+ EM.stop
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,184 @@
1
+ require 'minicron/alert'
2
+ require 'minicron/hub/models/host'
3
+ require 'minicron/hub/models/job'
4
+ require 'minicron/hub/models/execution'
5
+ require 'minicron/hub/models/job_execution_output'
6
+
7
+ module Minicron
8
+ module Transport
9
+ # An extension to the Faye server to store some of the data it receives
10
+ #
11
+ # TODO: A lot of this need more validation checks and error handling
12
+ # currently it's just assumed the correct data is passed and the server
13
+ # can crash if it isn't
14
+ class FayeJobHandler
15
+ # Called by Faye when a message is recieved
16
+ #
17
+ # @param message [Hash] The message data
18
+ # @param request the rack request object
19
+ # @param callback
20
+ def incoming(message, request, callback)
21
+ segments = message['channel'].split('/')
22
+
23
+ # Is it a job messages
24
+ if segments[1] == 'job'
25
+ data = message['data']['message']
26
+
27
+ # Is it a setup message?
28
+ if segments[3] == 'status' && data['action'] == 'SETUP'
29
+ message = handle_setup(request, message, segments)
30
+ end
31
+
32
+ # Is it a start message?
33
+ if segments[4] == 'status' && data[0..4] == 'START'
34
+ handle_start(request, message, segments)
35
+ end
36
+
37
+ # Is it job output?
38
+ if segments[4] == 'output'
39
+ message = handle_output(request, message, segments)
40
+ end
41
+
42
+ # Is it a finish message?
43
+ if segments[4] == 'status' && data[0..5] == 'FINISH'
44
+ handle_finish(request, message, segments)
45
+ end
46
+
47
+ # Is it an exit message?
48
+ if segments[4] == 'status' && data[0..3] == 'EXIT'
49
+ handle_exit(request, message, segments)
50
+ end
51
+ end
52
+
53
+ # Return the message back to faye
54
+ callback.call(message)
55
+ end
56
+
57
+ # Handle SETUP messages
58
+ #
59
+ # @param request the rack request object
60
+ # @param message [Hash] the decoded message sent with the request
61
+ # @param segments [Hash] the message channel split by /
62
+ def handle_setup(request, message, segments)
63
+ data = message['data']['message']
64
+
65
+ # Try and find the host
66
+ host = Minicron::Hub::Host.where(:fqdn => data['fqdn']).first
67
+
68
+ # Create it if it didn't exist!
69
+ if not host
70
+ host = Minicron::Hub::Host.create(
71
+ :name => data['hostname'],
72
+ :fqdn => data['fqdn'],
73
+ :host => request.ip
74
+ )
75
+
76
+ # Generate a new SSH key - TODO: add passphrase
77
+ key = Minicron.generate_ssh_key('host', host.id, host.fqdn)
78
+
79
+ # And finally we store the public key in te db with the host for convenience
80
+ Minicron::Hub::Host.where(:id => host.id).update_all(
81
+ :public_key => key.ssh_public_key
82
+ )
83
+ end
84
+
85
+ # Validate or create the job
86
+ job = Minicron::Hub::Job.where(:job_hash => segments[2]).first_or_create do |j|
87
+ j.job_hash = segments[2]
88
+ j.command = data['command']
89
+ j.host_id = host.id
90
+ end
91
+
92
+ # Create an execution for this job
93
+ execution = Minicron::Hub::Execution.create(
94
+ :job_id => job.id
95
+ )
96
+
97
+ # Alter the response channel to include the execution id for the
98
+ # client to use in later requests
99
+ segments[3] = "#{job.id}-#{execution.id}/status"
100
+ message['channel'] = segments.join('/')
101
+
102
+ # And finally return the message
103
+ message
104
+ end
105
+
106
+ # Handle START messages
107
+ #
108
+ # @param request the rack request object
109
+ # @param message [Hash] the decoded message sent with the request
110
+ # @param segments [Hash] the message channel split by /
111
+ def handle_start(request, message, segments)
112
+ data = message['data']['message']
113
+
114
+ # Update the execution and add the start time
115
+ Minicron::Hub::Execution.where(:id => segments[3]).update_all(
116
+ :started_at => data[6..-1]
117
+ )
118
+ end
119
+
120
+ # Handle job output
121
+ #
122
+ # @param request the rack request object
123
+ # @param message [Hash] the decoded message sent with the request
124
+ # @param segments [Hash] the message channel split by /
125
+ def handle_output(request, message, segments)
126
+ data = message['data']['message']
127
+ ts = message['data']['ts']
128
+ seq = message['data']['seq']
129
+
130
+ # Store the job execution output
131
+ output = Minicron::Hub::JobExecutionOutput.create(
132
+ :execution_id => segments[3],
133
+ :output => data,
134
+ :timestamp => ts,
135
+ :seq => seq
136
+ )
137
+
138
+ # Append the id to the message so we can use it on the frontend
139
+ message['data']['job_execution_output_id'] = output.id
140
+
141
+ # And finally return the message
142
+ message
143
+ end
144
+
145
+ # Handle FINISH messages
146
+ #
147
+ # @param request the rack request object
148
+ # @param message [Hash] the decoded message sent with the request
149
+ # @param segments [Hash] the message channel split by /
150
+ def handle_finish(request, message, segments)
151
+ data = message['data']['message']
152
+
153
+ # Update the execution and add the finish time
154
+ Minicron::Hub::Execution.where(:id => segments[3]).update_all(
155
+ :finished_at => data[7..-1]
156
+ )
157
+ end
158
+
159
+ # Handle EXIT messages
160
+ #
161
+ # @param request the rack request object
162
+ # @param message [Hash] the decoded message sent with the request
163
+ # @param segments [Hash] the message channel split by /
164
+ def handle_exit(request, message, segments)
165
+ data = message['data']['message']
166
+
167
+ # Update the execution and add the exit status
168
+ Minicron::Hub::Execution.where(:id => segments[3]).update_all(
169
+ :exit_status => data[5..-1]
170
+ )
171
+
172
+ # If the exit status was above 0 we need to trigger a failure alert
173
+ if data[5..-1].to_i > 0
174
+ alert = Minicron::Alert.new
175
+ alert.send_all(
176
+ :kind => 'fail',
177
+ :execution_id => segments[3],
178
+ :job_id => segments[2]
179
+ )
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,58 @@
1
+ require 'thin'
2
+ require 'rack'
3
+ require 'faye'
4
+ require 'minicron/transport/faye/extensions/job_handler'
5
+
6
+ module Minicron
7
+ module Transport
8
+ class FayeServer
9
+ attr_reader :server
10
+
11
+ def initialize
12
+ # Load the Faye thin adapter, this needs to happen first
13
+ Faye::WebSocket.load_adapter('thin')
14
+
15
+ # Show debug verbose output if requested
16
+ if Minicron.config['global']['verbose']
17
+ log = Logger.new(STDOUT)
18
+ log.level = Logger::DEBUG
19
+ Faye.logger = log
20
+ end
21
+
22
+ # Set up our Faye rack app
23
+ @server = Faye::RackAdapter.new(
24
+ :mount => '', # This is relative to the map faye_path set in server.rb
25
+ :timeout => 25
26
+ )
27
+
28
+ @server.add_extension(Minicron::Transport::FayeJobHandler.new)
29
+
30
+ # Add all the events we want to listen out for
31
+ add_faye_events
32
+ end
33
+
34
+ private
35
+ def add_faye_events
36
+ @server.on(:handshake) do |client_id|
37
+ p [:handshake, client_id] if Minicron.config['global']['verbose']
38
+ end
39
+
40
+ @server.on(:subscribe) do |client_id, channel|
41
+ p [:subscribe, client_id, channel] if Minicron.config['global']['verbose']
42
+ end
43
+
44
+ @server.on(:unsubscribe) do |client_id, channel|
45
+ p [:unsubscribe, client_id, channel] if Minicron.config['global']['verbose']
46
+ end
47
+
48
+ @server.on(:publish) do |client_id, channel, data|
49
+ p [:published, client_id, channel, data] if Minicron.config['global']['verbose']
50
+ end
51
+
52
+ @server.on(:disconnect) do |client_id|
53
+ p [:disconnect, client_id] if Minicron.config['global']['verbose']
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,62 @@
1
+ require 'thin'
2
+ require 'rack'
3
+
4
+ module Minicron
5
+ module Transport
6
+ # Used to mangage the web server minicron runs on
7
+ class Server
8
+ attr_reader :server
9
+
10
+ # Starts the thin server
11
+ #
12
+ # @param host [String] the host e.g 0.0.0.0
13
+ # @param port [Integer]
14
+ # @param path [String] The absolute path to the server e.g /server
15
+ def start!(host, port, path)
16
+ return false if running?
17
+
18
+ # Start the faye or rails apps depending on the path
19
+ server = Thin::Server.new(host, port) do
20
+ use Rack::CommonLogger
21
+ use Rack::ShowExceptions
22
+
23
+ # The 'hub', aka our sinatra web interface
24
+ map '/' do
25
+ require Minicron::LIB_PATH + '/minicron/hub/app'
26
+ use Minicron::Hub::ExceptionHandling
27
+ run Minicron::Hub::App.new
28
+ end
29
+
30
+ # Set the path faye should start relative to
31
+ faye_path = path == '/' ? '/faye' : "#{path}/faye"
32
+
33
+ # The faye server the server and browser clients talk to
34
+ map faye_path do
35
+ require Minicron::LIB_PATH + '/minicron/transport/faye/server'
36
+ run Minicron::Transport::FayeServer.new.server
37
+ end
38
+ end
39
+
40
+ server.start
41
+ true
42
+ end
43
+
44
+ # Stops the thin server if it's running
45
+ # @return [Boolean] whether the server was stopped or not
46
+ def stop!
47
+ return false unless running? && server != nil
48
+
49
+ server.stop
50
+ true
51
+ end
52
+
53
+ # Returns a bool based on whether
54
+ # @return [Boolean]
55
+ def running?
56
+ return false unless server != nil
57
+
58
+ server.running?
59
+ end
60
+ end
61
+ end
62
+ end