494 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			494 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: 2023-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)
 | |
|     }
 | |
| 
 | |
|     this.rDnsOption = {
 | |
|         shortname: 'rdns',
 | |
|         optionOn: 'rdns=true',
 | |
|         definition : {
 | |
|             describe: 'Reverse DNS on client IP. May slow MaxScale down.',
 | |
|             type: 'bool',
 | |
|             default: false
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| 
 | |
| //
 | |
| // 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)
 | |
| }
 | 
