mirror of
https://github.com/bluxmit/alnoda-workspaces.git
synced 2024-09-19 03:07:10 +12:00
547 lines
19 KiB
JavaScript
Executable file
547 lines
19 KiB
JavaScript
Executable file
// Cronicle API Layer - Events
|
|
// Copyright (c) 2015 Joseph Huckaby
|
|
// Released under the MIT License
|
|
|
|
var fs = require('fs');
|
|
var assert = require("assert");
|
|
var async = require('async');
|
|
|
|
var Class = require("pixl-class");
|
|
var Tools = require("pixl-tools");
|
|
|
|
module.exports = Class.create({
|
|
|
|
//
|
|
// Events:
|
|
//
|
|
|
|
api_get_schedule: function(args, callback) {
|
|
// get list of scheduled events (with pagination)
|
|
var self = this;
|
|
var params = Tools.mergeHashes( args.params, args.query );
|
|
if (!this.requireMaster(args, callback)) return;
|
|
|
|
this.loadSession(args, function(err, session, user) {
|
|
if (err) return self.doError('session', err.message, callback);
|
|
if (!self.requireValidUser(session, user, callback)) return;
|
|
|
|
self.storage.listGet( 'global/schedule', parseInt(params.offset || 0), parseInt(params.limit || 0), function(err, items, list) {
|
|
if (err) {
|
|
// no items found, not an error for this API
|
|
return callback({ code: 0, rows: [], list: { length: 0 } });
|
|
}
|
|
|
|
// success, return keys and list header
|
|
callback({ code: 0, rows: items, list: list });
|
|
} ); // got event list
|
|
} ); // loaded session
|
|
},
|
|
|
|
api_get_event: function(args, callback) {
|
|
// get single event for editing
|
|
var self = this;
|
|
var params = Tools.mergeHashes( args.params, args.query );
|
|
if (!this.requireMaster(args, callback)) return;
|
|
|
|
var criteria = {};
|
|
if (params.id) criteria.id = params.id;
|
|
else if (params.title) criteria.title = params.title;
|
|
else return this.doError('event', "Failed to locate event: No criteria specified", callback);
|
|
|
|
this.loadSession(args, function(err, session, user) {
|
|
if (err) return self.doError('session', err.message, callback);
|
|
if (!self.requireValidUser(session, user, callback)) return;
|
|
|
|
self.storage.listFind( 'global/schedule', criteria, function(err, item) {
|
|
if (err || !item) {
|
|
return self.doError('event', "Failed to locate event: " + (params.id || params.title), callback);
|
|
}
|
|
|
|
// success, return event
|
|
var resp = { code: 0, event: item };
|
|
if (item.queue) resp.queue = self.eventQueue[item.id] || 0;
|
|
|
|
// if event has any active jobs, include those as well
|
|
var all_jobs = self.getAllActiveJobs(true);
|
|
var event_jobs = [];
|
|
for (var key in all_jobs) {
|
|
var job = all_jobs[key];
|
|
if (job.event == item.id) event_jobs.push(job);
|
|
}
|
|
if (event_jobs.length) resp.jobs = event_jobs;
|
|
|
|
callback(resp);
|
|
} ); // got event
|
|
} ); // loaded session
|
|
},
|
|
|
|
api_create_event: function(args, callback) {
|
|
// add new event
|
|
var self = this;
|
|
var event = args.params;
|
|
if (!this.requireMaster(args, callback)) return;
|
|
|
|
if (!this.requireParams(event, {
|
|
title: /\S/,
|
|
enabled: /^(\d+|true|false)$/,
|
|
category: /^\w+$/,
|
|
target: /^[\w\-\.]+$/,
|
|
plugin: /^\w+$/
|
|
}, callback)) return;
|
|
|
|
// validate optional event data parameters
|
|
if (!this.requireValidEventData(event, callback)) return;
|
|
|
|
this.loadSession(args, function(err, session, user) {
|
|
if (err) return self.doError('session', err.message, callback);
|
|
if (!self.requireValidUser(session, user, callback)) return;
|
|
if (!self.requirePrivilege(user, "create_events", callback)) return;
|
|
if (!self.requireCategoryPrivilege(user, event.category, callback)) return;
|
|
if (!self.requireGroupPrivilege(args, user, event.target, callback)) return;
|
|
|
|
args.user = user;
|
|
args.session = session;
|
|
|
|
if (event.id) event.id = event.id.toString().toLowerCase().replace(/\W+/g, '');
|
|
if (!event.id) event.id = self.getUniqueID('e');
|
|
|
|
event.created = event.modified = Tools.timeNow(true);
|
|
|
|
if (!event.max_children) event.max_children = 0;
|
|
if (!event.timeout) event.timeout = 0;
|
|
if (!event.timezone) event.timezone = self.tz;
|
|
if (!event.params) event.params = {};
|
|
|
|
if (user.key) {
|
|
// API Key
|
|
event.api_key = user.key;
|
|
}
|
|
else {
|
|
event.username = user.username;
|
|
}
|
|
|
|
// validate category
|
|
self.storage.listFind( 'global/categories', { id: event.category }, function(err, item) {
|
|
if (err || !item) {
|
|
return self.doError('event', "Failed to create event: Category not found: " + event.category, callback);
|
|
}
|
|
|
|
self.logDebug(6, "Creating new event: " + event.title, event);
|
|
|
|
self.storage.listUnshift( 'global/schedule', event, function(err) {
|
|
if (err) {
|
|
return self.doError('event', "Failed to create event: " + err, callback);
|
|
}
|
|
|
|
self.logDebug(6, "Successfully created event: " + event.title, event);
|
|
self.logTransaction('event_create', event.title, self.getClientInfo(args, { event: event }));
|
|
self.logActivity('event_create', { event: event }, args);
|
|
|
|
callback({ code: 0, id: event.id });
|
|
|
|
// broadcast update to all websocket clients
|
|
self.updateClientData( 'schedule' );
|
|
|
|
// create cursor for new event
|
|
var now = Tools.normalizeTime( Tools.timeNow(), { sec: 0 } );
|
|
self.state.cursors[ event.id ] = now;
|
|
|
|
// send new state data to all web clients
|
|
self.authSocketEmit( 'update', { state: self.state } );
|
|
|
|
} ); // list insert
|
|
} ); // find cat
|
|
} ); // load session
|
|
},
|
|
|
|
api_update_event: function(args, callback) {
|
|
// update existing event
|
|
var self = this;
|
|
var params = args.params;
|
|
if (!this.requireMaster(args, callback)) return;
|
|
|
|
if (!this.requireParams(params, {
|
|
id: /^\w+$/
|
|
}, callback)) return;
|
|
|
|
// validate optional event data parameters
|
|
if (!this.requireValidEventData(params, callback)) return;
|
|
|
|
this.loadSession(args, function(err, session, user) {
|
|
if (err) return self.doError('session', err.message, callback);
|
|
if (!self.requireValidUser(session, user, callback)) return;
|
|
if (!self.requirePrivilege(user, "edit_events", callback)) return;
|
|
if (params.abort_jobs && !self.requirePrivilege(user, "abort_events", callback)) return;
|
|
|
|
args.user = user;
|
|
args.session = session;
|
|
|
|
self.storage.listFind( 'global/schedule', { id: params.id }, function(err, event) {
|
|
if (err || !event) {
|
|
return self.doError('event', "Failed to locate event: " + params.id, callback);
|
|
}
|
|
|
|
// validate category
|
|
self.storage.listFind('global/categories', { id: params.category || event.category }, function (err, item) {
|
|
if (err || !item) {
|
|
return self.doError('event', "Failed to update event: Category not found: " + params.category, callback);
|
|
}
|
|
|
|
if (!self.requireCategoryPrivilege(user, event.category, callback)) return;
|
|
if (!self.requireGroupPrivilege(args, user, event.target, callback)) return;
|
|
|
|
params.modified = Tools.timeNow(true);
|
|
|
|
self.logDebug(6, "Updating event: " + event.title, params);
|
|
|
|
// pull cursor reset out of event object, for use later
|
|
var new_cursor = 0;
|
|
if (params.reset_cursor) {
|
|
new_cursor = Tools.normalizeTime(params.reset_cursor - 60, { sec: 0 });
|
|
delete params.reset_cursor;
|
|
}
|
|
|
|
// pull abort flag out of event object, for use later
|
|
var abort_jobs = 0;
|
|
if (params.abort_jobs) {
|
|
abort_jobs = params.abort_jobs;
|
|
delete params.abort_jobs;
|
|
}
|
|
|
|
self.storage.listFindUpdate( 'global/schedule', { id: params.id }, params, function(err) {
|
|
if (err) {
|
|
return self.doError('event', "Failed to update event: " + err, callback);
|
|
}
|
|
|
|
// merge params into event, just so we have the full updated record
|
|
for (var key in params) event[key] = params[key];
|
|
|
|
// optionally reset cursor
|
|
if (new_cursor) {
|
|
var dargs = Tools.getDateArgs( new_cursor );
|
|
self.logDebug(6, "Resetting event cursor to: " + dargs.yyyy_mm_dd + ' ' + dargs.hh_mi_ss);
|
|
self.state.cursors[ params.id ] = new_cursor;
|
|
|
|
// send new state data to all web clients
|
|
self.authSocketEmit( 'update', { state: self.state } );
|
|
}
|
|
|
|
self.logDebug(6, "Successfully updated event: " + event.id + " (" + event.title + ")");
|
|
self.logTransaction('event_update', event.title, self.getClientInfo(args, { event: params }));
|
|
self.logActivity('event_update', { event: params }, args);
|
|
|
|
// send response to web client
|
|
callback({ code: 0 });
|
|
|
|
// broadcast update to all websocket clients
|
|
self.updateClientData( 'schedule' );
|
|
|
|
// if event is disabled, abort all applicable jobs
|
|
if (!event.enabled && abort_jobs) {
|
|
var all_jobs = self.getAllActiveJobs(true);
|
|
for (var key in all_jobs) {
|
|
var job = all_jobs[key];
|
|
if ((job.event == event.id) && !job.detached) {
|
|
var msg = "Event '" + event.title + "' has been disabled.";
|
|
self.logDebug(4, "Job " + job.id + " is being aborted: " + msg);
|
|
self.abortJob({ id: job.id, reason: msg });
|
|
} // matches event
|
|
} // foreach job
|
|
} // event disabled
|
|
|
|
// if this is a catch_up event and is being enabled, force scheduler to re-tick the minute
|
|
var dargs = Tools.getDateArgs( new Date() );
|
|
if (params.enabled && event.catch_up && !self.schedulerGraceTimer && !self.schedulerTicking && (dargs.sec != 59)) {
|
|
self.schedulerMinuteTick( null, true );
|
|
}
|
|
|
|
// check event queue
|
|
if (self.eventQueue[event.id]) {
|
|
if (event.queue) self.checkEventQueues( event.id );
|
|
else self.deleteEventQueues( event.id );
|
|
}
|
|
} ); // update event
|
|
} ); // find cat
|
|
} ); // find event
|
|
} ); // load session
|
|
},
|
|
|
|
api_delete_event: function(args, callback) {
|
|
// delete existing event
|
|
var self = this;
|
|
var params = args.params;
|
|
if (!this.requireMaster(args, callback)) return;
|
|
|
|
if (!this.requireParams(params, {
|
|
id: /^\w+$/
|
|
}, callback)) return;
|
|
|
|
this.loadSession(args, function(err, session, user) {
|
|
if (err) return self.doError('session', err.message, callback);
|
|
if (!self.requireValidUser(session, user, callback)) return;
|
|
if (!self.requirePrivilege(user, "delete_events", callback)) return;
|
|
|
|
args.user = user;
|
|
args.session = session;
|
|
|
|
self.storage.listFind( 'global/schedule', { id: params.id }, function(err, event) {
|
|
if (err || !event) {
|
|
return self.doError('event', "Failed to locate event: " + params.id, callback);
|
|
}
|
|
|
|
if (!self.requireCategoryPrivilege(user, event.category, callback)) return;
|
|
if (!self.requireGroupPrivilege(args, user, event.target, callback)) return;
|
|
|
|
// Do not allow deleting event if any active jobs
|
|
var all_jobs = self.getAllActiveJobs(true);
|
|
for (var key in all_jobs) {
|
|
var job = all_jobs[key];
|
|
if (job.event == params.id) {
|
|
var err = "Still has running jobs";
|
|
return self.doError('event', "Failed to delete event: " + err, callback);
|
|
} // matches event
|
|
} // foreach job
|
|
|
|
self.logDebug(6, "Deleting event: " + params.id);
|
|
|
|
self.storage.listFindDelete( 'global/schedule', { id: params.id }, function(err) {
|
|
if (err) {
|
|
return self.doError('event', "Failed to delete event: " + err, callback);
|
|
}
|
|
|
|
self.logDebug(6, "Successfully deleted event: " + event.title, event);
|
|
self.logTransaction('event_delete', event.title, self.getClientInfo(args, { event: event }));
|
|
self.logActivity('event_delete', { event: event }, args);
|
|
|
|
callback({ code: 0 });
|
|
|
|
// broadcast update to all websocket clients
|
|
self.updateClientData( 'schedule' );
|
|
|
|
// schedule event's activity log to be deleted at next maint run
|
|
self.storage.expire( 'logs/events/' + event.id, Tools.timeNow(true) + 86400 );
|
|
|
|
// delete state data
|
|
delete self.state.cursors[ event.id ];
|
|
if (self.state.robins) delete self.state.robins[ event.id ];
|
|
|
|
// send new state data to all web clients
|
|
self.authSocketEmit( 'update', { state: self.state } );
|
|
|
|
// check event queue
|
|
if (self.eventQueue[event.id]) {
|
|
self.deleteEventQueues( event.id );
|
|
}
|
|
|
|
} ); // delete
|
|
} ); // listFind
|
|
} ); // load session
|
|
},
|
|
|
|
api_run_event: function(args, callback) {
|
|
// run event manually (via "Run" button in UI or by API Key)
|
|
// can include any event overrides in params (such as 'now')
|
|
var self = this;
|
|
if (!this.requireMaster(args, callback)) return;
|
|
|
|
// default behavor: merge post params and query together
|
|
// alt behavior (post_data): store post params into post_data
|
|
var params = Tools.copyHash( args.query, true );
|
|
if (args.query.post_data) params.post_data = args.params;
|
|
else Tools.mergeHashInto( params, args.params );
|
|
|
|
var criteria = {};
|
|
if (params.id) criteria.id = params.id;
|
|
else if (params.title) criteria.title = params.title;
|
|
else return this.doError('event', "Failed to locate event: No criteria specified", callback);
|
|
|
|
// validate optional event data parameters
|
|
if (!this.requireValidEventData(params, callback)) return;
|
|
|
|
this.loadSession(args, function(err, session, user) {
|
|
if (err) return self.doError('session', err.message, callback);
|
|
if (!self.requireValidUser(session, user, callback)) return;
|
|
if (!self.requirePrivilege(user, "run_events", callback)) return;
|
|
|
|
args.user = user;
|
|
args.session = session;
|
|
|
|
self.storage.listFind( 'global/schedule', criteria, function(err, event) {
|
|
if (err || !event) {
|
|
return self.doError('event', "Failed to locate event: " + (params.id || params.title), callback);
|
|
}
|
|
|
|
if (!self.requireCategoryPrivilege(user, event.category, callback)) return;
|
|
if (!self.requireGroupPrivilege(args, user, event.target, callback)) return;
|
|
|
|
delete params.id;
|
|
delete params.title;
|
|
delete params.catch_up;
|
|
delete params.category;
|
|
delete params.multiplex;
|
|
delete params.stagger;
|
|
delete params.detached;
|
|
delete params.queue;
|
|
delete params.queue_max;
|
|
delete params.max_children;
|
|
delete params.session_id;
|
|
|
|
// allow for ¶ms/foo=bar and the like
|
|
for (var key in params) {
|
|
if (key.match(/^(\w+)\/(\w+)$/)) {
|
|
var parent_key = RegExp.$1;
|
|
var sub_key = RegExp.$2;
|
|
if (!params[parent_key]) params[parent_key] = {};
|
|
params[parent_key][sub_key] = params[key];
|
|
delete params[key];
|
|
}
|
|
}
|
|
|
|
// allow sparsely populated event params in request
|
|
if (params.params && event.params) {
|
|
for (var key in event.params) {
|
|
if (!(key in params.params)) params.params[key] = event.params[key];
|
|
}
|
|
}
|
|
|
|
var job = Tools.mergeHashes( Tools.copyHash(event, true), params );
|
|
if (user.key) {
|
|
// API Key
|
|
job.source = "API Key ("+user.title+")";
|
|
job.api_key = user.key;
|
|
}
|
|
else {
|
|
job.source = "Manual ("+user.username+")";
|
|
job.username = user.username;
|
|
}
|
|
|
|
self.logDebug(6, "Running event manually: " + job.title, job);
|
|
|
|
self.launchOrQueueJob( job, function(err, jobs_launched) {
|
|
if (err) {
|
|
return self.doError('event', "Failed to launch event: " + err.message, callback);
|
|
}
|
|
|
|
// multiple jobs may have been launched (multiplex)
|
|
var ids = [];
|
|
for (var idx = 0, len = jobs_launched.length; idx < len; idx++) {
|
|
var job = jobs_launched[idx];
|
|
var stub = { id: job.id, event: job.event };
|
|
self.logTransaction('job_run', job.event_title, self.getClientInfo(args, stub));
|
|
if (self.server.config.get('track_manual_jobs')) self.logActivity('job_run', stub, args);
|
|
ids.push( job.id );
|
|
}
|
|
|
|
var resp = { code: 0, ids: ids };
|
|
if (!ids.length) resp.queue = self.eventQueue[event.id] || 1;
|
|
callback(resp);
|
|
} ); // launch job
|
|
} ); // find event
|
|
} ); // load session
|
|
},
|
|
|
|
api_flush_event_queue: function(args, callback) {
|
|
// flush event queue
|
|
var self = this;
|
|
var params = args.params;
|
|
if (!this.requireMaster(args, callback)) return;
|
|
|
|
if (!this.requireParams(params, {
|
|
id: /^\w+$/
|
|
}, callback)) return;
|
|
|
|
this.loadSession(args, function(err, session, user) {
|
|
if (err) return self.doError('session', err.message, callback);
|
|
if (!self.requireValidUser(session, user, callback)) return;
|
|
if (!self.requirePrivilege(user, "abort_events", callback)) return;
|
|
|
|
args.user = user;
|
|
args.session = session;
|
|
|
|
self.storage.listFind( 'global/schedule', { id: params.id }, function(err, event) {
|
|
if (err || !event) {
|
|
return self.doError('event', "Failed to locate event: " + params.id, callback);
|
|
}
|
|
|
|
if (!self.requireCategoryPrivilege(user, event.category, callback)) return;
|
|
if (!self.requireGroupPrivilege(args, user, event.target, callback)) return;
|
|
|
|
self.deleteEventQueues( params.id, function() {
|
|
callback({ code: 0 });
|
|
} );
|
|
|
|
} ); // listFind
|
|
} ); // loadSession
|
|
},
|
|
|
|
api_get_event_history: function(args, callback) {
|
|
// get event history
|
|
var self = this;
|
|
var params = Tools.mergeHashes( args.params, args.query );
|
|
|
|
if (!this.requireParams(params, {
|
|
id: /^\w+$/
|
|
}, callback)) return;
|
|
|
|
this.loadSession(args, function(err, session, user) {
|
|
if (err) return self.doError('session', err.message, callback);
|
|
if (!self.requireValidUser(session, user, callback)) return;
|
|
|
|
args.user = user;
|
|
args.session = session;
|
|
|
|
self.storage.listFind( 'global/schedule', { id: params.id }, function(err, event) {
|
|
if (err || !event) {
|
|
return self.doError('event', "Failed to locate event: " + params.id, callback);
|
|
}
|
|
|
|
if (!self.requireCategoryPrivilege(user, event.category, callback)) return;
|
|
if (!self.requireGroupPrivilege(args, user, event.target, callback)) return;
|
|
|
|
self.storage.listGet( 'logs/events/' + params.id, parseInt(params.offset || 0), parseInt(params.limit || 100), function(err, items, list) {
|
|
if (err) {
|
|
// no rows found, not an error for this API
|
|
return callback({ code: 0, rows: [], list: { length: 0 } });
|
|
}
|
|
|
|
// success, return rows and list header
|
|
callback({ code: 0, rows: items, list: list });
|
|
|
|
} ); // got data
|
|
} ); // listFind
|
|
} ); // load session
|
|
},
|
|
|
|
api_get_history: function(args, callback) {
|
|
// get list of completed jobs for ALL events (with pagination)
|
|
var self = this;
|
|
var params = Tools.mergeHashes( args.params, args.query );
|
|
|
|
this.loadSession(args, function(err, session, user) {
|
|
if (err) return self.doError('session', err.message, callback);
|
|
if (!self.requireValidUser(session, user, callback)) return;
|
|
|
|
// user may be limited to certain categories
|
|
// but in order to keep pagination sane, just mask out IDs
|
|
var privs = user.privileges;
|
|
var cat_limited = (!privs.admin && privs.cat_limit);
|
|
|
|
self.storage.listGet( 'logs/completed', parseInt(params.offset || 0), parseInt(params.limit || 50), function(err, items, list) {
|
|
if (err) {
|
|
// no rows found, not an error for this API
|
|
return callback({ code: 0, rows: [], list: { length: 0 } });
|
|
}
|
|
|
|
// mask out IDs for cats that user shouldn't see
|
|
if (cat_limited) items.forEach( function(item) {
|
|
var priv_id = 'cat_' + item.category;
|
|
if (!privs[priv_id]) item.id = '';
|
|
} );
|
|
|
|
// success, return rows and list header
|
|
callback({ code: 0, rows: items, list: list });
|
|
} ); // got data
|
|
} ); // loaded session
|
|
}
|
|
|
|
} );
|