/* * 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: 2022-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 but not in 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 ' exports.desc = 'Cluster objects' exports.handler = function() {} exports.builder = function(yargs) { yargs .command('diff ', 'Show difference between host servers and .', 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 ' + 'must be in HOST:PORT format') .usage('Usage: cluster diff ') }, 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 ', '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 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 ') }, 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 ') .help() .command('*', 'the default command', {}, function(argv) { maxctrl(argv, function() { return error('Unknown command. See output of `help cluster` for a list of commands.') }) }) }