elasticsearch-watchdog
Version:
A watchdog of elasticsearch - cluster nodes' statuses monitor, auto restart, keep PRIMARY node unique.
326 lines (301 loc) • 8.92 kB
JavaScript
var crypto = require('crypto'),
_ = require('lodash'),
url = require('url'),
path = require('path'),
net = require('net'),
fs = require('fs'),
YAML = require('yamljs');
/**
* Utility.
* @type {Object}
*/
var helper = module.exports = {
// encrypt key.
_SECRET: 'ES-WATCHDOG',
// daemonic process script (relative to current directory).
_DOG : 'dog.js',
// persistent location.
get ROOT(){
if (this._root) {
return this._root;
}
this._root = path.resolve(process.env.WATCHDOG_HOME || process.env.HOME || process.env.HOMEPATH, '.watchdog');
!fs.existsSync(this._root) && fs.mkdirSync(this._root);
return this._root;
},
// IP addresses of current machine.
get ips(){
if (this._ips) {
return this._ips;
}
return this._ips = _.chain(require('os').networkInterfaces())
.values()
.flatten()
.filter(function(inf){
return inf.family == 'IPv4' && !inf.internal;
})
.pluck('address')
.uniq()
.value();
}
};
/**
* Load configuration from yaml.
* @param {String} file
* @return {*}
*/
helper.loadConfig = function(file){
// load configuration file.
var conf;
if (typeof file == 'string') {
try {
conf = YAML.load(file);
}
catch (err) {
throw err;
}
// deep clone.
for (var key in conf) {
this._deserializeObject(key, conf[key], conf);
!!~key.indexOf('.') && (delete conf[key]);
}
/** ELASTICSEARCH **/
// make sure `available` exists.
conf.elasticsearch = conf.elasticsearch || {};
conf.elasticsearch.autorestart = (typeof conf.elasticsearch.autorestart != 'boolean' ? true : !!conf.elasticsearch.autorestart);
// status
var status = conf.elasticsearch.status;
if (status && _.isArray(status) && status.length > 0) {
for (var i = status.length - 1; i >= 0; i--) {
var stt = status[i].toLowerCase();
if (!~['red', 'yellow', 'green'].indexOf(stt)) {
status.splice(i, 1);
}
}
} else {
status = null;
}
(!status || status.length == 0) && (status = ['yellow', 'green']);
conf.elasticsearch.status = status;
// primary strategy
var primary = conf.elasticsearch.primary;
if (!primary || (!net.isIP(primary) && primary != 'MS2M')) {
primary = 'MS2M';
}
conf.elasticsearch.primary = primary.toUpperCase();
// elasticsearch delay.
conf.elasticsearch.delay = this._parseTime(conf.elasticsearch.delay, 5e3);
/** NODES **/
if (conf.elasticsearch.autorestart && (!conf.nodes || !_.isArray(conf.nodes.elasticsearch) || !_.isArray(conf.nodes.ssh) || conf.nodes.elasticsearch.length != conf.nodes.ssh.length)) {
throw new Error('Both `nodes.elasticsearch` and `nodes.ssh` must be instances of Array and should keep the same lengths.');
}
// recombine nodes.
var nodes = {};
conf.nodes.elasticsearch.forEach(function(host){
var URL = url.parse('http://' + host);
// make sure port exists.
!URL.port && (URL.port = 9200);
// new node with both elasticsearch and ssh settings.
var node = {elasticsearch: URL.protocol + '//' + URL.hostname + ':' + URL.port};
for (var i = conf.nodes.ssh.length - 1; i >= 0; i--) {
var ssh = conf.nodes.ssh[i];
if (ssh.host == URL.hostname) {
node.ssh = ssh;
conf.nodes.ssh.splice(i, 1);
break;
}
}
if (!node.ssh) {
throw new Error('An OpenSSH connection for `' + URL.host + '` is required!');
}
if (!node.ssh.es_stop || !node.ssh.es_start) {
throw new Error('ElasticSearch `es_start` and `es_stop` command are both required!')
}
nodes[URL.hostname] = node;
});
conf.nodes = nodes;// make sure `name` exists.
/** WATCHDOG **/
if (!conf.watchdog || !conf.watchdog.name) {
throw new Error('The `name` of WATCHDOG must be provided!');
}
if (conf.watchdog.frequency) {
if (!~['low', 'medium', 'high', 'critical'].indexOf(conf.watchdog.frequency)) {
conf.watchdog.frequency = 'medium';
}
} else {
conf.watchdog.frequency = 'medium';
}
/** HTTP **/
// make sure `http` property exists.
conf.http = conf.http || {};
// http wait.
conf.http.wait = this._parseTime(conf.http.wait, 1200e3);
// http timeout.
conf.http.timeout = this._parseTime(conf.http.timeout, 10e3);
// http delay.
conf.http.delay = this._parseTime(conf.http.delay, 5e3);
// retry times
if (isNaN(conf.http.retry)) {
conf.http.retry = 3;
} else if (typeof conf.http.retry == 'string') {
conf.http.retry = parseInt(conf.http.retry);
}
} else if (typeof file == 'object') {
conf = file;
} else {
throw new Error('`conf` could only be the file path or object of configuration')
}
return conf;
}
/**
* Encrypt configuration to make sure password is not a plain text.
* @param {String} file
* @param {Boolean} keepBlank
*/
helper.encryptConfig = function(file, keepBlank){
fs.writeFileSync(file, _(fs.readFileSync(file, {encoding: 'utf-8'}).split('\n'))
.remove(function(line){
return keepBlank ? true : line.trim().length > 0;
})
.map(function(line){
if (!line) {
return '';
}
var regex = /\bpassword:\s*([\s\S]+)/,
matched;
// fetch password in configuration.
if ((matched = line.match(regex)) && matched.length == 2) {
var password = matched[1];
try {
// make sure password was encrypted or not.
helper.decrypt(password);
} catch (ex) {
// if not, try to encrypt it.
var encrypt_password = helper.encrypt(password);
return line.replace(password, encrypt_password);
}
}
return line;
}).value().join('\n'));
};
/**
* Encrypt string with secret.
* @param {String} str the string to be encrypted.
* @param {String} secret secret string.
* @return {String}
*/
helper.encrypt = function(str){
var cipher = crypto.createCipher('aes-256-ecb', this._SECRET);
var enc = cipher.update(str, 'utf8', 'hex');
enc += cipher.final('hex');
return enc;
};
/**
* decrypt password with secret
* @param str the string to be decrypted.
* @param secret secret secret string.
* @return {*|Progress|Progress}
*/
helper.decrypt = function(str){
var decipher = crypto.createDecipher('aes-256-ecb', this._SECRET);
var dec = decipher.update(str, 'hex', 'utf8');
dec += decipher.final('utf8');
return dec;
};
/**
* Parse time from descriptive to milliseconds.
* @param {String} time time string.
* @param {Integer} def default time.
* @private
*/
helper._parseTime = function(time, def){
if (!time) {
return def;
}
if (!isNaN(time)) {
return parseInt(time);
}
if (isNaN(time) && time.length >= 2) {
var d = time.substr(0, time.length - 1);
if (isNaN(d)) {
return def;
}
var f = time.substr(time.length - 1);
switch (f.toLocaleLowerCase()) {
case 'h':
return d * 3600e3;
case 'm':
return d * 60e3;
case 's':
return d * 1e3;
default:
return def;
}
}
return def;
};
/**
* Deserialize object, e.g.:
* nodes.elasticsearch: ["192.168.1.1"]
* will be:
* {
* nodes: {
* elasticsearch: ["192.168.1.1"]
* }
* }
* @param {String} key
* @param {Object} value
* @param {Object} output
* @private
*/
helper._deserializeObject = function(key, value, output){
var index = key.indexOf('.');
if (!~index) {
output[key] = value;
return;
}
var ck = key.substr(0, index), nk = key.substr(index + 1);
this._deserializeObject(nk, value, output[ck] = output[ck] || {});
};
/**
* Remove useless data files.
* @param {Array} processes
* @private
*/
helper._cleanHouse = function(processes, cleanupIncluded){
var persistents = (!processes || processes.length == 0) ? [] : processes.map(function(p){
return p.args.slice(1).join('.') + '.json';
});
var dataDir = path.resolve(helper.ROOT, 'data');
if (!fs.existsSync(dataDir)) {
return;
}
try {
fs.readdirSync(dataDir).forEach(function(file){
var shouldCleanUp = cleanupIncluded ? !!~persistents.indexOf(file) : !~persistents.indexOf(file);
if (shouldCleanUp) {
try {
fs.unlinkSync(dataDir + '/' + file);
}
catch (err) {
}
}
});
}
catch (err) {
}
}
/**
* Random a series number for puppy.
* @return {*}
* @private
*/
helper._randomNo = function(){
var number = _.random(1000, 9999),
dataPath = path.resolve(helper.ROOT, 'data', number + '.json');
while (fs.existsSync(dataPath)) {
number = _.random(1000, 9999);
dataPath = path.resolve(helper.ROOT, 'data', number + '.json');
}
return number;
};