
Figure out the console width and adjust output accordingly. In default mode use '\n' as separator (necessary for making the session query output sensible) and in tsv mode ','.
478 lines
14 KiB
JavaScript
478 lines
14 KiB
JavaScript
/*
|
|
* 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()
|
|
|
|
var separator
|
|
var max_length
|
|
|
|
if (this.argv.tsv) {
|
|
separator = ', '
|
|
max_length = Number.MAX_SAFE_INTEGER
|
|
} else {
|
|
separator = '\n'
|
|
var max_field_length = 0
|
|
fields.forEach(function (i) {
|
|
var k = Object.keys(i)[0]
|
|
if (k.length > max_field_length) {
|
|
max_field_length = k.length
|
|
}
|
|
})
|
|
max_field_length += 7 // Borders etc.
|
|
|
|
max_length = process.stdout.columns - max_field_length
|
|
if (max_length < 30) {
|
|
// Ignore excessively narrow terminals.
|
|
max_length = 30
|
|
}
|
|
}
|
|
|
|
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') {
|
|
if (separator == '\n') {
|
|
var s = ''
|
|
v.forEach(function (part) {
|
|
if (s.length) {
|
|
s = s + '\n'
|
|
}
|
|
if (part.length > max_length) {
|
|
part = part.substr(0, max_length - 3);
|
|
part = part + '...'
|
|
}
|
|
s = s + part;
|
|
});
|
|
v = s;
|
|
} else {
|
|
v = v.join(separator)
|
|
if (v.length > max_length) {
|
|
v = v.substr(0, max_length - 3);
|
|
v = v + '...'
|
|
}
|
|
}
|
|
} else if (typeof(v) == 'object') {
|
|
// We ignore max_length here.
|
|
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)
|
|
}
|