mongodb-tools
Version:
MongoDB Tools used for testing
879 lines (756 loc) • 24.1 kB
JavaScript
var f = require('util').format;
var path = require('path');
var mkdirp = require('mkdirp');
var rimraf = require('rimraf');
var exec = require('child_process').exec;
var ServerManager = require('./server_manager');
var ReadPreference = require('mongodb-core').ReadPreference;
var Server = require('mongodb-core').Server;
//
// Remove any non-server specific settings
var filterInternalOptionsOut = function(options, internalOptions) {
var opts = {};
for (var name in options) {
if (internalOptions.indexOf(name) === -1) {
opts[name] = options[name];
}
}
return opts;
};
var clone = function(obj) {
var o = {};
for(var name in obj) o[name] = obj[name];
return o;
}
var ReplSetManager = function(replsetOptions) {
replsetOptions = replsetOptions || {};
var startPort = replsetOptions.port = replsetOptions.startPort || 31000;
// Get the settings
var secondaries = typeof replsetOptions.secondaries === 'number'
? replsetOptions.secondaries : 2;
var arbiters = typeof replsetOptions.arbiters === 'number'
? replsetOptions.arbiters : 0;
var passives = typeof replsetOptions.passives === 'number'
? replsetOptions.passives : 1;
var replSet = replsetOptions.replSet = replsetOptions.replSet || 'rs';
var version = 1;
var configSet = null;
var tags = replsetOptions.tags;
var host = replsetOptions.host || 'localhost';
// All server addresses
var serverAddresses = [];
var secondaryServers = [];
var arbiterServers = [];
var passiveServers = [];
// DbPath
var dbpath = replsetOptions.dbpath = replsetOptions.dbpath || path.resolve('data');
// Get the keys
var keys = Object.keys(replsetOptions);
// Internal check server
var server = null;
// Clone the options
replsetOptions = clone(replsetOptions);
// Any needed credentials
var credentials;
// Contains all the server managers
var serverManagers = [];
// filtered out internal keys
var internalOptions = filterInternalOptionsOut(replsetOptions
, ['bin', 'host', 'secondaries', 'arbiters', 'startPort', 'tags']);
Object.defineProperty(this, 'secondaries', {
enumerable: true,
get: function() {
return secondaryServers.slice(0);
}
});
Object.defineProperty(this, 'passives', {
enumerable: true,
get: function() {
return passiveServers.slice(0);
}
});
Object.defineProperty(this, 'arbiters', {
enumerable: true,
get: function() {
return arbiterServers.slice(0);
}
});
Object.defineProperty(this, 'name', {
enumerable: true,
get: function() {
return replSet;
}
});
Object.defineProperty(this, 'startPort', {
enumerable: true,
get: function() {
return startPort;
}
});
Object.defineProperty(this, 'replicasetName', {
enumerable: true,
get: function() {
return replSet;
}
});
//
// ensure replicaset is up and running
var ensureUp = function(server, callback) {
// Get the replicaset status
server.command('admin.$cmd', {
replSetGetStatus: 1
}, function(err, result) {
if (err || result.result.ok === 0) {
return setTimeout(function() {
ensureUp(server, callback);
}, 1000);
}
// The result
result = result.result;
var ready = true;
var hasPrimary = false;
// Figure out if all the servers are ready
result.members.forEach(function(m) {
if ([1, 2, 7].indexOf(m.state) === -1) {
ready = false;
}
});
if (ready) {
result.members.forEach(function(m) {
if (m.state === 1) {
hasPrimary = true;
}
});
}
if (ready && hasPrimary) {
// Get the ismaster
server.destroy();
// debug('`%s` is ready!', server.name);
return callback(null, result.result);
}
// debug('`%s` not ready yet...', server.name);
// Query the state of the replicaset again
setTimeout(function() {
ensureUp(server, callback);
}, 1000);
});
};
//
// Check all hosts to locate the primary
var ensureIsMasterUp = function(configSet, callback) {
var members = configSet.members.slice(0);
var foundPrimary = false;
// Query the server
var queryServer = function(hosts, callback) {
if (foundPrimary) {
return callback(null, null);
}
if (hosts.length === 0 && !foundPrimary) {
return setTimeout(function() {
ensureIsMasterUp(configSet, callback);
}, 1000);
}
var hostString = hosts.shift().host;
var host = hostString.split(':')[0];
var port = parseInt(hostString.split(':')[1], 10);
// Set the basic options
var opts = {
host: host,
port: port,
size: 1
};
// Set the key
if (keys.indexOf('sslOnNormalPorts') !== -1) {
opts.ssl = true;
}
if (keys.indexOf('ssl') !== -1) {
opts.ssl = replsetOptions.ssl;
}
if (keys.indexOf('ca') !== -1) {
opts.ca = replsetOptions.ca;
}
if (keys.indexOf('cert') !== -1) {
opts.cert = replsetOptions.cert;
}
if (keys.indexOf('rejectUnauthorized') !== -1) {
opts.rejectUnauthorized = replsetOptions.rejectUnauthorized;
}
if (keys.indexOf('key') !== -1) {
opts.key = replsetOptions.key;
}
if (keys.indexOf('passphrase') !== -1) {
opts.passphrase = replsetOptions.passphrase;
}
// Create a server instance
// var server = new Server({host: host, port: port, size: 1});
var server = new Server(opts);
// Process result
var processResult = function(_s) {
_s.destroy();
if (_s.lastIsMaster().ismaster) {
foundPrimary = true;
}
queryServer(hosts, callback);
};
var connectFailed = function() {
server.destroy();
queryServer(hosts, callback);
};
server.once('connect', processResult);
server.once('error', connectFailed);
server.once('close', connectFailed);
server.once('timeout', connectFailed);
server.connect();
};
queryServer(members, function() {
if (foundPrimary) return callback(null, null);
setTimeout(ensureIsMasterUp(configSet, callback));
});
};
//
// Configure and ensure
var configureAndEnsure = function(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
var _id = 0;
configSet = {
_id: replSet,
version: version,
members: [{
_id: _id,
host: serverManagers[_id].name
}]
};
// Update _id
_id = _id + 1;
var i;
// For all servers add the members
for (i = 0; i < secondaries; i++, _id++) {
configSet.members[_id] = {
_id: _id,
host: serverManagers[_id].name
};
}
// For all servers add the members
for (i = 0; i < arbiters; i++, _id++) {
configSet.members[_id] = {
_id: _id,
host: serverManagers[_id].name,
arbiterOnly: true
};
}
// For all servers add the members
for (i = 0; i < passives; i++, _id++) {
configSet.members[_id] = {
_id: _id,
host: serverManagers[_id].name,
priority: 0
};
}
// Do we have tags to add to our config
if (Array.isArray(tags)) {
for (i = 0; i < tags.length; i++) {
if (configSet.members[i] !== null) {
configSet.members[i].tags = tags[i];
}
}
}
var opts = {
host: serverManagers[0].host,
port: serverManagers[0].port,
connectionTimeout: 2000,
size: 1,
reconnect: false,
emitError: true
};
// Set the key
if (keys.indexOf('sslOnNormalPorts') !== -1) {
opts.ssl = true;
}
if (keys.indexOf('ssl') !== -1) {
opts.ssl = replsetOptions.ssl;
}
if (keys.indexOf('ca') !== -1) {
opts.ca = replsetOptions.ca;
}
if (keys.indexOf('cert') !== -1) {
opts.cert = replsetOptions.cert;
}
if (keys.indexOf('rejectUnauthorized') !== -1) {
opts.rejectUnauthorized = replsetOptions.rejectUnauthorized;
}
if (keys.indexOf('key') !== -1) {
opts.key = replsetOptions.key;
}
if (keys.indexOf('passphrase') !== -1) {
opts.passphrase = replsetOptions.passphrase;
}
// Let's pick one of the servers and run the command against it
server = new Server(opts);
var onError = function(err) {
callback(err, null);
};
// Set up the connection
server.on('connect', function(server) {
// Execute configure replicaset
server.command('admin.$cmd'
, {
replSetInitiate: configSet
}
, {
readPreference: new ReadPreference('secondary')
}, function(err) {
if (options.override || err === null) {
// server.command('system.$cmd'
// , {ismaster:true}, function(err, r) {
return ensureUp(server, function(err) {
if (err) return callback(err);
// Ensure IsMaster up across all the servers
ensureIsMasterUp(configSet, function(err, r) {
server.destroy();
callback(err, r);
});
});
}
// Return error
if (err) return callback(err, null);
});
});
server.once('error', onError);
server.once('close', onError);
server.once('timeout', onError);
// Connect
server.connect();
};
//
// Reconfigure and ensure
//
// Start the server
this.start = function(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
// Create server instances
var totalServers = secondaries + arbiters + passives + 1;
var serversLeft = totalServers;
var purge = typeof options.purge === 'boolean' ? options.purge : true;
var kill = typeof options.kill === 'boolean' ? options.kill : true;
var signal = typeof options.signal === 'number' ? options.signal : -15;
var self = this;
// Remove db path and recreate it
if (purge) {
try {
rimraf.sync(dbpath);
mkdirp.sync(dbpath);
} catch (err) {
console.error('Purge failed', err);
}
}
// Do we not have any server managers
if (serverManagers.length === 0) {
// Start all the servers
for (var i = 0; i < totalServers; i++) {
// Clone the options
var opts = clone(internalOptions);
// Emit errors
opts.emitError = true;
// Set the current Port
opts.host = host;
opts.port = startPort + i;
opts.dbpath = opts.dbpath ? opts.dbpath + f('/data-%s', opts.port) : null;
opts.logpath = opts.logpath ? opts.logpath + f('/data-%s.log', opts.port) : null;
// Add list
serverAddresses.push(f('%s:%s', host, opts.port));
// Create a server manager
serverManagers.push(new ServerManager(opts));
}
}
// Starts all the provided servers
var startServers = function() {
// Start all the servers
for (var i = 0; i < serverManagers.length; i++) {
var startOpts = {
purge: purge,
signal: signal
};
// Start the server
serverManagers[i].start(startOpts, function(err) {
if (err) {
throw err;
}
serversLeft = serversLeft - 1;
// All servers are down
if (serversLeft === 0) {
// Configure the replicaset
configureAndEnsure(function() {
// Refresh view
getServerManagerByType('primary', function() {
callback(null, self);
});
});
}
});
}
};
// Kill all mongod instances if kill specified
if (kill) {
return exec(f('killall %d mongod', signal), function() {
setTimeout(function() {
startServers();
}, 1000);
});
}
// Start all the servers
startServers();
};
this.stop = function(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
if (server) {
server.destroy();
}
var count = serverManagers.length;
var servers = serverManagers.slice(0);
// If we have no servers return
if (count === 0) return callback();
// Stop all servers
servers.forEach(function(s) {
s.stop(options, function() {
count = count - 1;
if (count === 0) {
callback();
}
});
});
};
this.restart = function(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
// Total server managers to check
var count = serverManagers.length;
// Check an individual servers state
var checkServerManager = function(serverManager, _callback) {
serverManager.connect(function(err) {
// Attempt to restart server manager
if (err) {
serverManager.start(options, function() {
count = count - 1;
if (count === 0) {
// Started all the servers, reconfigure and ensure
configureAndEnsure({
override: true
}, function() {
_callback(null, null);
});
}
});
} else {
count = count - 1;
}
if (count === 0) {
// Started all the servers, reconfigure and ensure
configureAndEnsure({
override: true
}, function() {
_callback(null, null);
});
}
});
};
// Iterate over all the server managers restarting as needed
for (var i = 0; i < serverManagers.length; i++) {
var stopAndCheck = function(j) {
var currServerManager = serverManagers[j];
if (options.shouldStop){
currServerManager.stop(options, function () {
setTimeout( function () {
checkServerManager(currServerManager, callback);
}, 1000);
});
} else {
checkServerManager(currServerManager, callback);
}
};
stopAndCheck(i);
}
};
this.setCredentials = function(provider, db, user, password) {
credentials = {
provider: provider,
db: db,
user: user,
password: password
};
};
this.updateServerOptions = function(options) {
for (var attrname in options) {
replsetOptions[attrname] = options[attrname];
}
for (var i = 0; i < serverManagers.length; i++) {
serverManagers[i].updateServerOptions(options);
}
};
//
// Get the current ismaster
var getIsMaster = function(callback) {
if (serverManagers.length === 0) return callback(new Error('no servers'));
// Run ismaster against primary only
getServerManagerByType('primary', function(err, manager) {
if (err) return callback(err);
manager.ismaster(callback);
});
};
//
// Get a current serve
var getServerManager = function(address, callback) {
if (serverManagers.length === 0) return callback(new Error('no servers'));
var manager = null;
for (var i = 0; i < serverManagers.length; i++) {
if (serverManagers[i].lastIsMaster().me === address) {
manager = serverManagers[i];
}
}
if (manager === null) return callback(new Error('no servers'));
// We have an active server connection return it
return callback(null, manager);
};
//
// Get server by type
var getServerManagerByType = function(type, callback) {
if (serverManagers.length === 0) return callback(new Error('no servers'));
// Left to validate
var left = serverManagers.length;
var manager = null;
// Server function
var s = function(_manager, callback) {
_manager.ismaster(function(err, ismaster) {
if (err) return callback(err, null);
if (ismaster.secondary && ismaster.passive === null) {
secondaryServers.push(ismaster.me);
} else if (ismaster.arbiterOnly) {
arbiterServers.push(ismaster.me);
} else if (ismaster.ismaster) {
primaryServer = ismaster.me;
}
if (type === 'secondary' && manager === null && ismaster.secondary) {
manager = _manager;
} else if (type === 'primary' && manager === null && ismaster.ismaster) {
manager = _manager;
} else if (type === 'arbiter' && manager === null && ismaster.arbiterOnly) {
manager = _manager;
}
callback(null, null);
});
};
// Remove all internal services
secondaryServers = [];
arbiterServers = [];
primaryServer = [];
// Locate a server of the right type
for (var i = 0; i < serverManagers.length; i++) {
s(serverManagers[i], function() {
left = left - 1;
// Done talking to all the servers
if (left === 0) {
if (manager === null) return callback(new Error('no servers'));
callback(null, manager);
}
});
}
};
this.getIsMaster = function(callback) {
return getIsMaster(callback);
};
this.shutdown = function(type, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
options.signal = options.signal || -3;
// Get server by type
getServerManagerByType(type, function(err, manager) {
if (err) return callback(err);
// Set credentials
if (credentials) {
manager.setCredentials(credentials.provider, credentials.db, credentials.user, credentials.password);
}
// Shut down the server
manager.stop(options, callback);
});
};
this.restartServer = function(type, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
// Locate a downed secondary server
var manager = null;
for (var i = 0; i < serverManagers.length; i++) {
if (!serverManagers[i].isConnected() && serverManagers[i].lastIsMaster().secondary) {
manager = serverManagers[i];
break;
}
}
// Purge and delete
options.purge = true;
// No manager
if (manager === null) return callback(new Error('no downed secondary server found'));
// Restart the server
manager.start(options, callback);
};
this.getServerManagerByType = function(type, callback) {
return getServerManagerByType(type, callback);
};
this.remove = function(t, callback) {
if (typeof t === 'function') {
callback = t;
t = 'secondary';
}
// Get primary manager
getServerManagerByType('primary', function(err, manager) {
if (err) return callback(err, null);
// Get a connection to the master
manager.connect(function(err, server) {
if (err) return callback(err, null);
// Avoid double calls
var done = false;
// Execute find
var cursor = server.cursor('local.system.replset', {
find: 'local.system.replset',
query: {}
}).next(function(err, d) {
if (err) return callback(err, null);
if (d === null) return;
if (done) return;
done = true;
// Destroy the connection
server.destroy();
// Locate a secondary and remove it
getServerManagerByType(t, function(err, m) {
if (err) return callback(err);
// Remove from the list of the result
var members = [];
var removedServer = null;
// Find all all servers not matching the one we want removed
for (var i = 0; i < d.members.length; i++) {
if (d.members[i].host !== m.lastIsMaster().me) {
members.push(d.members[i]);
} else {
removedServer = d.members[i];
}
}
// Update the config
d.members = members;
d.version = d.version + 1;
// Set credentials
if (credentials) {
manager.setCredentials(credentials.provider, credentials.db, credentials.user, credentials.password);
}
// Get a connection to the master
manager.connect(function(err, server) {
if (err) return callback(err, null);
// Reconfigure the replicaset
server.command('admin.$cmd', {
replSetReconfig: d
}, function(err, result) {});
// Call back as the command above will fail
callback(null, removedServer);
});
});
});
});
});
};
this.add = function(serverDetails, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
// Get primary manager
getServerManagerByType('primary', function(err, manager) {
if (err) return callback(err, null);
// Get a connection to the master
manager.connect(function(err, server) {
if (err) return callback(err, null);
// Avoid double calls
var done = false;
// Execute find
var cursor = server.cursor('local.system.replset', {
find: 'local.system.replset',
query: {}
}).next(function(err, d) {
if (err) return callback(err, null);
if (done) return;
done = true;
// Create a new members entry
d.members.push(serverDetails);
d.version = d.version + 1;
// Set credentials
if (credentials) {
manager.setCredentials(credentials.provider, credentials.db, credentials.user, credentials.password);
}
// Get a connection to the master
manager.connect(function(err, server) {
if (err) return callback(err, null);
// Reconfigure the replicaset
server.command('admin.$cmd', {
replSetReconfig: d
}, function(err, result) {
if (err) return callback(err);
callback(null, result.result);
});
});
});
});
});
};
this.stepDown = function(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
// Clone the options
options = clone(options);
// Unpack all the options
options.replSetStepDown = options.replSetStepDown || 60;
options.force = typeof options.force == 'boolean' ? options.force : false;
options.avoidElectionFor = typeof options.avoidElectionFor == 'number'
? typeof options.avoidElectionFor : 10;
options.force = typeof options.force === 'boolean' ? options.force : false;
// Get is master from one of the servers
getIsMaster(function(err, ismaster) {
if (err) return callback(err);
// Get a live connection to the primary
getServerManager(ismaster.primary, function(err, manager) {
if (err) return callback(err);
// Set credentials
if (credentials) {
manager.setCredentials(credentials.provider, credentials.db, credentials.user, credentials.password);
}
// Get a new connection
manager.connect(function(err, server) {
if (err) return callback(err);
// Execute step down
server.command('admin.$cmd', {
replSetStepDown: options.avoidElectionFor,
force: options.force
}, function(err, result) {
// Destroy the server
server.destroy();
callback(err, result);
});
});
});
});
};
};
module.exports = ReplSetManager;