MaxScale/maxctrl/lib/cluster.js
Markus Mäkelä f102a563e9 Add explicit usage to each command
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.
2017-09-28 12:40:51 +03:00

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.')
})
})
}