/* * Copyright (c) 2016 MariaDB Corporation Ab * * Use of this software is governed by the Business Source License included * in the LICENSE.TXT file and at www.mariadb.com/bsl11. * * Change Date: 2022-01-01 * * On the date above, in accordance with the Business Source License, use * of this software will be governed by version 2 or later of the General * Public License. */ var request = require('request-promise-native'); var colors = require('colors/safe'); var Table = require('cli-table'); var consoleLib = require('console') var os = require('os') var fs = require('fs') var readlineSync = require('readline-sync') function normalizeWhitespace(table) { table.forEach((v) => { if (Array.isArray(v)) { // `table` is an array of arrays v.forEach((k) => { if (typeof(v[k]) == 'string') { v[k] = v[k].replace( /\s+/g, ' ') } }) } else if (!Array.isArray(v) && v instanceof Object) { // `table` is an array of objects Object.keys(v).forEach((k) => { if (typeof(v[k]) == 'string') { v[k] = v[k].replace( /\s+/g, ' ') } }) } }) } module.exports = function() { this._ = require('lodash-getpath') // The main entry point into the library. This function is used to do // cluster health checks and to propagate the commands to multiple // servers. this.maxctrl = function(argv, cb) { // No password given, ask it from the command line if (argv.p == '') { argv.p = readlineSync.question('Enter password: ', { hideEchoBack: true }) } // Split the hostnames, separated by commas argv.hosts = argv.hosts.split(',') this.argv = argv if (!argv.hosts || argv.hosts.length < 1) { argv.reject("No hosts defined") } return pingCluster(argv.hosts) .then(function() { var promises = [] var rval = [] argv.hosts.forEach(function(i) { promises.push(cb(i) .then(function(output) { if (argv.hosts.length > 1) { rval.push(colors.yellow(i)) } rval.push(output) })) }) return Promise.all(promises) .then(function() { argv.resolve(argv.quiet ? undefined : rval.join(os.EOL)) }, function(err) { argv.reject(err) }) }, function(err) { if (err.error.cert) { // TLS errors cause extremely verbose errors, truncate the certifiate details // from the error output delete err.error.cert } // One of the HTTP request pings to the cluster failed, log the error argv.reject(JSON.stringify(err.error, null, 4)) }) } // Filter and format a JSON API resource from JSON to a table this.filterResource = function (res, fields) { table = [] res.data.forEach(function(i) { row = [] fields.forEach(function(p) { var v = _.getPath(i, p[Object.keys(p)[0]], '') if (Array.isArray(v)) { v = v.join(', ') } row.push(v) }) table.push(row) }) return table } // Convert a table that was generated from JSON into a string this.tableToString = function(table) { if (this.argv.tsv) { // Convert whitespace into spaces to prevent breaking the TSV format normalizeWhitespace(table) } str = table.toString() if (this.argv.tsv) { // Based on the regex found in: https://github.com/jonschlinkert/strip-color str = str.replace( /\x1B\[[(?);]{0,2}(;?\d)*./g, '') } return str } // Get a resource as raw collection; a matrix of strings this.getRawCollection = function (host, resource, fields) { return getJson(host, resource) .then((res) => filterResource(res, fields)) } // Convert the raw matrix of strings into a formatted string this.rawCollectionAsTable = function (arr, fields) { var header = [] fields.forEach(function(i) { header.push(Object.keys(i)) }) var table = getTable(header) arr.forEach((row) => { table.push(row) }) return tableToString(table) } // Request a resource collection and format it as a string this.getCollection = function (host, resource, fields) { return getRawCollection(host, resource, fields) .then((res) => rawCollectionAsTable(res, fields)) } // Request a part of a resource as a collection and return it as a string this.getSubCollection = function (host, resource, subres, fields) { return doRequest(host, resource, function(res) { var header = [] fields.forEach(function(i) { header.push(Object.keys(i)) }) var table = getTable(header) _.getPath(res.data, subres, []).forEach(function(i) { row = [] fields.forEach(function(p) { var v = _.getPath(i, p[Object.keys(p)[0]], '') if (Array.isArray(v) && typeof(v[0]) != 'object') { v = v.join(', ') } else if (typeof(v) == 'object') { v = JSON.stringify(v, null, 4) } row.push(v) }) table.push(row) }) return tableToString(table) }) } // Format and filter a JSON object into a string by using a key-value list this.formatResource = function (fields, data) { var table = getList() fields.forEach(function(i) { var k = Object.keys(i)[0] var path = i[k] var v = _.getPath(data, path, '') if (Array.isArray(v) && typeof(v[0]) != 'object') { v = v.join(', ') } else if (typeof(v) == 'object') { v = JSON.stringify(v, null, 4) } var o = {} o[k] = v table.push(o) }) return tableToString(table) } // Request a single resource and format it with a key-value list this.getResource = function (host, resource, fields) { return doRequest(host, resource, (res) => { return formatResource(fields, res.data) }) } // Perform a getResource on a collection of resources and return it in string format this.getCollectionAsResource = function(host, resource, fields) { return doRequest(host, resource, (res) => { //return formatResource(fields, res.data[0]) return res.data.map((i) => formatResource(fields, i)).join('\n') }) } // Perform a PATCH on a resource this.updateValue = function(host, resource, key, value) { var body = {} // Convert string booleans into JSON booleans if (value == "true") { value = true } else if (value == "false") { value = false } _.set(body, key, value) return doRequest(host, resource, null, { method: 'PATCH', body: body }) } // Helper for converting endpoints to acutal URLs this.getUri = function(host, secure, endpoint) { var base = 'http://' if (secure) { base = 'https://' } return base + argv.u + ':' + argv.p + '@' + host + '/v1/' + endpoint } // Return an OK message this.OK = function() { return Promise.resolve(colors.green('OK')) } // Set TLS certificates this.setTlsCerts = function(args) { args.agentOptions = {} if (this.argv['tls-key']) { args.agentOptions.key = fs.readFileSync(this.argv['tls-key']) } if (this.argv['tls-cert']) { args.agentOptions.cert = fs.readFileSync(this.argv['tls-cert']) } if (this.argv['tls-ca-cert']) { args.agentOptions.ca = fs.readFileSync(this.argv['tls-ca-cert']) } if (this.argv['tls-passphrase']) { args.agentOptions.passphrase = this.argv['tls-passphrase'] } if (!this.argv['tls-verify-server-cert']) { args.agentOptions.checkServerIdentity = function() { } } } // Helper for executing requests and handling their responses, returns a // promise that is fulfilled when all requests successfully complete. The // promise is rejected if any of the requests fails. this.doAsyncRequest = function(host, resource, cb, obj) { args = obj || {} args.uri = getUri(host, this.argv.secure, resource) args.json = true args.timeout = this.argv.timeout setTlsCerts(args) return request(args) .then(function(res) { if (res && cb) { // Request OK, returns data return cb(res) } else { // Request OK, no data or data is ignored return OK() } }, function(err) { if (err.response && err.response.body) { return error('Server at '+ err.response.request.uri.host +' responded with status code ' + err.statusCode + ' to `' + err.response.request.method +' ' + resource + '`:' + JSON.stringify(err.response.body, null, 4)) } else if (err.statusCode) { return error('Server at '+ err.response.request.uri.host +' responded with status code ' + err.statusCode + ' to `' + err.response.request.method +' ' + resource + '`') } else if (err.error) { return error(JSON.stringify(err.error, null, 4)) } else { return error('Undefined error: ' + JSON.stringify(err, null, 4)) } }) } // Perform a request, alias for doAsyncRequest this.doRequest = function(host, resource, cb, obj) { return doAsyncRequest(host, resource, cb, obj) } // Perform a request and return the resulting JSON as a promise this.getJson = function(host, resource) { return doAsyncRequest(host, resource, (res) => { return res }) } // Return an error message as a rejected promise this.error = function(err) { return Promise.reject(colors.red('Error: ') + err) } } // // The following are mainly for internal use // var tsvopts = { chars: { 'top': '' , 'top-mid': '' , 'top-left': '' , 'top-right': '' , 'bottom': '' , 'bottom-mid': '' , 'bottom-left': '' , 'bottom-right': '' , 'left': '' , 'left-mid': '' , 'mid': '' , 'mid-mid': '' , 'right': '' , 'right-mid': '' , 'middle': ' ' }, style: { 'padding-left': 0, 'padding-right': 0, compact: true }, } function getList() { var opts = { style: { head: ['cyan'] } } if (this.argv.tsv) { opts = _.assign(opts, tsvopts) } return new Table(opts) } // Creates a table-like array for output. The parameter is an array of header names function getTable(headobj) { for (i = 0; i < headobj.length; i++) { headobj[i] = colors.cyan(headobj[i]) } var opts if (this.argv.tsv) { opts = _.assign(opts, tsvopts) } else { opts = { head: headobj } } return new Table(opts) } function pingCluster(hosts) { var promises = [] if (hosts.length > 1 ) { hosts.forEach(function(i) { args = {} args.uri = getUri(i, this.argv.secure, '') args.json = true setTlsCerts(args) promises.push(request(args)) }) } return Promise.all(promises) }