minicron 0.1.0

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.
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