1
0
Fork 0
arangodb/js/actions/api-cluster.js

1373 lines
46 KiB
JavaScript

/*jshint strict: false, unused: false */
/*global AQL_EXECUTE, SYS_CLUSTER_TEST, UPGRADE_ARGS: true,
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");
////////////////////////////////////////////////////////////////////////////////
/// @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-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 function to parse an authorization header
////////////////////////////////////////////////////////////////////////////////
function parseAuthorization (authorization) {
var auth = require("internal").base64Decode(authorization.substr(6));
var pos = auth.indexOf(":");
if (pos === -1) {
return {username:"root", passwd:""};
}
return { username: auth.substr(0, pos),
passwd: auth.substr(pos+1) || "" };
}
////////////////////////////////////////////////////////////////////////////////
/// @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) {}
// Now get to work, first get the write lock on the Plan in the Agency:
var success = ArangoAgency.lockRead("Supervision", timeout);
if (! success) {
actions.resultError(req, res, actions.HTTP_REQUEST_TIMEOUT, 0,
"could not get a read lock on Plan in Agency");
return;
}
try {
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} );
} finally {
ArangoAgency.unlockRead("Supervision", timeout);
}
}
});
////////////////////////////////////////////////////////////////////////////////
/// @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} );
}
}
}
});
////////////////////////////////////////////////////////////////////////////////
/// @brief bootstraps the all db servers
////////////////////////////////////////////////////////////////////////////////
actions.defineHttp({
url: "_admin/cluster/bootstrapDbServers",
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;
}
if (!require("@arangodb/cluster").isCoordinator()) {
actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0,
"only allowed on coordinator");
return;
}
var body = actions.getJsonBody(req, res);
try {
var result = cluster.bootstrapDbServers(body.isRelaunch);
if (result) {
actions.resultOk(req, res, actions.HTTP_OK);
}
else {
actions.resultBad(req, res);
}
}
catch(err) {
actions.resultException(req, res, err);
}
}
});
////////////////////////////////////////////////////////////////////////////////
/// @brief bootstraps one db server
////////////////////////////////////////////////////////////////////////////////
actions.defineHttp({
url: "_admin/cluster/bootstrapDbServer",
prefix: false,
callback: function (req, res) {
var body = actions.getJsonBody(req, res);
UPGRADE_ARGS = {
isCluster: true,
isDbServer: true,
isRelaunch: body.isRelaunch
};
try {
var func = internal.loadStartup("server/bootstrap/db-server.js");
var result = func && func();
if (result) {
actions.resultOk(req, res, actions.HTTP_OK);
}
else {
actions.resultBad(req, res);
}
}
catch(err) {
actions.resultException(req, res, err);
}
}
});
////////////////////////////////////////////////////////////////////////////////
/// @brief upgrade cluster database
////////////////////////////////////////////////////////////////////////////////
actions.defineHttp({
url: "_admin/cluster/upgradeClusterDatabase",
prefix: false,
callback: function (req, res) {
var body = actions.getJsonBody(req, res);
UPGRADE_ARGS = {
isCluster: true,
isCoordinator: true,
isRelaunch: body.isRelaunch || false,
upgrade: body.upgrade || false
};
try {
var result = internal.loadStartup("server/upgrade-database.js");
if (result) {
actions.resultOk(req, res, actions.HTTP_OK);
}
else {
actions.resultBad(req, res);
}
}
catch(err) {
actions.resultException(req, res, err);
}
}
});
////////////////////////////////////////////////////////////////////////////////
/// @brief bootstraps the coordinator
////////////////////////////////////////////////////////////////////////////////
actions.defineHttp({
url: "_admin/cluster/bootstrapCoordinator",
allowUseDatabase: true,
prefix: false,
callback: function (req, res) {
try {
var func = internal.loadStartup("server/bootstrap/coordinator.js");
var result = func && func();
if (result) {
actions.resultOk(req, res, actions.HTTP_OK);
}
else {
actions.resultBad(req, res);
}
}
catch(err) {
actions.resultException(req, res, err);
}
}
});
////////////////////////////////////////////////////////////////////////////////
/// @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 timeout = 60.0;
try {
if (req.parameters.hasOwnProperty("timeout")) {
timeout = Number(req.parameters.timeout);
}
}
catch (e) {
}
// Now get to work, first get the write lock on the Plan in the Agency:
var success = ArangoAgency.lockRead("Plan", timeout);
if (! success) {
actions.resultError(req, res, actions.HTTP_REQUEST_TIMEOUT, 0,
"could not get a read lock on Plan in Agency");
return;
}
try {
var oldValue;
try {
oldValue = ArangoAgency.get("Plan/DBServers/" + primary, false, false);
oldValue = oldValue.arango.Plan.DBServers[primary];
}
catch (e1) {
actions.resultError(req, res, actions.HTTP_NOT_FOUND, 0,
"Primary with the given ID is not configured in Agency.");
return;
}
oldValue = oldValue["Plan/DBServers/" + primary];
actions.resultOk(req, res, actions.HTTP_OK, { primary: primary,
secondary: oldValue } );
}
finally {
ArangoAgency.unlockRead("Plan", timeout);
}
}
});
////////////////////////////////////////////////////////////////////////////////
/// @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;
}
var ttl = 60.0;
var timeout = 60.0;
if (body.hasOwnProperty("ttl") && typeof body.ttl === "number") {
ttl = body.ttl;
}
if (body.hasOwnProperty("timeout") && typeof body.timeout === "number") {
timeout = body.timeout;
}
// Now get to work, first get the write lock on the Plan in the Agency:
var success = ArangoAgency.lockWrite("Plan", ttl, timeout);
if (! success) {
actions.resultError(req, res, actions.HTTP_REQUEST_TIMEOUT, 0,
"could not get a write lock on Plan in Agency");
return;
}
try {
var oldValue;
try {
oldValue = ArangoAgency.get("Plan/DBServers/" + body.primary, false,
false);
oldValue = oldValue.arango.Plan.DBServers[body.primary];
}
catch (e1) {
actions.resultError(req, res, actions.HTTP_NOT_FOUND, 0,
"Primary with the given ID is not configured in Agency.");
return;
}
if (oldValue !== body.oldSecondary) {
actions.resultError(req, res, actions.HTTP_PRECONDITION_FAILED, 0,
"Primary does not have the given oldSecondary as "+
"its secondary, current value: " + oldValue);
return;
}
try {
ArangoAgency.set("Plan/DBServers/" + body.primary, body.newSecondary,
0);
}
catch (e2) {
actions.resultError(req, res, actions.HTTP_SERVER_ERROR, 0,
"Cannot change secondary of given primary.");
return;
}
try {
ArangoAgency.increaseVersion("Plan/Version");
}
catch (e3) {
actions.resultError(req, res, actions.HTTP_SERVER_ERROR, 0,
"Cannot increase Plan/Version.");
return;
}
actions.resultOk(req, res, actions.HTTP_OK, body);
}
finally {
ArangoAgency.unlockWrite("Plan", timeout);
}
}
});
////////////////////////////////////////////////////////////////////////////////
/// @brief changes responsibility for all shards from oldServer to newServer.
/// This needs to be done atomically!
////////////////////////////////////////////////////////////////////////////////
function changeAllShardReponsibilities (oldServer, newServer) {
// This is only called when we have the write lock and we "only" have to
// make sure that either all or none of the shards are moved.
var collections = ArangoAgency.get("Plan/Collections", true, false);
collections = collections.arango.Plan.Collections;
var done = {};
try {
Object.keys(collections).forEach(function(collectionKey) {
var collection = collections[collectionKey];
var old = _.cloneDeep(collection);
Object.keys(collection.shards).forEach(function(shardKey) {
var servers = collection.shards[shardKey];
collection.shards[shardKey] = servers.map(function(server) {
if (server === oldServer) {
return newServer;
} else {
return server;
}
});
});
ArangoAgency.set(collectionKey, collection, 0);
done[collectionKey] = old;
});
} catch (e) {
// mop: rollback
try {
Object.keys(done).forEach(function(collectionKey) {
ArangoAgency.set(collectionKey, done[collectionKey], 0);
});
} catch (e2) {
console.error("Got error during rolback", e2);
}
throw e;
}
}
////////////////////////////////////////////////////////////////////////////////
/// @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;
}
var ttl = 60.0;
var timeout = 60.0;
if (body.hasOwnProperty("ttl") && typeof body.ttl === "number") {
ttl = body.ttl;
}
if (body.hasOwnProperty("timeout") && typeof body.timeout === "number") {
timeout = body.timeout;
}
// Now get to work, first get the write lock on the Plan in the Agency:
var success = ArangoAgency.lockWrite("Plan", ttl, timeout);
if (! success) {
actions.resultError(req, res, actions.HTTP_REQUEST_TIMEOUT, 0,
"could not get a write lock on Plan in Agency");
return;
}
try {
var oldValue;
try {
oldValue = ArangoAgency.get("Plan/DBServers/" + body.primary, false,
false);
oldValue = oldValue.arango.Plan.DBservers[body.primary];
}
catch (e1) {
actions.resultError(req, res, actions.HTTP_NOT_FOUND, 0,
"Primary with the given ID is not configured in Agency.");
return;
}
oldValue = oldValue["Plan/DBServers/"+body.primary];
if (oldValue !== body.secondary) {
actions.resultError(req, res, actions.HTTP_PRECONDITION_FAILED, 0,
"Primary does not have the given secondary as "+
"its secondary, current value: " + oldValue);
return;
}
try {
ArangoAgency.remove("Plan/DBServers/" + body.primary, false);
}
catch (e2) {
actions.resultError(req, res, actions.HTTP_SERVER_ERROR, 0,
"Cannot remove old primary entry.");
return;
}
try {
ArangoAgency.set("Plan/DBServers/" + body.secondary,
body.primary, 0);
}
catch (e3) {
actions.resultError(req, res, actions.HTTP_SERVER_ERROR, 0,
"Cannot set secondary as primary.");
// Try to reset the old primary:
try {
ArangoAgency.set("Plan/DBServers/" + body.primary,
body.secondary, 0);
}
catch (e4) {
actions.resultError(req, res, actions.HTTP_SERVER_ERROR, 0,
"Cannot set secondary as primary, could not "+
"even reset the old value!");
}
return;
}
try {
// Now change all responsibilities for shards to the "new" primary
// body.secondary:
changeAllShardReponsibilities(body.primary, body.secondary);
}
catch (e5) {
actions.resultError(req, res, actions.HTTP_SERVER_ERROR, 0,
"Could not change responsibilities for shards.");
// Try to reset the old primary:
try {
ArangoAgency.set("Plan/DBServers/" + body.primary,
body.secondary, 0);
ArangoAgency.remove("Plan/DBServers/" + body.secondary);
}
catch (e4) {
actions.resultError(req, res, actions.HTTP_SERVER_ERROR, 0,
"Cannot change responsibility for shards and "+
"could not even reset the old value!");
}
return;
}
try {
ArangoAgency.increaseVersion("Plan/Version");
}
catch (e3) {
actions.resultError(req, res, actions.HTTP_SERVER_ERROR, 0,
"Cannot increase Plan/Version.");
return;
}
actions.resultOk(req, res, actions.HTTP_OK, {primary: body.secondary,
secondary: body.primary});
}
finally {
ArangoAgency.unlockWrite("Plan", timeout);
}
}
});
////////////////////////////////////////////////////////////////////////////////
/// @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 }
/// must be supplied. Either one of the values can be left out and will
/// then not be changed. Either 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: 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.GET &&
req.requestType !== actions.PUT) {
actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0,
"only GET and PUT methods are allowed");
return;
}
var timeout = 60.0;
// 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 {
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;
}
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: 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;
}
var timeout = 60.0;
// 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;
}
var ok = true;
try {
var id = ArangoClusterInfo.uniqid();
var todo = { "type": "cleanOutServer",
"server": body.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,
"Cannot write to agency.");
return;
}
actions.resultOk(req, res, actions.HTTP_ACCEPTED, true);
}
});
////////////////////////////////////////////////////////////////////////////////
/// @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: 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;
}
var timeout = 60.0;
// 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;
}
var ok = true;
var isLeader;
var collInfo;
try {
collInfo = ArangoClusterInfo.getCollectionInfo(body.database,
body.collection);
var shards = collInfo.shards;
var shard = shards[body.shard];
var pos = shard.indexOf(body.fromServer);
if (pos === -1) {
throw "Banana";
} else if (pos === 0) {
isLeader = true;
} else {
isLeader = false;
}
} catch (e2) {
actions.resultError(req, res, actions.HTTP_BAD,
"Combination of database, collection, shard and fromServer does not make sense.");
return;
}
try {
var id = ArangoClusterInfo.uniqid();
var todo = { "type": "moveShard",
"database": body.database,
"collection": collInfo.id,
"shard": body.shard,
"fromServer": body.fromServer,
"toServer": body.toServer,
"jobId": id,
"timeCreated": (new Date()).toISOString(),
"creator": ArangoServerState.id() };
ArangoAgency.set("Target/ToDo/" + id, todo);
} catch (e1) {
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_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": ["DBServer1", "DBServer2"],
/// "s100002": ["DBServer3", "DBServer4"] },
/// "Current": { "s100001": ["DBServer1", "DBServer2"],
/// "s100002": ["DBServer3"] } },
/// "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 POST.
///
/// @end Docu Block
////////////////////////////////////////////////////////////////////////////////
actions.defineHttp({
url: "_admin/cluster/shardDistribution",
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.GET) {
actions.resultError(req, res, actions.HTTP_FORBIDDEN, 0,
"only the GET method is allowed");
return;
}
var db = require("internal").db;
var dbName = db._name();
var colls = db._collections();
var result = {};
for (var i = 0; i < colls.length; ++i) {
var collName = colls[i].name();
var collInfo = global.ArangoClusterInfo.getCollectionInfo(dbName,
collName);
var shards = collInfo.shards;
var collInfoCurrent = {};
var shardNames = Object.keys(shards);
for (var j = 0; j < shardNames.length; ++j) {
collInfoCurrent[shardNames[j]] =
global.ArangoClusterInfo.getCollectionInfoCurrent(
dbName, collName, shardNames[j]).servers;
}
result[collName] = {Plan: collInfo.shards,
Current: collInfoCurrent};
}
console.error("GUCK:", result);
actions.resultOk(req, res, actions.HTTP_OK, result);
}
});