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,7 @@
1
+ module Minicron
2
+ VERSION = '0.1.0'
3
+ DEFAULT_CONFIG_FILE = '/etc/minicron.toml'
4
+ BASE_PATH = File.expand_path('../../../', __FILE__)
5
+ LIB_PATH = File.expand_path('../../', __FILE__)
6
+ HUB_PATH = File.expand_path('../../minicron/hub', __FILE__)
7
+ end
@@ -0,0 +1,192 @@
1
+ require 'shellwords'
2
+
3
+ module Minicron
4
+ # Used to interact with the crontab on hosts over an ssh connection
5
+ # TODO: I've had a moment of clarity, I don't need to do all the CRUD
6
+ # using unix commands. I can cat the crontab, manipulate it in ruby
7
+ # and then echo it back!
8
+ class Cron
9
+ # Initialise the cron class
10
+ #
11
+ # @param ssh [Minicron::Transport::SSH] instance
12
+ def initialize(ssh)
13
+ @ssh = ssh
14
+ end
15
+
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
+ # Build the minicron command to be used in the crontab
25
+ #
26
+ # @param command [String]
27
+ # @param schedule [String]
28
+ # @return [String]
29
+ def build_minicron_command(command, schedule)
30
+ # Escape any quotes in the command
31
+ command = escape_command(command)
32
+
33
+ "#{schedule} root minicron run '#{command}'"
34
+ end
35
+
36
+ # Used to find a string and replace it with another in the crontab by
37
+ # using the sed command
38
+ #
39
+ # @param conn an instance of an open ssh connection
40
+ # @param find [String]
41
+ # @param replace [String]
42
+ def find_and_replace(conn, find, replace)
43
+ # TODO: move ssh test here for crontab permissions
44
+
45
+ # Get the full crontab
46
+ crontab = conn.exec!('cat /etc/crontab').to_s.strip
47
+
48
+ # Replace the full string with the replacement string
49
+ begin
50
+ crontab[find] = replace
51
+ rescue Exception => e
52
+ raise Exception, "Unable to replace '#{find}' with '#{replace}' in the crontab, reason: #{e}"
53
+ end
54
+
55
+ # Echo the crontab back to the tmp crontab
56
+ update = conn.exec!("echo #{crontab.shellescape} > /etc/crontab.tmp && echo 'y' || echo 'n'").to_s.strip
57
+
58
+ # Throw an exception if it failed
59
+ if update != 'y'
60
+ raise Exception, "Unable to replace '#{find}' with '#{replace}' in the crontab"
61
+ end
62
+
63
+ # If it's a delete
64
+ if replace == ''
65
+ # Check the original line is no longer there
66
+ grep = conn.exec!("grep -F \"#{find}\" /etc/crontab.tmp").to_s.strip
67
+
68
+ # Throw an exception if we can't see our new line at the end of the file
69
+ if grep != replace
70
+ raise Exception, "Expected to find nothing when grepping crontab but found #{grep}"
71
+ end
72
+ else
73
+ # Check the updated line is there
74
+ grep = conn.exec!("grep -F \"#{replace}\" /etc/crontab.tmp").to_s.strip
75
+
76
+ # Throw an exception if we can't see our new line at the end of the file
77
+ if grep != replace
78
+ raise Exception, "Expected to find '#{replace}' when grepping crontab but found #{grep}"
79
+ end
80
+ end
81
+
82
+ # And finally replace the crontab with the new one now we now the change worked
83
+ move = conn.exec!("mv /etc/crontab.tmp /etc/crontab && echo 'y' || echo 'n'").to_s.strip
84
+
85
+ if move != 'y'
86
+ raise Exception, 'Unable to move tmp crontab with updated crontab'
87
+ end
88
+ end
89
+
90
+ # Add the schedule for this job to the crontab
91
+ #
92
+ # @param job [Minicron::Hub::Job] an instance of a job model
93
+ # @param schedule [String] the job schedule as a string
94
+ # @param conn an instance of an open ssh connection
95
+ def add_schedule(job, schedule, conn = nil)
96
+ # Open an SSH connection
97
+ conn ||= @ssh.open
98
+
99
+ # Prepare the line we are going to write to the crontab
100
+ line = build_minicron_command(job.command, schedule)
101
+ echo_line = "echo \"#{line}\" >> /etc/crontab && echo 'y' || echo 'n'"
102
+
103
+ # Append it to the end of the crontab
104
+ write = conn.exec!(echo_line).strip
105
+
106
+ # Throw an exception if it failed
107
+ if write != 'y'
108
+ raise Exception, "Unable to write '#{line}' to the crontab"
109
+ end
110
+
111
+ # Check the line is there
112
+ tail = conn.exec!('tail -n 1 /etc/crontab').strip
113
+
114
+ # Throw an exception if we can't see our new line at the end of the file
115
+ if tail != line
116
+ raise Exception, "Expected to find '#{line}' at eof but found '#{tail}'"
117
+ end
118
+ end
119
+
120
+ # Update the schedule for this job in the crontab
121
+ #
122
+ # @param job [Minicron::Hub::Job] an instance of a job model
123
+ # @param old_schedule [String] the old job schedule as a string
124
+ # @param new_schedule [String] the new job schedule as a string
125
+ # @param conn an instance of an open ssh connection
126
+ def update_schedule(job, old_schedule, new_schedule, conn = nil)
127
+ # Open an SSH connection
128
+ conn ||= @ssh.open
129
+
130
+ # We are looking for the current value of the schedule
131
+ find = build_minicron_command(job.command, old_schedule)
132
+
133
+ # And replacing it with the updated value
134
+ replace = build_minicron_command(job.command, new_schedule)
135
+
136
+ # Replace the old schedule with the new schedule
137
+ find_and_replace(conn, find, replace)
138
+ end
139
+
140
+ # Remove the schedule for this job from the crontab
141
+ #
142
+ # @param job [Minicron::Hub::Job] an instance of a job model
143
+ # @param schedule [String] the job schedule as a string
144
+ # @param conn an instance of an open ssh connection
145
+ def delete_schedule(job, schedule, conn = nil)
146
+ # Open an SSH connection
147
+ conn ||= @ssh.open
148
+
149
+ # We are looking for the current value of the schedule
150
+ find = build_minicron_command(job.command, schedule)
151
+
152
+ # Replace the old schedule with nothing i.e deleting it
153
+ find_and_replace(conn, find, '')
154
+ end
155
+
156
+ # Delete a job and all it's schedules from the crontab
157
+ #
158
+ # @param job [Minicron::Hub::Job] a job instance with it's schedules
159
+ # @param conn an instance of an open ssh connection
160
+ def delete_job(job, conn = nil)
161
+ conn ||= @ssh.open
162
+
163
+ # Loop through each schedule and delete them one by one
164
+ # TODO: share the ssh connection for this so it's faster when
165
+ # many schedules exist
166
+ # TODO: what if one schedule removal fails but others don't? Should
167
+ # we try and rollback somehow or just return the job with half its
168
+ # schedules deleted?
169
+ job.schedules.each do |schedule|
170
+ delete_schedule(job, schedule.formatted, conn)
171
+ end
172
+ end
173
+
174
+ # Delete a host and all it's jobs from the crontab
175
+ #
176
+ # @param job [Minicron::Hub::Job] a job instance with it's schedules
177
+ # @param conn an instance of an open ssh connection
178
+ def delete_host(host, conn = nil)
179
+ conn ||= @ssh.open
180
+
181
+ # Loop through each job and delete them one by one
182
+ # TODO: share the ssh connection for this so it's faster when
183
+ # many schedules exist
184
+ # TODO: what if one schedule removal fails but others don't? Should
185
+ # we try and rollback somehow or just return the job with half its
186
+ # schedules deleted?
187
+ host.jobs.each do |job|
188
+ delete_job(job, conn)
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,132 @@
1
+ require 'minicron'
2
+ require 'sinatra/base'
3
+ require 'sinatra/activerecord'
4
+ require 'sinatra/assetpack'
5
+ require 'erubis'
6
+ require 'oj'
7
+
8
+ module Minicron::Hub
9
+ class App < Sinatra::Base
10
+ register Sinatra::ActiveRecordExtension
11
+ register Sinatra::AssetPack
12
+
13
+ # Set the application root
14
+ set :root, Minicron::HUB_PATH
15
+
16
+ configure do
17
+ # Don't log them. We'll do that ourself
18
+ set :dump_errors, false
19
+
20
+ # Don't capture any errors. Throw them up the stack
21
+ set :raise_errors, true
22
+
23
+ # Disable internal middleware for presenting errors
24
+ # as useful HTML pages
25
+ set :show_exceptions, false
26
+ end
27
+
28
+ # Configure how we server assets
29
+ assets do
30
+ serve '/css', :from => 'assets/css'
31
+ serve '/js', :from => 'assets/js'
32
+ serve '/fonts', :from => 'assets/fonts'
33
+ serve '/app', :from => 'assets/app'
34
+
35
+ # Set up the application css
36
+ css :app, '/css/all.css', [
37
+ '/css/bootswatch.min.css',
38
+ '/css/main.css'
39
+ ]
40
+
41
+ # Set up the application javascript
42
+ js :app, '/js/all.js', [
43
+ # Dependencies, the order of these is important
44
+ '/js/jquery-2.1.0.min.js',
45
+ '/js/handlebars-1.3.0.min.js',
46
+ '/js/ember-1.4.1.min.js',
47
+ '/js/ember-data-1.0.0-beta.7.f87cba88.min.js',
48
+ '/js/faye-browser-1.0.1.min.js',
49
+ '/js/ansi_up-1.1.1.min.js',
50
+ '/js/bootstrap-3.1.1.min.js',
51
+ '/js/moment-2.5.1.min.js',
52
+
53
+ # Ember application files
54
+ '/app/**/*.js'
55
+ ]
56
+ end
57
+
58
+ # Called on class initilisation, sets up the database and requires all
59
+ # the application files
60
+ def initialize
61
+ super
62
+
63
+ # Initialize the db
64
+ Minicron::Hub::App.setup_db
65
+
66
+ # Load all our model serializers
67
+ Dir[File.dirname(__FILE__) + '/serializers/*.rb'].each do |serializer|
68
+ require serializer
69
+ end
70
+
71
+ # Load all our models
72
+ Dir[File.dirname(__FILE__) + '/models/*.rb'].each do |model|
73
+ require model
74
+ end
75
+
76
+ # Load all our controllers
77
+ Dir[File.dirname(__FILE__) + '/controllers/**/*.rb'].each do |controller|
78
+ require controller
79
+ end
80
+ end
81
+
82
+ # Used to set up the database connection
83
+ def self.setup_db
84
+ # Configure the database
85
+ case Minicron.config['database']['type']
86
+ when 'mysql'
87
+ set :database, {
88
+ :adapter => 'mysql2',
89
+ :host => Minicron.config['database']['host'],
90
+ :database => Minicron.config['database']['database'],
91
+ :username => Minicron.config['database']['username'],
92
+ :password => Minicron.config['database']['password']
93
+ }
94
+ else
95
+ raise Exception, "The database #{Minicron.config['database']['type']} is not supported"
96
+ end
97
+ end
98
+ end
99
+
100
+ # Based on http://hawkins.io/2013/06/error-handling-in-sinatra-apis/
101
+ class ExceptionHandling
102
+ def initialize(app)
103
+ @app = app
104
+ end
105
+
106
+ def handle_exception(env, e, status)
107
+ if Minicron.config['global']['trace']
108
+ env['rack.errors'].puts(e)
109
+ env['rack.errors'].puts(e.backtrace.join("\n"))
110
+ env['rack.errors'].flush
111
+ end
112
+
113
+ # Display the error message
114
+ hash = { :error => e.to_s }
115
+
116
+ # Display the full trace if tracing is enabled
117
+ hash[:trace] = e.backtrace if Minicron.config['global']['trace']
118
+
119
+ [status, { 'Content-Type' => 'application/json' }, [hash.to_json]]
120
+ end
121
+
122
+ def call(env)
123
+ begin
124
+ @app.call env
125
+ rescue ActiveRecord::RecordNotFound => e
126
+ handle_exception(env, e, 404)
127
+ rescue Exception => e
128
+ handle_exception(env, e, 500)
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,151 @@
1
+ 'use strict';
2
+
3
+ (function() {
4
+ window.Minicron = Ember.Application.create({
5
+ LOG_TRANSITIONS: true,
6
+ LOG_ACTIVE_GENERATION: true,
7
+ LOG_VIEW_LOOKUPS: true,
8
+ LOG_TRANSITIONS_INTERNAL: true
9
+ });
10
+
11
+ // This should be called by all views when they are loaded
12
+ Minicron.onViewLoad = function() {
13
+ // Twitter tooltip plugin
14
+ Ember.$('body').tooltip({
15
+ selector: '[data-toggle=tooltip]'
16
+ });
17
+ };
18
+
19
+ // Configure Ember Data so it can find the API
20
+ Minicron.ApplicationAdapter = DS.RESTAdapter.extend({
21
+ namespace: 'api',
22
+ });
23
+
24
+ Minicron.ApplicationController = Ember.ArrayController.extend({
25
+ sortedExecutions: function() {
26
+ return this.get('content').toArray().sort(function(a, b) {
27
+ a = +moment(a.get('created_at'));
28
+ b = +moment(b.get('created_at'));
29
+
30
+ if (a > b) {
31
+ return -1;
32
+ } else if (a < b) {
33
+ return 1;
34
+ }
35
+
36
+ return 0;
37
+ });
38
+ }.property('content.@each').cacheable()
39
+ });
40
+
41
+ Minicron.ApplicationRoute = Ember.Route.extend({
42
+ actions: {
43
+ error: function(error) {
44
+ console.log(error);
45
+
46
+ // Create a new error to be passed as the controller
47
+ var ember_error = new Ember.Error();
48
+
49
+ // Set the details of the error we need
50
+ ember_error.name = 'Error';
51
+ ember_error.message = error.responseJSON.error;
52
+ ember_error.number = error.status;
53
+
54
+ this.render('fatal-error', {
55
+ controller: ember_error
56
+ });
57
+ }
58
+ },
59
+ model: function() {
60
+ return this.store.find('execution');
61
+ },
62
+ setupController: function(controller, model) {
63
+ controller.set('application', model);
64
+ this._super(controller, model);
65
+ },
66
+ afterModel: function(model, transition) {
67
+ var client = new Faye.Client(window.location.protocol + '//' + window.location.host + '/faye'),
68
+ store = model.store,
69
+ self = this;
70
+ window.store = store;
71
+
72
+ client.addExtension({
73
+ incoming: function(message, callback) {
74
+ // We only care about job messages
75
+ if (message.channel.substr(1, 3) === 'job') {
76
+ var segments = message.channel.split('/');
77
+ var job_id = segments[2];
78
+ var job_execution_id = segments[3];
79
+ var type = segments[4];
80
+ var message_data = message.data.message;
81
+
82
+ // TODO: remove this!
83
+ console.log(job_id, job_execution_id, type, message);
84
+
85
+ // Is it a status message?
86
+ if (type === 'status') {
87
+ // Is it a setup message
88
+ if (typeof message_data.action != 'undefined' && message_data.action === 'SETUP') {
89
+ // The SETUP message is defined slightly differently, segment 2 contains the
90
+ // job hash and segment 3 contains '*job_id*-*job_execution_id*'
91
+ var ids = job_execution_id.split('-');
92
+ job_id = ids[0];
93
+ job_execution_id = ids[1];
94
+
95
+ // Append the job relationship to it
96
+ store.find('job', job_id).then(function(job) {
97
+ // Create the execution
98
+ store.push('execution', {
99
+ id: job_execution_id,
100
+ created_at: moment.utc(message.data.ts).format('YYYY-MM-DDTHH:mm:ss[Z]'),
101
+ job: job
102
+ }, true);
103
+ });
104
+ // Is it a start message?
105
+ } else if (message_data.substr(0, 5) === 'START') {
106
+ // Set the execution start time
107
+ store.find('execution', job_execution_id).then(function(execution) {
108
+ execution.set('started_at', moment.utc(message_data.substr(6)).format('YYYY-MM-DDTHH:mm:ss[Z]'));
109
+ });
110
+ // Is it a finish message?
111
+ } else if (message_data.substr(0, 6) === 'FINISH') {
112
+ // Set the execution finish time
113
+ store.find('execution', job_execution_id).then(function(execution) {
114
+ execution.set('finished_at', moment.utc(message_data.substr(7)).format('YYYY-MM-DDTHH:mm:ss[Z]'));
115
+ });
116
+ // Is it an exit message?
117
+ } else if (message_data.substr(0, 4) === 'EXIT') {
118
+ // Set the execution exit status
119
+ store.find('execution', job_execution_id).then(function(execution) {
120
+ execution.set('exit_status', +message_data.substr(5));
121
+ });
122
+ }
123
+ // Is it an output message?
124
+ } else if (type === 'output') {
125
+ store.find('execution', job_execution_id).then(function(execution) {
126
+ // Add this bit of job execution output
127
+ var output = store.createRecord('job_execution_output', {
128
+ id: message.data.job_execution_output_id,
129
+ output: message_data,
130
+ seq: message.data.seq,
131
+ timestamp: moment(message.data.ts).format()
132
+ });
133
+
134
+ output.set('execution', execution);
135
+
136
+ execution.get('job_execution_outputs').then(function(outputs) {
137
+ outputs.pushObject(output);
138
+ });
139
+ });
140
+ }
141
+ }
142
+
143
+ // We're done with the message, pass it back to Faye
144
+ callback(message);
145
+ }
146
+ });
147
+
148
+ client.subscribe('/job/**');
149
+ }
150
+ });
151
+ })();