From 8fb45324684c54adfd2cf2f0a3ce5c080ca20f3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Mon, 31 Jul 2017 13:44:42 +0300 Subject: [PATCH] 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. --- maxctrl/lib/cluster.js | 175 +++++++++++++++++++++++++++++++---------- 1 file changed, 133 insertions(+), 42 deletions(-) diff --git a/maxctrl/lib/cluster.js b/maxctrl/lib/cluster.js index 362b91a7f..8fb55fd13 100644 --- a/maxctrl/lib/cluster.js +++ b/maxctrl/lib/cluster.js @@ -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 ' exports.desc = 'Cluster objects' exports.handler = function() {} exports.builder = function(yargs) { yargs - .command('diff ', 'Show differences of when compared with . ' + - '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 ', 'Show difference between host servers and . ' + + '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 ', '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 ') .help() .command('*', 'the default command', {}, function(argv) {