484 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			484 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 == '') {
 | 
						|
            if (process.stdin.isTTY) {
 | 
						|
                argv.p = readlineSync.question('Enter password: ', {
 | 
						|
                    hideEchoBack: true
 | 
						|
                })
 | 
						|
            } else {
 | 
						|
                var line = fs.readFileSync(0)
 | 
						|
                argv.p = line.toString().trim()
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        // 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 + 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.auth = {user: argv.u, pass: argv.p}
 | 
						|
        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)
 | 
						|
}
 |