
The yargs framework combined with the pkg packaging causes the executable name to be mangled on installation. For this reason, the usage should be explicitly added to each command.
293 lines
14 KiB
JavaScript
293 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: 2020-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.
|
|
*/
|
|
require('./common.js')()
|
|
var colors = require('colors/safe')
|
|
|
|
function equalResources(oldVal, newVal) {
|
|
return _.isEqual(_.get(oldVal, 'attributes.parameters'), _.get(newVal, 'attributes.parameters')) &&
|
|
_.isEqual(_.get(oldVal, 'relationships.servers.data'), _.get(newVal, 'relationships.servers.data')) &&
|
|
_.isEqual(_.get(oldVal, 'relationships.services.data'), _.get(newVal, 'relationships.services.data')) &&
|
|
_.isEqual(_.get(oldVal, 'relationships.monitors.data'), _.get(newVal, 'relationships.monitors.data'))
|
|
}
|
|
|
|
function sameResource(oldVal, newVal) {
|
|
return oldVal.id == newVal.id
|
|
}
|
|
|
|
// Return objets that are in <a> but not in <b>
|
|
function getDifference(a, b) {
|
|
return _.differenceWith(a, b, sameResource)
|
|
}
|
|
|
|
// Return a list of objects that differ from each other
|
|
function getChangedObjects(a, b) {
|
|
var ours = _.intersectionWith(a, b, sameResource)
|
|
var theirs = _.intersectionWith(b, a, sameResource)
|
|
return _.differenceWith(ours, theirs, equalResources)
|
|
}
|
|
|
|
// Check if the diffs add or delete services
|
|
function haveExtraServices(src, dest, srcHost, destHost) {
|
|
var newObj = getDifference(src.services.data, dest.services.data)
|
|
var oldObj = getDifference(dest.services.data, src.services.data)
|
|
|
|
if (newObj.length > 0 || oldObj.length > 0) {
|
|
const EOL = require('os').EOL
|
|
|
|
var srcObj = _.transform(newObj, function(out, i) {
|
|
out.push(i.id)
|
|
})
|
|
var destObj = _.transform(oldObj, function(out, i) {
|
|
out.push(i.id)
|
|
})
|
|
err = ['Cannot diff host `' + srcHost + '` with target `' +
|
|
destHost + '`: New or deleted services on target host, ' +
|
|
'both hosts must have the same services.',
|
|
'Extra services on `' + srcHost + '`:',
|
|
JSON.stringify(srcObj, null, 4),
|
|
'Extra services on `' + destHost + '`:',
|
|
JSON.stringify(destObj, null, 4)
|
|
]
|
|
return error(err.join(EOL))
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
// Resource collections
|
|
const collections = [
|
|
'servers',
|
|
'monitors',
|
|
'services',
|
|
'users'
|
|
]
|
|
|
|
// Individual resources
|
|
const endpoints = [
|
|
'maxscale',
|
|
'maxscale/logs'
|
|
]
|
|
|
|
// Calculate a diff between two MaxScale servers
|
|
function getDiffs(a, b) {
|
|
var src = {}
|
|
var dest = {}
|
|
var promises = []
|
|
|
|
collections.forEach(function(i) {
|
|
promises.push(doAsyncRequest(b, i, function(res) {
|
|
dest[i] = res
|
|
}))
|
|
promises.push(doAsyncRequest(a, i, function(res) {
|
|
src[i] = res
|
|
}))
|
|
})
|
|
|
|
endpoints.forEach(function(i) {
|
|
promises.push(doAsyncRequest(b, i, function(res) {
|
|
dest[i] = res
|
|
}))
|
|
promises.push(doAsyncRequest(a, i, function(res) {
|
|
src[i] = res
|
|
}))
|
|
})
|
|
|
|
return Promise.all(promises)
|
|
.then(function() {
|
|
// We can get the listeners only after we've requested the services
|
|
dest.services.data.forEach(function(i) {
|
|
dest['services/' + i.id + '/listeners'] = { data: i.attributes.listeners }
|
|
})
|
|
src.services.data.forEach(function(i) {
|
|
src['services/' + i.id + '/listeners'] = { data: i.attributes.listeners }
|
|
})
|
|
})
|
|
.then(function() {
|
|
return Promise.resolve([src, dest])
|
|
})
|
|
}
|
|
|
|
exports.haveExtraServices = haveExtraServices
|
|
exports.getDifference = getDifference
|
|
exports.getChangedObjects = getChangedObjects
|
|
exports.command = 'cluster <command>'
|
|
exports.desc = 'Cluster objects'
|
|
exports.handler = function() {}
|
|
exports.builder = function(yargs) {
|
|
yargs
|
|
.command('diff <target>', 'Show difference between host servers and <target>.', function(yargs) {
|
|
return yargs.epilog('The list of host servers is controlled with the --hosts option. ' +
|
|
'The target server should not be in the host list. Value of <target> ' +
|
|
'must be in HOST:PORT format')
|
|
.usage('Usage: cluster diff <target>')
|
|
}, function(argv) {
|
|
|
|
maxctrl(argv, function(host) {
|
|
return getDiffs(host, argv.target)
|
|
.then(function(diffs) {
|
|
var output = []
|
|
var src = diffs[0]
|
|
var dest = diffs[1]
|
|
|
|
var err = haveExtraServices(src, dest, host, argv.target)
|
|
if (err) {
|
|
return err
|
|
}
|
|
|
|
_.uniq(_.concat(Object.keys(src), Object.keys(dest))).forEach(function(i) {
|
|
var newObj = getDifference(src[i].data, dest[i].data)
|
|
var oldObj = getDifference(dest[i].data, src[i].data)
|
|
var changed = getChangedObjects(src[i].data, dest[i].data)
|
|
|
|
if (newObj.length) {
|
|
output.push("New:", i)
|
|
output.push(colors.green(JSON.stringify(newObj, null, 4)))
|
|
}
|
|
if (oldObj.length) {
|
|
output.push("Deleted:", i)
|
|
output.push(colors.red(JSON.stringify(oldObj, null, 4)))
|
|
}
|
|
if (changed.length) {
|
|
output.push("Changed:", i)
|
|
output.push(colors.yellow(JSON.stringify(changed, null, 4)))
|
|
}
|
|
})
|
|
endpoints.forEach(function(i) {
|
|
// Treating the resource endpoints as arrays allows the same functions to be used
|
|
// to compare individual resources and resource collections
|
|
var changed = getChangedObjects([src[i].data], [dest[i].data])
|
|
if (changed.length) {
|
|
output.push("Changed:", i)
|
|
output.push(colors.yellow(JSON.stringify(changed, null, 4)))
|
|
}
|
|
})
|
|
|
|
return output.join(require('os').EOL)
|
|
})
|
|
})
|
|
})
|
|
.command('sync <target>', 'Synchronize the cluster with target MaxScale server.', function(yargs) {
|
|
return yargs.epilog('This command will alter all MaxScale instances given in the --hosts ' +
|
|
'option to represent the <target> MaxScale. If the synchronization of ' +
|
|
'a MaxScale instance fails, it will be disabled by executing the `stop maxscale` ' +
|
|
'command on that instance. Synchronization can be attempted again if a previous ' +
|
|
'attempt failed due to a network failure or some other ephemeral error. Any other ' +
|
|
'errors require manual synchronization of the MaxScale configuration files and a ' +
|
|
'restart of the failed Maxscale.')
|
|
.usage('Usage: cluster sync <target>')
|
|
}, function(argv) {
|
|
maxctrl(argv, function(host) {
|
|
return getDiffs(argv.target, host)
|
|
.then(function(diffs) {
|
|
var promises = []
|
|
var src = diffs[0]
|
|
var dest = diffs[1]
|
|
|
|
var err = haveExtraServices(src, dest, host, argv.target)
|
|
if (err) {
|
|
return err
|
|
}
|
|
|
|
// Delete old servers
|
|
getDifference(dest.servers.data, src.servers.data).forEach(function(i) {
|
|
// First unlink the servers from all services and monitors
|
|
promises.push(
|
|
doAsyncRequest(host, 'servers/' + i.id, null, {method: 'PATCH', body: _.set({}, 'data.relationships', {})})
|
|
.then(function() {
|
|
return doAsyncRequest(host, 'servers/' + i.id, null, {method: 'DELETE'})
|
|
}))
|
|
})
|
|
|
|
// Add new servers
|
|
getDifference(src.servers.data, dest.servers.data).forEach(function(i) {
|
|
// Create the servers without relationships, those are generated when services and
|
|
// monitors are updated
|
|
var newserv = {
|
|
data: {
|
|
id: i.id,
|
|
type: i.type,
|
|
attributes: {
|
|
parameters: i.attributes.parameters
|
|
}
|
|
}
|
|
}
|
|
promises.push(doAsyncRequest(host, 'servers', null, {method: 'POST', body: newserv}))
|
|
})
|
|
return Promise.all(promises)
|
|
.then(function() {
|
|
var promises = []
|
|
// Delete old monitors
|
|
getDifference(dest.monitors.data, src.monitors.data).forEach(function(i) {
|
|
promises.push(
|
|
doAsyncRequest(host, 'monitors/' + i.id, null, {
|
|
method: 'PATCH', body: _.set({}, 'data.relationships', {})
|
|
})
|
|
.then(function() {
|
|
return doAsyncRequest(host, 'monitors/' + i.id, null, {method: 'DELETE'})
|
|
}))
|
|
})
|
|
return Promise.all(promises)
|
|
})
|
|
.then(function() {
|
|
var promises = []
|
|
// Add new monitors
|
|
getDifference(src.monitors.data, dest.monitors.data).forEach(function(i) {
|
|
promises.push(doAsyncRequest(host, 'monitors', null, {method: 'POST', body: {data: i}}))
|
|
})
|
|
return Promise.all(promises)
|
|
})
|
|
.then(function() {
|
|
// Add new and delete old listeners
|
|
var promises = []
|
|
var all_keys = _.concat(Object.keys(src), Object.keys(dest))
|
|
var unwanted_keys = _.concat(collections, endpoints)
|
|
var relevant_keys = _.uniq(_.difference(all_keys, unwanted_keys))
|
|
|
|
relevant_keys.forEach(function(i) {
|
|
getDifference(dest[i].data, src[i].data).forEach(function(j) {
|
|
promises.push(doAsyncRequest(host, i + '/' + j.id, null, {method: 'DELETE'}))
|
|
})
|
|
getDifference(src[i].data, dest[i].data).forEach(function(j) {
|
|
promises.push(doAsyncRequest(host, i, null, {method: 'POST', body: {data: j}}))
|
|
})
|
|
})
|
|
|
|
return Promise.all(promises)
|
|
})
|
|
.then(function() {
|
|
var promises = []
|
|
// PATCH all remaining resource collections in src from dest apart from the
|
|
// user resource as it requires passwords to be entered
|
|
_.difference(collections, ['users']).forEach(function(i) {
|
|
src[i].data.forEach(function(j) {
|
|
promises.push(doAsyncRequest(host, i + '/' + j.id, null, {method: 'PATCH', body: {data: j}}))
|
|
})
|
|
})
|
|
// Do the same for individual resources
|
|
endpoints.forEach(function(i) {
|
|
promises.push(doAsyncRequest(host, i, null, {method: 'PATCH', body: dest[i]}))
|
|
})
|
|
return Promise.all(promises)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
.usage('Usage: cluster <command>')
|
|
.help()
|
|
.command('*', 'the default command', {}, function(argv) {
|
|
maxctrl(argv, function() {
|
|
return error('Unknown command. See output of `help cluster` for a list of commands.')
|
|
})
|
|
})
|
|
}
|