MXS-1300: Add cluster sync command

The `cluster sync` command synchronizes a MaxScale cluster with one
server. This allows new MaxScale instances to be synchronized with an old
instance by using the same configuration file and performing a cluster
sync.
This commit is contained in:
Markus Mäkelä 2017-07-31 13:44:42 +03:00
parent fad1b4e3e0
commit 8fb4532468

View File

@ -29,59 +29,78 @@ 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)
}
// 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() {
return Promise.resolve([src, dest])
})
}
// Check if the diffs add or delete services
function haveExtraServices(src, dest) {
var newObj = getDifference(src.services.data, dest.services.data)
var oldObj = getDifference(dest.services.data, src.services.data)
return newObj.length > 0 || oldObj.length > 0
}
exports.command = 'cluster <command>'
exports.desc = 'Cluster objects'
exports.handler = function() {}
exports.builder = function(yargs) {
yargs
.command('diff <a> <b>', 'Show differences of <a> when compared with <b>. ' +
'Values must be in HOST:PORT format', {}, function(argv) {
// Sort of a hack-ish way to force only one iteration of this command
argv.hosts = [argv.a]
.command('diff <target>', 'Show difference between host servers and <target>. ' +
'Value must be in HOST:PORT format', {}, function(argv) {
maxctrl(argv, function(host) {
const collections = [
'servers',
'monitors',
'services',
'users'
]
const endpoints = [
'maxscale',
'maxscale/logs'
]
return getDiffs(host, argv.target)
.then(function(diffs) {
var src = diffs[0]
var dest = diffs[1]
var src = {}
var dest = {}
var promises = []
collections.forEach(function(i) {
promises.push(doAsyncRequest(argv.b, i, function(res) {
dest[i] = res
}))
promises.push(doAsyncRequest(argv.a, i, function(res) {
src[i] = res
}))
})
endpoints.forEach(function(i) {
promises.push(doAsyncRequest(argv.b, i, function(res) {
dest[i] = res
}))
promises.push(doAsyncRequest(argv.a, i, function(res) {
src[i] = res
}))
})
return Promise.all(promises)
.then(function() {
collections.forEach(function(i) {
var newObj = getDifference(src[i].data, dest[i].data)
var oldObj = getDifference(dest[i].data, src[i].data)
@ -101,9 +120,8 @@ exports.builder = function(yargs) {
}
})
endpoints.forEach(function(i) {
// Treating the resource endpoints as arrays allows
// the same functions to be used to compare
// individual resources and resource collections
// 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) {
logger.log("Changed:", i)
@ -113,6 +131,79 @@ exports.builder = function(yargs) {
})
})
})
.command('sync <target>', 'Synchronize the cluster with target MaxScale server.', {}, function(argv) {
maxctrl(argv, function(host) {
// TODO: Create new listeners
return getDiffs(argv.target, host)
.then(function(diffs) {
var promises = []
var src = diffs[0]
var dest = diffs[1]
if (haveExtraServices(src, dest)) {
return error('Cannot synchronize host `' + host + '` with target `' +
argv.target + '`: New or deleted services on target host, ' +
'both hosts must have the same services.')
}
// 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
delete i.relationships
promises.push(doAsyncRequest(host, 'servers', null, {method: 'POST', body: {data: i}}))
})
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, 'servers', null, {method: 'POST', body: {data: i}}))
})
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) {