alnoda-workspaces/workspaces/base-workspace/Cronicle-0.8.61/lib/api/admin.js
2021-07-30 12:18:29 +00:00

346 lines
11 KiB
JavaScript
Executable file

// Cronicle API Layer - Administrative
// 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({
//
// Servers / Master Control
//
api_check_add_server: function(args, callback) {
// check if it is okay to manually add this server to a remote cluster
// (This is a server-to-server API, sent from master to a potential remote slave)
var self = this;
var params = args.params;
if (!this.requireParams(params, {
master: /\S/,
now: /^\d+$/,
token: /\S/
}, callback)) return;
if (params.token != Tools.digestHex( params.master + params.now + this.server.config.get('secret_key') )) {
return this.doError('server', "Secret keys do not match. Please synchronize your config files.", callback);
}
if (this.multi.master) {
return this.doError('server', "Server is already a master server, controlling its own cluster.", callback);
}
if (this.multi.masterHostname && (this.multi.masterHostname != params.master)) {
return this.doError('server', "Server is already a member of a cluster (Primary: " + this.multi.masterHostname + ")", callback);
}
callback({ code: 0, hostname: this.server.hostname, ip: this.server.ip });
},
api_add_server: function(args, callback) {
// add any arbitrary server to cluster (i.e. outside of UDP broadcast range)
var self = this;
var params = args.params;
if (!this.requireMaster(args, callback)) return;
if (!this.requireParams(params, {
hostname: /\S/
}, callback)) return;
var hostname = params.hostname.toLowerCase();
this.loadSession(args, function(err, session, user) {
if (err) return self.doError('session', err.message, callback);
if (!self.requireAdmin(session, user, callback)) return;
args.user = user;
args.session = session;
// make sure server isn't already added
if (self.slaves[hostname]) {
return self.doError('server', "Server is already in cluster: " + hostname, callback);
}
// send HTTP request to server, to make sure we can reach it
var api_url = self.getServerBaseAPIURL( hostname ) + '/app/check_add_server';
var now = Tools.timeNow(true);
var api_args = {
master: self.server.hostname,
now: now,
token: Tools.digestHex( self.server.hostname + now + self.server.config.get('secret_key') )
};
self.logDebug(9, "Sending API request to remote server: " + api_url);
// send request
self.request.json( api_url, api_args, { timeout: 8 * 1000 }, function(err, resp, data) {
if (err) {
return self.doError('server', "Failed to contact server: " + err.message, callback );
}
if (resp.statusCode != 200) {
return self.doError('server', "Failed to contact server: " + hostname + ": HTTP " + resp.statusCode + " " + resp.statusMessage, callback);
}
if (data.code != 0) {
return self.doError('server', "Failed to add server to cluster: " + hostname + ": " + data.description, callback);
}
// replace user-entered hostname with one returned from server (check_add_server api response)
// just in case user entered an IP, or some CNAME
hostname = data.hostname;
// re-check this, for sanity
if (self.slaves[hostname]) {
return self.doError('server', "Server is already in cluster: " + hostname, callback);
}
// one more sanity check, with the IP this time
for (var key in self.slaves) {
var slave = self.slaves[key];
if (slave.ip == data.ip) {
return self.doError('server', "Server is already in cluster: " + hostname + " (" + data.ip + ")", callback);
}
}
// okay to add
var stub = { hostname: hostname, ip: data.ip };
self.logDebug(4, "Adding remote slave server to cluster: " + hostname, stub);
self.addServer(stub, args);
// add to global/servers list
self.storage.listFind( 'global/servers', { hostname: hostname }, function(err, item) {
if (item) {
// server is already in list, just ignore and go
return callback({ code: 0 });
}
// okay to add
self.storage.listPush( 'global/servers', stub, function(err) {
if (err) {
// should never happen
self.logError('server', "Failed to add server to storage: " + hostname + ": " + err);
}
// success
callback({ code: 0 });
} ); // listPush
} ); // listFind
} ); // http request
} ); // load session
},
api_remove_server: function(args, callback) {
// remove any manually-added server from cluster
var self = this;
var params = args.params;
if (!this.requireMaster(args, callback)) return;
if (!this.requireParams(params, {
hostname: /\S/
}, callback)) return;
var hostname = params.hostname.toLowerCase();
this.loadSession(args, function(err, session, user) {
if (err) return self.doError('session', err.message, callback);
if (!self.requireAdmin(session, user, callback)) return;
args.user = user;
args.session = session;
// do not allow removal of current master
if (hostname == self.server.hostname) {
return self.doError('server', "Cannot remove current master server: " + hostname, callback);
}
var slave = self.slaves[hostname];
if (!slave) {
return self.doError('server', "Server not found in cluster: " + hostname, callback);
}
// Do not allow removing server if it has any active jobs
var all_jobs = self.getAllActiveJobs(true);
for (var key in all_jobs) {
var job = all_jobs[key];
if (job.hostname == hostname) {
var err = "Still has running jobs";
return self.doError('server', "Failed to remove server: " + err, callback);
} // matches server
} // foreach job
// okay to remove
self.logDebug(4, "Removing remote slave server from cluster: " + hostname);
self.removeServer({ hostname: hostname }, args);
// delete from global/servers list
self.storage.listFindDelete( 'global/servers', { hostname: hostname }, function(err) {
if (err) {
// should never happen
self.logError('server', "Failed to remove server from storage: " + hostname + ": " + err);
}
// success
callback({ code: 0 });
} ); // listFindDelete
} ); // load session
},
api_restart_server: function(args, callback) {
// restart any server in cluster
var self = this;
var params = args.params;
if (!this.requireMaster(args, callback)) return;
if (!this.requireParams(params, {
hostname: /\S/
}, callback)) return;
this.loadSession(args, function(err, session, user) {
if (err) return self.doError('session', err.message, callback);
if (!self.requireAdmin(session, user, callback)) return;
args.user = user;
args.session = session;
self.logTransaction('server_restart', '', self.getClientInfo(args, params));
self.logActivity('server_restart', params, args);
var reason = "User request by: " + user.username;
if (params.hostname == self.server.hostname) {
// restart this server
self.restartLocalServer({ reason: reason });
callback({ code: 0 });
}
else {
// restart another server in the cluster
var slave = self.slaves[ params.hostname ];
if (slave && slave.socket) {
self.logDebug(6, "Sending remote restart command to: " + slave.hostname);
slave.socket.emit( 'restart_server', { reason: reason } );
callback({ code: 0 });
}
else {
callback({ code: 1, description: "Could not locate server: " + params.hostname });
}
}
} );
},
api_shutdown_server: function(args, callback) {
// shutdown any server in cluster
var self = this;
var params = args.params;
if (!this.requireMaster(args, callback)) return;
if (!this.requireParams(params, {
hostname: /\S/
}, callback)) return;
this.loadSession(args, function(err, session, user) {
if (err) return self.doError('session', err.message, callback);
if (!self.requireAdmin(session, user, callback)) return;
args.user = user;
args.session = session;
self.logTransaction('server_shutdown', '', self.getClientInfo(args, params));
self.logActivity('server_shutdown', params, args);
var reason = "User request by: " + user.username;
if (params.hostname == self.server.hostname) {
// shutdown this server
self.shutdownLocalServer({ reason: reason });
callback({ code: 0 });
}
else {
// shutdown another server in the cluster
var slave = self.slaves[ params.hostname ];
if (slave && slave.socket) {
self.logDebug(6, "Sending remote shutdown command to: " + slave.hostname);
slave.socket.emit( 'shutdown_server', { reason: reason } );
callback({ code: 0 });
}
else {
callback({ code: 1, description: "Could not locate server: " + params.hostname });
}
}
} );
},
api_update_master_state: function(args, callback) {
// update master state (i.e. scheduler enabled)
var self = this;
var params = args.params;
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;
if (!self.requirePrivilege(user, "state_update", callback)) return;
args.user = user;
args.session = session;
// import params into state
self.logDebug(4, "Updating master state:", params);
self.logTransaction('state_update', '', self.getClientInfo(args, params));
self.logActivity('state_update', params, args);
if (params.enabled) {
// need to re-initialize schedule if being enabled
var now = Tools.normalizeTime( Tools.timeNow(), { sec: 0 } );
var cursors = self.state.cursors;
self.storage.listGet( 'global/schedule', 0, 0, function(err, items) {
// got all schedule items
for (var idx = 0, len = items.length; idx < len; idx++) {
var item = items[idx];
// reset cursor to now if event is NOT set to catch up
if (!item.catch_up) {
cursors[ item.id ] = now;
}
} // foreach item
// now it's safe to enable
Tools.mergeHashInto( self.state, params );
self.authSocketEmit( 'update', { state: self.state } );
} ); // loaded schedule
} // params.enabled
else {
// not enabling scheduler, so merge right away
Tools.mergeHashInto( self.state, params );
self.authSocketEmit( 'update', { state: self.state } );
}
callback({ code: 0 });
} );
},
api_get_activity: function(args, callback) {
// get rows from activity log (with pagination)
var self = this;
var params = args.params;
this.loadSession(args, function(err, session, user) {
if (err) return self.doError('session', err.message, callback);
if (!self.requireAdmin(session, user, callback)) return;
self.storage.listGet( 'logs/activity', 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 } });
}
// success, return rows and list header
callback({ code: 0, rows: items, list: list });
} ); // got data
} ); // loaded session
}
} );