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