UNPKG

elasticsearch-watchdog

Version:

A watchdog of elasticsearch - cluster nodes' statuses monitor, auto restart, keep PRIMARY node unique.

326 lines (301 loc) 8.92 kB
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; };