/* jshint strict: false, unused: false */ /* global AQL_EXECUTE, SYS_CLUSTER_TEST ArangoServerState, ArangoClusterComm, ArangoClusterInfo, ArangoAgency */ // ////////////////////////////////////////////////////////////////////////////// // / @brief cluster actions // / // / @file // / // / DISCLAIMER // / // / Copyright 2014 ArangoDB GmbH, Cologne, Germany // / // / Licensed under the Apache License, Version 2.0 (the "License") // / you may not use this file except in compliance with the License. // / You may obtain a copy of the License at // / // / http://www.apache.org/licenses/LICENSE-2.0 // / // / Unless required by applicable law or agreed to in writing, software // / distributed under the License is distributed on an "AS IS" BASIS, // / WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // / See the License for the specific language governing permissions and // / limitations under the License. // / // / Copyright holder is ArangoDB GmbH, Cologne, Germany // / // / @author Max Neunhoeffer // / @author Copyright 2014, ArangoDB GmbH, Cologne, Germany // / @author Copyright 2014, ArangoDB GmbH, Cologne, Germany // / @author Copyright 2013-2014, triAGENS GmbH, Cologne, Germany // ////////////////////////////////////////////////////////////////////////////// var actions = require('@arangodb/actions'); var cluster = require('@arangodb/cluster'); //var internal = require('internal'); var _ = require('lodash'); var fetchKey = cluster.fetchKey; // ////////////////////////////////////////////////////////////////////////////// // / @brief was docuBlock JSF_cluster_test_GET // ////////////////////////////////////////////////////////////////////////////// // ////////////////////////////////////////////////////////////////////////////// // / @brief was docuBlock JSF_cluster_test_POST // ////////////////////////////////////////////////////////////////////////////// // ////////////////////////////////////////////////////////////////////////////// // / @brief was docuBlock JSF_cluster_test_PUT // ////////////////////////////////////////////////////////////////////////////// // ////////////////////////////////////////////////////////////////////////////// // / @brief was docuBlock JSF_cluster_test_DELETE // ////////////////////////////////////////////////////////////////////////////// // ////////////////////////////////////////////////////////////////////////////// // / @brief was docuBlock JSF_cluster_test_PATCH // ////////////////////////////////////////////////////////////////////////////// // ////////////////////////////////////////////////////////////////////////////// // / @brief was docuBlock JSF_cluster_test_HEAD // ////////////////////////////////////////////////////////////////////////////// actions.defineHttp({ url: '_admin/cluster/removeServer', allowUseDatabase: true, prefix: false, callback: function (req, res) { if (req.requestType !== actions.POST || !require('@arangodb/cluster').isCoordinator()) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only DELETE requests are allowed and only to coordinators'); return; } let serverId; try { serverId = JSON.parse(req.requestBody); } catch (e) { } if (typeof serverId !== 'string') { actions.resultError(req, res, actions.HTTP_BAD, 'required parameter ServerID was not given'); return; } let agency = ArangoAgency.get('', false, true).arango; let node = agency.Supervision.Health[serverId]; if (node === undefined) { actions.resultError(req, res, actions.HTTP_NOT_FOUND, 'unknown server id'); return; } if (node.Role !== 'Coordinator' && node.Role !== 'DBServer') { actions.resultError(req, res, actions.HTTP_BAD, 'unhandled role ' + node.role); return; } let preconditions = {}; preconditions['/arango/Supervision/Health/' + serverId + '/Status'] = {'old': 'FAILED'}; // need to make sure it is not responsible for anything if (node.Role === 'DBServer') { let used = []; preconditions = reducePlanServers(function(data, agencyKey, servers) { data[agencyKey] = {'old': servers}; if (servers.indexOf(serverId) !== -1) { used.push(agencyKey); } return data; }, {}); preconditions = reduceCurrentServers(function(data, agencyKey, servers) { data[agencyKey] = {'old': servers}; if (servers.indexOf(serverId) !== -1) { used.push(agencyKey); } return data; }, preconditions); if (used.length > 0) { actions.resultError(req, res, actions.HTTP_PRECONDITION_FAILED, 'the server is still in use at the following locations: ' + JSON.stringify(used)); return; } } let operations = {}; operations['/arango/Coordinators/' + serverId] = {'op': 'delete'}; operations['/arango/DBServers/' + serverId] = {'op': 'delete'}; operations['/arango/Current/ServersRegistered/' + serverId] = {'op': 'delete'}; operations['/arango/Supervision/Health/' + serverId] = {'op': 'delete'}; operations['/arango/MapUniqueToShortID/' + serverId] = {'op': 'delete'}; try { global.ArangoAgency.write([[operations, preconditions]]); } catch (e) { if (e.code === 412) { actions.resultError(req, res, actions.HTTP_PRECONDITION_FAILED, 'you can only remove failed servers'); return; } throw e; } actions.resultOk(req, res, actions.HTTP_OK, true); /*DBOnly: Current/Databases/YYY/XXX */ } }); actions.defineHttp({ url: '_admin/cluster-test', prefix: true, callback: function (req, res) { var path; if (req.hasOwnProperty('suffix') && req.suffix.length !== 0) { path = '/' + req.suffix.join('/'); } else { path = '/_admin/version'; } var params = ''; var shard = ''; var p; for (p in req.parameters) { if (req.parameters.hasOwnProperty(p)) { if (params === '') { params = '?'; } else { params += '&'; } params += p + '=' + encodeURIComponent(String(req.parameters[p])); } } if (params !== '') { path += params; } var headers = {}; var transID = ''; var timeout = 24 * 3600.0; var asyncMode = true; for (p in req.headers) { if (req.headers.hasOwnProperty(p)) { if (p === 'x-client-transaction-id') { transID = req.headers[p]; } else if (p === 'x-timeout') { timeout = parseFloat(req.headers[p]); if (isNaN(timeout)) { timeout = 24 * 3600.0; } } else if (p === 'x-synchronous-mode') { asyncMode = false; } else if (p === 'x-shard-id') { shard = req.headers[p]; } else { headers[p] = req.headers[p]; } } } var body; if (req.requestBody === undefined || typeof req.requestBody !== 'string') { body = ''; } else { body = req.requestBody; } var r; if (typeof SYS_CLUSTER_TEST === 'undefined') { actions.resultError(req, res, actions.HTTP_NOT_FOUND, 'Not compiled for cluster operation'); } else { try { r = SYS_CLUSTER_TEST(req, res, shard, path, transID, headers, body, timeout, asyncMode); if (r.timeout || typeof r.errorMessage === 'string') { res.responseCode = actions.HTTP_OK; res.contentType = 'application/json; charset=utf-8'; var s = JSON.stringify(r); res.body = s; } else { res.responseCode = actions.HTTP_OK; res.contentType = r.headers.contentType; res.headers = r.headers; res.body = r.body; } } catch (err) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, String(err)); } } } }); // ////////////////////////////////////////////////////////////////////////////// // / @brief was docuBlock JSF_cluster_statistics_GET // ////////////////////////////////////////////////////////////////////////////// actions.defineHttp({ url: '_admin/clusterStatistics', prefix: false, callback: function (req, res) { if (req.requestType !== actions.GET) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only GET requests are allowed'); return; } if (!require('@arangodb/cluster').isCoordinator()) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only allowed on coordinator'); return; } if (!req.parameters.hasOwnProperty('DBserver')) { actions.resultError(req, res, actions.HTTP_BAD, 'required parameter DBserver was not given'); return; } var DBserver = req.parameters.DBserver; var options = { timeout: 10 }; var op = ArangoClusterComm.asyncRequest('GET', 'server:' + DBserver, '_system', '/_admin/statistics', '', {}, options); var r = ArangoClusterComm.wait(op); res.contentType = 'application/json; charset=utf-8'; if (r.status === 'RECEIVED') { res.responseCode = actions.HTTP_OK; res.body = r.body; } else if (r.status === 'TIMEOUT') { res.responseCode = actions.HTTP_BAD; res.body = JSON.stringify({'error': true, 'errorMessage': 'operation timed out'}); } else { res.responseCode = actions.HTTP_BAD; var bodyobj; try { bodyobj = JSON.parse(r.body); } catch (err) {} res.body = JSON.stringify({'error': true, 'errorMessage': 'error from DBserver, possibly DBserver unknown', 'body': bodyobj}); } } }); // ////////////////////////////////////////////////////////////////////////////// // / @brief was docuBlock JSF_cluster_statistics_GET // ////////////////////////////////////////////////////////////////////////////// actions.defineHttp({ url: '_admin/cluster/health', allowUseDatabase: true, prefix: false, callback: function (req, res) { if (req.requestType !== actions.GET || !require('@arangodb/cluster').isCoordinator()) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only GET requests are allowed and only to coordinators'); return; } var timeout = 60.0; try { if (req.parameters.hasOwnProperty('timeout')) { timeout = Number(req.parameters.timeout); } } catch (e) {} var Health; try { Health = ArangoAgency.get('Supervision/Health', false, true).arango.Supervision.Health; } catch (e1) { actions.resultError(req, res, actions.HTTP_NOT_FOUND, 0, 'Failed to retrieve supervision node from agency!'); return; } actions.resultOk(req, res, actions.HTTP_OK, {Health}); } }); // ////////////////////////////////////////////////////////////////////////////// // / @brief allows to query the historic statistics of a DBserver in the cluster // ////////////////////////////////////////////////////////////////////////////// actions.defineHttp({ url: '_admin/history', prefix: false, callback: function (req, res) { if (req.requestType !== actions.POST) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only POST requests are allowed'); return; } var body = actions.getJsonBody(req, res); if (body === undefined) { return; } var DBserver = req.parameters.DBserver; // build query var figures = body.figures; var filterString = ' filter u.time > @startDate'; var bind = { startDate: (new Date().getTime() / 1000) - 20 * 60 }; if (cluster.isCoordinator() && !req.parameters.hasOwnProperty('DBserver')) { filterString += ' filter u.clusterId == @serverId'; bind.serverId = cluster.coordinatorId(); } var returnValue = ' return u'; if (figures) { returnValue = ' return { time : u.time, server : {uptime : u.server.uptime} '; var groups = {}; figures.forEach(function (f) { var g = f.split('.')[0]; if (!groups[g]) { groups[g] = []; } groups[g].push(f.split('.')[1] + ' : u.' + f); }); Object.keys(groups).forEach(function (key) { returnValue += ', ' + key + ' : {' + groups[key] + '}'; }); returnValue += '}'; } // allow at most ((60 / 10) * 20) * 2 documents to prevent total chaos var myQueryVal = 'FOR u in _statistics ' + filterString + ' LIMIT 240 SORT u.time' + returnValue; if (!req.parameters.hasOwnProperty('DBserver')) { // query the local statistics collection var cursor = AQL_EXECUTE(myQueryVal, bind); res.contentType = 'application/json; charset=utf-8'; if (cursor instanceof Error) { res.responseCode = actions.HTTP_BAD; res.body = JSON.stringify({'error': true, 'errorMessage': 'an error occurred'}); } res.responseCode = actions.HTTP_OK; res.body = JSON.stringify({result: cursor.docs}); } else { // query a remote statistics collection var options = { timeout: 10 }; var op = ArangoClusterComm.asyncRequest('POST', 'server:' + DBserver, '_system', '/_api/cursor', JSON.stringify({query: myQueryVal, bindVars: bind}), {}, options); var r = ArangoClusterComm.wait(op); res.contentType = 'application/json; charset=utf-8'; if (r.status === 'RECEIVED') { res.responseCode = actions.HTTP_OK; res.body = r.body; } else if (r.status === 'TIMEOUT') { res.responseCode = actions.HTTP_BAD; res.body = JSON.stringify({'error': true, 'errorMessage': 'operation timed out'}); } else { res.responseCode = actions.HTTP_BAD; var bodyobj; try { bodyobj = JSON.parse(r.body); } catch (err) {} res.body = JSON.stringify({'error': true, 'errorMessage': 'error from DBserver, possibly DBserver unknown', 'body': bodyobj}); } } } }); // ////////////////////////////////////////////////////////////////////////////// // / @start Docu Block JSF_getSecondary // / (intentionally not in manual) // / @brief gets the secondary of a primary DBserver // / // / @ RESTHEADER{GET /_admin/cluster/getSecondary, Get secondary of a primary DBServer} // / // / @ RESTQUERYPARAMETERS // / // / @ RESTDESCRIPTION Gets the configuration in the agency of the secondary // / replicating a primary. // / // / @ RESTQUERYPARAMETERS // / // / @ RESTQUERYPARAM{primary,string,required} // / is the ID of the primary whose secondary we would like to get. // / // / @ RESTQUERYPARAM{timeout,number,optional} // / the timeout to use in HTTP requests to the agency, default is 60. // / // / @ RESTRETURNCODES // / // / @ RESTRETURNCODE{200} is returned when everything went well. // / // / @ RESTRETURNCODE{400} the primary was not given as query parameter. // / // / @ RESTRETURNCODE{403} server is not a coordinator or method was not GET. // / // / @ RESTRETURNCODE{404} the given primary name is not configured in Agency. // / // / @ RESTRETURNCODE{408} there was a timeout in the Agency communication. // / // / @ RESTRETURNCODE{500} the get operation did not work. // / // / @end Docu Block // ////////////////////////////////////////////////////////////////////////////// actions.defineHttp({ url: '_admin/cluster/getSecondary', allowUseDatabase: true, prefix: false, callback: function (req, res) { if (req.requestType !== actions.GET || !require('@arangodb/cluster').isCoordinator()) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only GET requests are allowed and only to coordinators'); return; } if (!req.parameters.hasOwnProperty('primary')) { actions.resultError(req, res, actions.HTTP_BAD, 0, '"primary" is not given as parameter'); return; } var primary = req.parameters.primary; var agency = ArangoAgency.get('Plan/DBServers/' + primary); let secondary = fetchKey(agency, 'arango', 'Plan', 'DBServers', primary); if (secondary === undefined) { actions.resultError(req, res, actions.HTTP_NOT_FOUND, 0, 'Primary with the given ID is not configured in Agency.'); } actions.resultOk(req, res, actions.HTTP_OK, { primary: primary, secondary: secondary }); } }); // ////////////////////////////////////////////////////////////////////////////// // / @start Docu Block JSF_replaceSecondary // / (intentionally not in manual) // / @brief exchanges the secondary of a primary DBserver // / // / @ RESTHEADER{PUT /_admin/cluster/replaceSecondary, Replace secondary of a primary DBServer} // / // / @ RESTDESCRIPTION Replaces the configuration in the agency of the secondary // / replicating a primary. Use with care, because the old secondary will // / relatively quickly delete its data. For security reasons and to avoid // / races, the ID of the old secondary must be given as well. // / // / @ RESTBODYPARAM{primary,string,required,string} // / is the ID of the primary whose secondary is to be changed. // / // / @ RESTBODYPARAM{oldSecondary,string,required,string} // / is the old ID of the secondary. // / // / @ RESTBODYPARAM{newSecondary,string,required,string} // / is the new ID of the secondary. // / // / @ RESTBODYPARAM{ttl,number,optional,number} // / the time to live in seconds for the write lock, default is 60. // / // / @ RESTBODYPARAM{timeout,number,optional,number} // / the timeout to use in HTTP requests to the agency, default is 60. // / // / @ RESTRETURNCODES // / // / @ RESTRETURNCODE{200} is returned when everything went well. // / // / @ RESTRETURNCODE{400} either one of the required body parameters was // / not given or no server with this ID exists. // / // / @ RESTRETURNCODE{403} server is not a coordinator or method was not PUT. // / // / @ RESTRETURNCODE{404} the given primary name is not configured in Agency. // / // / @ RESTRETURNCODE{408} there was a timeout in the Agency communication. // / // / @ RESTRETURNCODE{412} the given oldSecondary was not the current secondary // / of the given primary. // / // / @ RESTRETURNCODE{500} the change operation did not work. // / // / @end Docu Block // ////////////////////////////////////////////////////////////////////////////// actions.defineHttp({ url: '_admin/cluster/replaceSecondary', allowUseDatabase: true, prefix: false, callback: function (req, res) { if (req.requestType !== actions.PUT || !require('@arangodb/cluster').isCoordinator()) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only PUT requests are allowed and only to coordinators'); return; } var body = actions.getJsonBody(req, res); if (body === undefined) { return; } if (!body.hasOwnProperty('primary') || typeof (body.primary) !== 'string' || !body.hasOwnProperty('oldSecondary') || typeof (body.oldSecondary) !== 'string' || !body.hasOwnProperty('newSecondary') || typeof (body.newSecondary) !== 'string') { actions.resultError(req, res, actions.HTTP_BAD, 0, 'not all three of "primary", "oldSecondary" and ' + '"newSecondary" are given in body and are strings'); return; } let agency = ArangoAgency.get('Plan/DBServers/' + body.primary); if (fetchKey(agency, 'arango', 'Plan', 'DBServers', body.primary) === undefined) { actions.resultError(req, res, actions.HTTP_NOT_FOUND, 0, 'Primary with the given ID is not configured in Agency.'); return; } let operations = {}; operations['/arango/Plan/DBServers/' + body.primary] = body.newSecondary; operations['/arango/Plan/Version'] = {'op': 'increment'}; let preconditions = {}; preconditions['/arango/Plan/DBServers/' + body.primary] = {'old': body.oldSecondary}; try { global.ArangoAgency.write([[operations, preconditions]]); } catch (e) { if (e.code === 412) { let oldValue = ArangoAgency.get('Plan/DBServers/' + body.primary); actions.resultError(req, res, actions.HTTP_PRECONDITION_FAILED, 0, 'Primary does not have the given oldSecondary as ' + 'its secondary, current value: ' + JSON.stringify( fetchKey(oldValue, 'arango', 'Plan', 'DBServers', body.primary) )); return; } throw e; } } }); function reducePlanServers(reducer, data) { var databases = ArangoAgency.get('Plan/Collections'); databases = databases.arango.Plan.Collections; return Object.keys(databases).reduce(function(data, databaseName) { var collections = databases[databaseName]; return Object.keys(collections).reduce(function(data, collectionKey) { var collection = collections[collectionKey]; return Object.keys(collection.shards).reduce(function(data, shardKey) { var servers = collection.shards[shardKey]; let key = '/arango/Plan/Collections/' + databaseName + '/' + collectionKey + '/shards/' + shardKey; return reducer(data, key, servers); }, data); }, data); }, data); } function reduceCurrentServers(reducer, data) { var databases = ArangoAgency.get('Current/Collections'); databases = databases.arango.Current.Collections; return Object.keys(databases).reduce(function(data, databaseName) { var collections = databases[databaseName]; return Object.keys(collections).reduce(function(data, collectionKey) { var collection = collections[collectionKey]; return Object.keys(collection).reduce(function(data, shardKey) { var servers = collection[shardKey].servers; let key = '/arango/Current/Collections/' + databaseName + '/' + collectionKey + '/' + shardKey + '/servers'; return reducer(data, key, servers); }, data); }, data); }, data); } // ////////////////////////////////////////////////////////////////////////////// // / @brief changes responsibility for all shards from oldServer to newServer. // / This needs to be done atomically! // ////////////////////////////////////////////////////////////////////////////// function changeAllShardReponsibilities (oldServer, newServer) { return reducePlanServers(function(data, key, servers) { var oldServers = _.cloneDeep(servers); servers = servers.map(function(server) { if (server === oldServer) { return newServer; } else { return server; } }); data.operations[key] = servers; data.preconditions[key] = {'old': oldServers}; return data; }, { operations: {}, preconditions: {}, }); } // ////////////////////////////////////////////////////////////////////////////// // / @start Docu Block JSF_swapPrimaryAndSecondary // / (intentionally not in manual) // / @brief swaps the roles of a primary and secondary pair // / // / @ RESTHEADER{PUT /_admin/cluster/swapPrimaryAndSecondary, Swaps the roles of a primary and secondary pair.} // / // / @RESTDESCRIPTION Swaps the roles of a primary and replicating secondary // / pair. This includes changing the entry for all shards for which the // / primary was responsible to the name of the secondary. All changes happen // / in a single write transaction (using a write lock) and the Plan/Version // / is increased. Use with care, because currently replication in the cluster // / is asynchronous and the old secondary might not yet have all the data. // / For security reasons and to avoid races, the ID of the old secondary // / must be given as well. // / // / @ RESTBODYPARAM{primary,string,required,string} // / is the ID of the primary whose secondary is to be changed. // / // / @ RESTBODYPARAM{secondary,string,required,string} // / is the ID of the secondary, which must be the secondary of this primay. // / // / @ RESTBODYPARAM{ttl,number,optional,number} // / the time to live in seconds for the write lock, default is 60. // / // / @ RESTBODYPARAM{timeout,number,optional,number} // / the timeout to use in HTTP requests to the agency, default is 60. // / // / @ RESTRETURNCODES // / // / @ RESTRETURNCODE{200} is returned when everything went well. // / // / @ RESTRETURNCODE{400} either one of the required body parameters was // / not given or no server with this ID exists. // / // / @ RESTRETURNCODE{403} server is not a coordinator or method was not PUT. // / // / @ RESTRETURNCODE{404} the given primary name is not configured in Agency. // / // / @ RESTRETURNCODE{408} there was a timeout in the Agency communication. // / // / @ RESTRETURNCODE{412} the given secondary was not the current secondary // / of the given primary. // / // / @ RESTRETURNCODE{500} the change operation did not work. // / // / @end Docu Block // ////////////////////////////////////////////////////////////////////////////// actions.defineHttp({ url: '_admin/cluster/swapPrimaryAndSecondary', allowUseDatabase: true, prefix: false, callback: function (req, res) { if (req.requestType !== actions.PUT || !require('@arangodb/cluster').isCoordinator()) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only PUT requests are allowed and only to coordinators'); return; } var body = actions.getJsonBody(req, res); if (body === undefined) { return; } if (!body.hasOwnProperty('primary') || typeof (body.primary) !== 'string' || !body.hasOwnProperty('secondary') || typeof (body.secondary) !== 'string') { actions.resultError(req, res, actions.HTTP_BAD, 0, 'not both "primary" and "secondary" ' + 'are given in body and are strings'); return; } let operations = {}; operations['/arango/Plan/DBServers/' + body.secondary] = body.primary; operations['/arango/Plan/DBServers/' + body.primary] = {'op': 'delete'}; operations['/arango/Plan/Version'] = {'op': 'increment'}; let preconditions = {}; preconditions['/arango/Plan/DBServers/' + body.primary] = {'old': body.secondary}; let shardChanges = changeAllShardReponsibilities(body.primary, body.secondary); operations = Object.assign(operations, shardChanges.operations); preconditions = Object.assign(preconditions, shardChanges.preconditions); try { global.ArangoAgency.write([[operations, preconditions]]); } catch (e) { if (e.code === 412) { let oldValue = ArangoAgency.get('Plan/DBServers/' + body.primary); actions.resultError(req, res, actions.HTTP_PRECONDITION_FAILED, 0, 'Could not change primary to secondary.'); return; } throw e; } actions.resultOk(req, res, actions.HTTP_OK, {primary: body.secondary, secondary: body.primary}); } }); // ////////////////////////////////////////////////////////////////////////////// // / @start Docu Block JSF_getNumberOfServers // / (intentionally not in manual) // / @brief gets the number of coordinators desired, which are stored in // / /Target/NumberOfDBServers in the agency. // / // / @ RESTHEADER{GET /_admin/cluster/numberOfServers, Get desired number of coordinators and DBServers.} // / // / @ RESTQUERYPARAMETERS // / // / @ RESTDESCRIPTION gets the number of coordinators and DBServers desired, // / which are stored in `/Target` in the agency. A body of the form // / { "numberOfCoordinators": 12, "numberOfDBServers": 12 } // / is returned. Note that both value can be `null` indicating that the // / cluster cannot be scaled. // / // / @ RESTRETURNCODES // / // / @ RESTRETURNCODE{200} is returned when everything went well. // / // / @ RESTRETURNCODE{403} server is not a coordinator or method was not GET // / or PUT. // / // / @ RESTRETURNCODE{503} the get operation did not work. // / // / @end Docu Block // ////////////////////////////////////////////////////////////////////////////// // ////////////////////////////////////////////////////////////////////////////// // / @start Docu Block JSF_putNumberOfServers // / (intentionally not in manual) // / @brief sets the number of coordinators and or DBServers desired, which // / are stored in /Target in the agency. // / // / @ RESTHEADER{PUT /_admin/cluster/numberOfServers, Set desired number of coordinators and or DBServers.} // / // / @ RESTQUERYPARAMETERS // / // / @ RESTDESCRIPTION sets the number of coordinators and DBServers desired, // / which are stored in `/Target` in the agency. A body of the form // / { "numberOfCoordinators": 12, "numberOfDBServers": 12, // / "cleanedServers": [] } // / must be supplied. Either one of the values can be left out and will // / then not be changed. Either numeric value can be `null` to indicate // / that the cluster cannot be scaled. // / // / @ RESTRETURNCODES // / // / @ RESTRETURNCODE{200} is returned when everything went well. // / // / @ RESTRETURNCODE{400} body is not valid JSON. // / // / @ RESTRETURNCODE{403} server is not a coordinator or method was not GET // / or PUT. // / // / @ RESTRETURNCODE{503} the agency operation did not work. // / // / @end Docu Block // ////////////////////////////////////////////////////////////////////////////// actions.defineHttp({ url: '_admin/cluster/numberOfServers', allowUseDatabase: false, prefix: false, callback: function (req, res) { if (!require('@arangodb/cluster').isCoordinator()) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only coordinators can serve this request'); return; } if (req.requestType !== actions.GET && req.requestType !== actions.PUT) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only GET and PUT methods are allowed'); return; } // Now get to work: if (req.requestType === actions.GET) { var nrCoordinators; var nrDBServers; var cleanedServers; try { nrCoordinators = ArangoAgency.get('Target/NumberOfCoordinators'); nrCoordinators = nrCoordinators.arango.Target.NumberOfCoordinators; nrDBServers = ArangoAgency.get('Target/NumberOfDBServers'); nrDBServers = nrDBServers.arango.Target.NumberOfDBServers; cleanedServers = ArangoAgency.get('Target/CleanedServers'); cleanedServers = cleanedServers.arango.Target.CleanedServers; } catch (e1) { actions.resultError(req, res, actions.HTTP_SERVICE_UNAVAILABLE, 'Cannot read from agency.'); return; } actions.resultOk(req, res, actions.HTTP_OK, {numberOfCoordinators: nrCoordinators, numberOfDBServers: nrDBServers, cleanedServers}); } else { // PUT var body = actions.getJsonBody(req, res); if (body === undefined) { return; } if (typeof body !== 'object') { actions.resultError(req, res, actions.HTTP_BAD, 'body must be an object'); return; } var ok = true; try { if (body.hasOwnProperty('numberOfCoordinators') && (typeof body.numberOfCoordinators === 'number' || body.numberOfCoordinators === null)) { ArangoAgency.set('Target/NumberOfCoordinators', body.numberOfCoordinators); } } catch (e1) { ok = false; } try { if (body.hasOwnProperty('numberOfDBServers') && (typeof body.numberOfDBServers === 'number' || body.numberOfDBServers === null)) { ArangoAgency.set('Target/NumberOfDBServers', body.numberOfDBServers); } } catch (e2) { ok = false; } try { if (body.hasOwnProperty('cleanedServers') && typeof body.cleanedServers === 'object' && Array.isArray(body.cleanedServers)) { ArangoAgency.set('Target/CleanedServers', body.cleanedServers); } } catch (e3) { ok = false; } if (!ok) { actions.resultError(req, res, actions.HTTP_SERVICE_UNAVAILABLE, 'Cannot write to agency.'); return; } actions.resultOk(req, res, actions.HTTP_OK, true); } } }); // ////////////////////////////////////////////////////////////////////////////// // / @start Docu Block JSF_postCleanOutServer // / (intentionally not in manual) // / @brief triggers activities to clean out a DBServer // / // / @ RESTHEADER{POST /_admin/cluster/cleanOutServer, Trigger activities to clean out a DBServers.} // / // / @ RESTQUERYPARAMETERS // / // / @ RESTDESCRIPTION Triggers activities to clean out a DBServer. // / The body must be a JSON object with attribute "server" that is a string // / with the ID of the server to be cleaned out. // / // / @ RESTRETURNCODES // / // / @ RESTRETURNCODE{202} is returned when everything went well and the // / job is scheduled. // / // / @ RESTRETURNCODE{400} body is not valid JSON. // / // / @ RESTRETURNCODE{403} server is not a coordinator or method was not POST. // / // / @ RESTRETURNCODE{503} the agency operation did not work. // / // / @end Docu Block // ////////////////////////////////////////////////////////////////////////////// actions.defineHttp({ url: '_admin/cluster/cleanOutServer', allowUseDatabase: false, prefix: false, callback: function (req, res) { if (!require('@arangodb/cluster').isCoordinator()) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only coordinators can serve this request'); return; } if (req.requestType !== actions.POST) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only the POST method is allowed'); return; } // Now get to work: var body = actions.getJsonBody(req, res); if (body === undefined) { return; } if (typeof body !== 'object' || !body.hasOwnProperty('server') || typeof body.server !== 'string') { actions.resultError(req, res, actions.HTTP_BAD, "body must be an object with a string attribute 'server'"); return; } // First translate the server name from short name to long name: var server = body.server; var servers = global.ArangoClusterInfo.getDBServers(); for (let i = 0; i < servers.length; i++) { if (servers[i].serverId !== server) { if (servers[i].serverName === server) { server = servers[i].serverId; break; } } } var ok = true; var id; try { id = ArangoClusterInfo.uniqid(); var todo = { 'type': 'cleanOutServer', 'server': server, 'jobId': id, 'timeCreated': (new Date()).toISOString(), 'creator': ArangoServerState.id() }; ArangoAgency.set('Target/ToDo/' + id, todo); } catch (e1) { ok = false; } if (!ok) { actions.resultError(req, res, actions.HTTP_SERVICE_UNAVAILABLE, {error: true, errorMsg: 'Cannot write to agency.'}); return; } actions.resultOk(req, res, actions.HTTP_ACCEPTED, {error: false, id: id}); } }); // ////////////////////////////////////////////////////////////////////////////// // / @start Docu Block JSF_postMoveShard // / (intentionally not in manual) // / @brief triggers activities to move a shard // / // / @ RESTHEADER{POST /_admin/cluster/moveShard, Trigger activities to move a shard.} // / // / @ RESTQUERYPARAMETERS // / // / @ RESTDESCRIPTION Triggers activities to move a shard. // / The body must be a JSON document with the following attributes: // / - `"database"`: a string with the name of the database // / - `"collection"`: a string with the name of the collection // / - `"shard"`: a string with the name of the shard to move // / - `"fromServer"`: a string with the ID of a server that is currently // / the leader or a follower for this shard // / - `"toServer"`: a string with the ID of a server that is currently // / not the leader and not a follower for this shard // / // / @ RESTRETURNCODES // / // / @ RESTRETURNCODE{202} is returned when everything went well and the // / job is scheduled. // / // / @ RESTRETURNCODE{400} body is not valid JSON. // / // / @ RESTRETURNCODE{403} server is not a coordinator or method was not POST. // / // / @ RESTRETURNCODE{503} the agency operation did not work. // / // / @end Docu Block // ////////////////////////////////////////////////////////////////////////////// actions.defineHttp({ url: '_admin/cluster/moveShard', allowUseDatabase: false, prefix: false, callback: function (req, res) { if (!require('@arangodb/cluster').isCoordinator()) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only coordinators can serve this request'); return; } if (req.requestType !== actions.POST) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only the POST method is allowed'); return; } // Now get to work: var body = actions.getJsonBody(req, res); if (body === undefined) { return; } if (typeof body !== 'object' || !body.hasOwnProperty('database') || typeof body.database !== 'string' || !body.hasOwnProperty('collection') || typeof body.collection !== 'string' || !body.hasOwnProperty('shard') || typeof body.shard !== 'string' || !body.hasOwnProperty('fromServer') || typeof body.fromServer !== 'string' || !body.hasOwnProperty('toServer') || typeof body.toServer !== 'string') { actions.resultError(req, res, actions.HTTP_BAD, "body must be an object with string attributes 'database', 'collection', 'shard', 'fromServer' and 'toServer'"); return; } body.shards=[body.shard]; body.collections=[body.collection]; var r = require('@arangodb/cluster').moveShard(body); if (r.error) { actions.resultError(req, res, actions.HTTP_SERVICE_UNAVAILABLE, r); return; } actions.resultOk(req, res, actions.HTTP_ACCEPTED, r); } }); // ////////////////////////////////////////////////////////////////////////////// // / @start Docu Block JSF_getShardDistribution // / (intentionally not in manual) // / @brief returns information about all collections and their shard // / distribution // / // / @ RESTHEADER{GET /_admin/cluster/shardDistribution, Get shard distribution for all collections.} // / // / @ RESTDESCRIPTION Returns an object with an attribute for each collection. // / The attribute name is the collection name. Each value is an object // / of the following form: // / // / { "collection1": { "Plan": { "s100001": ["DBServer001", "DBServer002"], // / "s100002": ["DBServer003", "DBServer004"] }, // / "Current": { "s100001": ["DBServer001", "DBServer002"], // / "s100002": ["DBServer003"] } }, // / "collection2": ... // / } // / // / @ RESTRETURNCODES // / // / @ RESTRETURNCODE{200} is returned when everything went well and the // / job is scheduled. // / // / @ RESTRETURNCODE{403} server is not a coordinator or method was not GET. // / // / @end Docu Block // ////////////////////////////////////////////////////////////////////////////// actions.defineHttp({ url: '_admin/cluster/shardDistribution', allowUseDatabase: false, prefix: false, callback: function (req, res) { if (!require('@arangodb/cluster').isCoordinator()) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only coordinators can serve this request'); return; } if (req.requestType !== actions.GET) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only the GET method is allowed'); return; } var result = require('@arangodb/cluster').shardDistribution(); var dbsToCheck = []; var diff; var getDifference = function (a, b) { return a.filter(function (i) { return b.indexOf(i) < 0; }); }; _.each(result.results, function (info, collection) { _.each(info.Plan, function (shard, shardkey) { // check if shard is out of sync if (!_.isEqual(shard.followers, info.Current[shardkey].followers)) { // if not in sync, get document counts of leader and compare with follower diff = getDifference(shard.followers, info.Current[shardkey].followers); dbsToCheck.push({ shard: shardkey, toCheck: diff, leader: info.Plan[shardkey].leader, collection: collection }); } }); }); var leaderOP, followerOP, leaderR, followerR, leaderBody, followerBody; var options = { timeout: 10 }; _.each(dbsToCheck, function (shard) { if (shard.leader.charAt(0) === '_') { shard.leader = shard.leader.substr(1, shard.leader.length - 1); } if (typeof shard.toCheck === 'object') { if (shard.toCheck.length === 0) { return; } } // get counts of leader and follower shard leaderOP = null; try { leaderOP = ArangoClusterComm.asyncRequest('GET', 'server:' + shard.leader, '_system', '/_api/collection/' + shard.shard + '/count', '', {}, options); } catch (e) { } // IMHO these try...catch things should at least log something but I don't want to // introduce last minute log spam before the release (this was not logging either before restructuring it) let followerOps = shard.toCheck.map(follower => { try { return ArangoClusterComm.asyncRequest('GET', 'server:' + follower, '_system', '/_api/collection/' + shard.shard + '/count', '', {}, options); } catch (e) { return null; } }); let [minFollowerCount, maxFollowerCount] = followerOps.reduce((result, followerOp) => { if (!followerOp) { return result; } let followerCount = 0; try { followerR = ArangoClusterComm.wait(followerOp); if (followerR.status !== 'BACKEND_UNAVAILABLE') { try { followerBody = JSON.parse(followerR.body); followerCount = followerBody.count; } catch (e) { } } } catch(e) { } if (result === null) { return [followerCount, followerCount]; } else { return [Math.min(followerCount, result[0]), Math.max(followerCount, result[1])]; } }, null); let leaderCount = null; if (leaderOP) { leaderR = ArangoClusterComm.wait(leaderOP); try { leaderBody = JSON.parse(leaderR.body); leaderCount = leaderBody.count; } catch (e) { } } let followerCount; if (minFollowerCount < leaderCount) { followerCount = minFollowerCount; } else { followerCount = maxFollowerCount; } result.results[shard.collection].Plan[shard.shard].progress = { total: leaderCount, current: followerCount, }; }); actions.resultOk(req, res, actions.HTTP_OK, result); } }); // ////////////////////////////////////////////////////////////////////////////// // / @start Docu Block JSF_postRebalanceShards // / (intentionally not in manual) // / @brief triggers activities to rebalance shards // / // / @ RESTHEADER{POST /_admin/cluster/rebalanceShards, Trigger activities to rebalance shards.} // / // / @ RESTDESCRIPTION Triggers activities to rebalance shards. // / The body must be an empty JSON object. // / // / @ RESTRETURNCODES // / // / @ RESTRETURNCODE{202} is returned when everything went well. // / // / @ RESTRETURNCODE{400} body is not valid JSON. // / // / @ RESTRETURNCODE{403} server is not a coordinator or method was not POST. // / // / @ RESTRETURNCODE{503} the agency operation did not work. // / // / @end Docu Block // ////////////////////////////////////////////////////////////////////////////// actions.defineHttp({ url: '_admin/cluster/rebalanceShards', allowUseDatabase: true, prefix: false, callback: function (req, res) { if (!require('@arangodb/cluster').isCoordinator()) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only coordinators can serve this request'); return; } if (req.requestType !== actions.POST) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only the POST method is allowed'); return; } // Now get to work: var body = actions.getJsonBody(req, res); if (body === undefined) { return; } if (typeof body !== 'object') { actions.resultError(req, res, actions.HTTP_BAD, 'body must be an object.'); return; } var ok = require('@arangodb/cluster').rebalanceShards(); if (!ok) { actions.resultError(req, res, actions.HTTP_SERVICE_UNAVAILABLE, 'Cannot write to agency.'); return; } actions.resultOk(req, res, actions.HTTP_ACCEPTED, true); } }); // ////////////////////////////////////////////////////////////////////////////// // / @start Docu Block JSF_getSupervisionState // / (intentionally not in manual) // / @brief returns information about the state of Supervision jobs // / // / @ RESTHEADER{GET /_admin/cluster/supervisionState, Get information // / about the state of Supervision jobs. // / // / @ RESTDESCRIPTION Returns an object with attributes `ToDo`, `Pending`, // / `Failed` and `Finished` mirroring the state of Supervision jobs in // / the agency. // / // / @ RESTRETURNCODES // / // / @ RESTRETURNCODE{200} is returned when everything went well and the // / job is scheduled. // / // / @ RESTRETURNCODE{403} server is not a coordinator or method was not GET. // / // / @end Docu Block // ////////////////////////////////////////////////////////////////////////////// actions.defineHttp({ url: '_admin/cluster/supervisionState', allowUseDatabase: false, prefix: false, callback: function (req, res) { if (!require('@arangodb/cluster').isCoordinator()) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only coordinators can serve this request'); return; } if (req.requestType !== actions.GET) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only the GET method is allowed'); return; } var result = require('@arangodb/cluster').supervisionState(); if (result.error) { actions.resultError(req, res, actions.HTTP_BAD, result); return; } actions.resultOk(req, res, actions.HTTP_OK, result); } }); // ////////////////////////////////////////////////////////////////////////////// // / @start Docu Block JSF_getClusterEndpoints // / @brief returns information about all coordinator endpoints // / // / @ RESTHEADER{GET /_api/cluster/endpoints, Get information // / about all coordinator endpoints // / // / @ RESTDESCRIPTION Returns an array of objects, which each have // / the attribute `endpoint`, whose value is a string with the endpoint // / description. There is an entry for each coordinator in the cluster. // / // / @ RESTRETURNCODES // / // / @ RESTRETURNCODE{200} is returned when everything went well. // / // / @ RESTRETURNCODE{403} server is not a coordinator or method was not GET. // / // / @end Docu Block // ////////////////////////////////////////////////////////////////////////////// actions.defineHttp({ url: '_api/cluster/endpoints', allowUseDatabase: false, prefix: false, callback: function (req, res) { if (!require('@arangodb/cluster').isCoordinator()) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only coordinators can serve this request'); return; } if (req.requestType !== actions.GET) { actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0, 'only the GET method is allowed'); return; } var result = require('@arangodb/cluster').endpoints(); if (result.error) { actions.resultError(req, res, actions.HTTP_BAD, result); return; } actions.resultOk(req, res, actions.HTTP_OK, result); } });