diff --git a/docs/rest-server/API.md b/docs/rest-server/API.md
index 55a21c83b9..13742a34da 100644
--- a/docs/rest-server/API.md
+++ b/docs/rest-server/API.md
@@ -1018,4 +1018,4 @@ the version upgrade, which has no namespaces. They are called "legacy jobs", whi
but cannot be created. To figure out them, there is a "legacy: true" field of them in list apis.
In the next versions, all operations of legacy jobs may be disabled, so please re-create them as namespaced
-job as soon as possible.
+job as soon as possible.
\ No newline at end of file
diff --git a/src/hadoop-resource-manager/deploy/hadoop-resource-manager-configuration/yarn-site.xml b/src/hadoop-resource-manager/deploy/hadoop-resource-manager-configuration/yarn-site.xml
index 9ef7105a42..85f64f240b 100644
--- a/src/hadoop-resource-manager/deploy/hadoop-resource-manager-configuration/yarn-site.xml
+++ b/src/hadoop-resource-manager/deploy/hadoop-resource-manager-configuration/yarn-site.xml
@@ -44,6 +44,12 @@
1048576
default is 8GB, here we set 1024G
+
+
+ yarn.scheduler.configuration.store.class
+ zk
+ default is file, change it to zk to enable config by rest api
+
yarn.resourcemanager.scheduler.class
diff --git a/src/rest-server/package.json b/src/rest-server/package.json
index 9ff097096e..2609c7f7f5 100644
--- a/src/rest-server/package.json
+++ b/src/rest-server/package.json
@@ -49,10 +49,11 @@
"node-cache": "~4.2.0",
"node-etcd": "~5.1.0",
"nyc": "~11.6.0",
+ "ssh-keygen": "~0.4.2",
"statuses": "~1.5.0",
"unirest": "~0.5.1",
"winston": "~2.4.0",
- "ssh-keygen": "~0.4.2"
+ "xml2js": "~0.4.19"
},
"scripts": {
"coveralls": "nyc report --reporter=text-lcov | coveralls ..",
diff --git a/src/rest-server/src/config/vc.js b/src/rest-server/src/config/vc.js
new file mode 100644
index 0000000000..5e571b543f
--- /dev/null
+++ b/src/rest-server/src/config/vc.js
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft Corporation
+// All rights reserved.
+//
+// MIT License
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
+// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+// module dependencies
+const Joi = require('joi');
+
+// define the input schema for the 'update vc' api
+const vcPutInputSchema = Joi.object().keys({
+ vcCapacity: Joi.number()
+ .integer()
+ .min(0)
+ .max(100)
+ .required(),
+}).required();
+
+// define the input schema for the 'put vc status' api
+const vcStatusPutInputSchema = Joi.object().keys({
+ vcStatus: Joi.string()
+ .valid(['stopped', 'running'])
+ .required(),
+}).required();
+
+// module exports
+module.exports = {
+ vcPutInputSchema: vcPutInputSchema,
+ vcStatusPutInputSchema: vcStatusPutInputSchema,
+};
diff --git a/src/rest-server/src/config/yarn.js b/src/rest-server/src/config/yarn.js
index c4fe666a3e..fede335fb2 100644
--- a/src/rest-server/src/config/yarn.js
+++ b/src/rest-server/src/config/yarn.js
@@ -18,6 +18,9 @@
// module dependencies
const Joi = require('joi');
+const unirest = require('unirest');
+const config = require('./index');
+const logger = require('./logger');
// get config from environment variables
let yarnConfig = {
@@ -26,8 +29,13 @@ let yarnConfig = {
'Accept': 'application/json',
},
yarnVcInfoPath: `${process.env.YARN_URI}/ws/v1/cluster/scheduler`,
+ webserviceUpdateQueueHeaders: {
+ 'Content-Type': 'application/xml',
+ },
+ yarnVcUpdatePath: `${process.env.YARN_URI}/ws/v1/cluster/scheduler-conf`,
};
+
const yarnConfigSchema = Joi.object().keys({
yarnUri: Joi.string()
.uri()
@@ -37,6 +45,11 @@ const yarnConfigSchema = Joi.object().keys({
yarnVcInfoPath: Joi.string()
.uri()
.required(),
+ webserviceUpdateQueueHeaders: Joi.object()
+ .required(),
+ yarnVcUpdatePath: Joi.string()
+ .uri()
+ .required(),
}).required();
const {error, value} = Joi.validate(yarnConfig, yarnConfigSchema);
@@ -45,4 +58,18 @@ if (error) {
}
yarnConfig = value;
+
+// framework launcher health check
+if (config.env !== 'test') {
+ unirest.get(yarnConfig.yarnVcInfoPath)
+ .timeout(2000)
+ .end((res) => {
+ if (res.status === 200) {
+ logger.info('connected to yarn successfully');
+ } else {
+ throw new Error('cannot connect to yarn');
+ }
+ });
+}
+
module.exports = yarnConfig;
diff --git a/src/rest-server/src/controllers/vc.js b/src/rest-server/src/controllers/vc.js
index 31fa38dec8..bed60bacb1 100644
--- a/src/rest-server/src/controllers/vc.js
+++ b/src/rest-server/src/controllers/vc.js
@@ -20,23 +20,14 @@ const VirtualCluster = require('../models/vc');
const createError = require('../util/error');
/**
- * Load virtual cluster and append to req.
+ * Validation, not allow operation to "default" vc.
*/
-const load = (req, res, next, vcName) => {
- new VirtualCluster(vcName, (vcInfo, error) => {
- if (error) {
- return next(createError.unknown(error));
- }
- req.vc = vcInfo;
+const validate = (req, res, next, vcName) => {
+ if (vcName === 'default' && req.method !== 'GET') {
+ return next(createError('Forbidden', 'ForbiddenUserError', `Update operation to default vc isn't allowed`));
+ } else {
return next();
- });
-};
-
-/**
- * Get virtual cluster status.
- */
-const get = (req, res) => {
- return res.json(req.vc);
+ }
};
/**
@@ -59,9 +50,105 @@ const list = (req, res, next) => {
});
};
+/**
+ * Get a vc.
+ */
+const get = (req, res, next) => {
+ const vcName = req.params.vcName;
+ VirtualCluster.prototype.getVc(vcName, (vcInfo, err) => {
+ if (err) {
+ return next(createError.unknown(err));
+ } else {
+ return res.status(200).json(vcInfo);
+ }
+ });
+};
+
+
+/**
+ * Add a vc.
+ */
+const update = (req, res, next) => {
+ const vcName = req.params.vcName;
+ const vcCapacity = parseInt(req.body.vcCapacity);
+ if (req.user.admin) {
+ VirtualCluster.prototype.updateVc(vcName, vcCapacity, (err) => {
+ if (err) {
+ return next(createError.unknown(err));
+ } else {
+ return res.status(201).json({
+ message: `update vc: ${vcName} to capacity: ${vcCapacity} successfully`,
+ });
+ }
+ });
+ } else {
+ next(createError('Forbidden', 'ForbiddenUserError', `Non-admin is not allowed to do this operation.`));
+ }
+};
+
+
+/**
+ * Update vc status, changing a vc from running to stopped a vc will only prevent new job in this vc.
+ */
+const updateStatus = (req, res, next) => {
+ const vcName = req.params.vcName;
+ const vcStatus = req.body.vcStatus;
+ if (req.user.admin) {
+ if (vcStatus === 'stopped') {
+ VirtualCluster.prototype.stopVc(vcName, (err) => {
+ if (err) {
+ return next(createError.unknown(err));
+ } else {
+ return res.status(201).json({
+ message: `stop vc ${vcName} successfully`,
+ });
+ }
+ });
+ } else if (vcStatus === 'running') {
+ VirtualCluster.prototype.activeVc(vcName, (err) => {
+ if (err) {
+ return next(createError.unknown(err));
+ } else {
+ return res.status(201).json({
+ message: `active vc ${vcName} successfully`,
+ });
+ }
+ });
+ } else {
+ next(createError('Bad Request', 'BadConfigurationError', `Unknown vc status: ${vcStatus}`));
+ }
+ } else {
+ next(createError('Forbidden', 'ForbiddenUserError', `Non-admin is not allowed to do this operation.`));
+ }
+};
+
+
+/**
+ * Remove a vc.
+ */
+const remove = (req, res, next) => {
+ const vcName = req.params.vcName;
+ if (req.user.admin) {
+ VirtualCluster.prototype.removeVc(vcName, (err) => {
+ if (err) {
+ return next(createError.unknown(err));
+ } else {
+ return res.status(201).json({
+ message: `remove vc: ${vcName} successfully`,
+ });
+ }
+ });
+ } else {
+ next(createError('Forbidden', 'ForbiddenUserError', `Non-admin is not allowed to do this operation.`));
+ }
+};
+
// module exports
module.exports = {
- load,
get,
list,
+ update,
+ remove,
+ updateStatus,
+ validate,
};
diff --git a/src/rest-server/src/models/vc.js b/src/rest-server/src/models/vc.js
index 86708f4d7d..e010c2b8ff 100644
--- a/src/rest-server/src/models/vc.js
+++ b/src/rest-server/src/models/vc.js
@@ -18,44 +18,35 @@
// module dependencies
const unirest = require('unirest');
+const xml2js = require('xml2js');
const yarnConfig = require('../config/yarn');
const createError = require('../util/error');
+const logger = require('../config/logger');
-class VirtualCluster {
- constructor(name, next) {
- this.getVcList((vcList, error) => {
- if (error === null) {
- if (name in vcList) {
- for (let key of Object.keys(vcList[name])) {
- this[key] = vcList[name][key];
- }
- } else {
- error = createError('Not Found', 'NoVirtualClusterError', `Virtual cluster ${name} is not found.`);
- }
- }
- next(this, error);
- });
- }
+class VirtualCluster {
getCapacitySchedulerInfo(queueInfo) {
let queues = {};
+
function traverse(queueInfo, queueDict) {
if (queueInfo.type === 'capacitySchedulerLeafQueueInfo') {
queueDict[queueInfo.queueName] = {
- capacity: queueInfo.absoluteCapacity,
- maxCapacity: queueInfo.absoluteMaxCapacity,
+ capacity: Math.round(queueInfo.absoluteCapacity),
+ maxCapacity: Math.round(queueInfo.absoluteMaxCapacity),
usedCapacity: queueInfo.absoluteUsedCapacity,
numActiveJobs: queueInfo.numActiveApplications,
numJobs: queueInfo.numApplications,
numPendingJobs: queueInfo.numPendingApplications,
resourcesUsed: queueInfo.resourcesUsed,
+ status: queueInfo.state,
};
} else {
for (let i = 0; i < queueInfo.queues.queue.length; i++) {
- traverse(queueInfo.queues.queue[i], queueDict);
+ traverse(queueInfo.queues.queue[i], queueDict);
}
}
}
+
traverse(queueInfo, queues);
return queues;
}
@@ -66,7 +57,7 @@ class VirtualCluster {
.end((res) => {
try {
const resJson = typeof res.body === 'object' ?
- res.body : JSON.parse(res.body);
+ res.body : JSON.parse(res.body);
const schedulerInfo = resJson.scheduler.schedulerInfo;
if (schedulerInfo.type === 'capacityScheduler') {
const vcInfo = this.getCapacitySchedulerInfo(schedulerInfo);
@@ -80,6 +71,253 @@ class VirtualCluster {
}
});
}
+
+ generateUpdateInfo(updateData) {
+ let jsonBuilder = new xml2js.Builder({rootName: 'sched-conf'});
+ let data = [];
+ if (updateData.hasOwnProperty('pendingAdd')) {
+ for (let item in updateData['pendingAdd']) {
+ if (updateData['pendingAdd'].hasOwnProperty(item)) {
+ let singleQueue = {
+ 'queue-name': 'root.' + item,
+ 'params': {
+ 'entry': {
+ 'key': 'capacity',
+ 'value': updateData['pendingAdd'][item],
+ },
+ },
+ };
+ data.push({'add-queue': singleQueue});
+ }
+ }
+ }
+ if (updateData.hasOwnProperty('pendingUpdate')) {
+ for (let item in updateData['pendingUpdate']) {
+ if (updateData['pendingUpdate'].hasOwnProperty(item)) {
+ let singleQueue = {
+ 'queue-name': 'root.' + item,
+ 'params': {
+ 'entry': {
+ 'key': 'capacity',
+ 'value': updateData['pendingUpdate'][item],
+ },
+ },
+ };
+ data.push({'update-queue': singleQueue});
+ }
+ }
+ }
+ if (updateData.hasOwnProperty('pendingStop')) {
+ for (let item in updateData['pendingStop']) {
+ if (updateData['pendingStop'].hasOwnProperty(item)) {
+ let singleQueue = {
+ 'queue-name': 'root.' + item,
+ 'params': {
+ 'entry': {
+ 'key': 'state',
+ 'value': 'STOPPED',
+ },
+ },
+ };
+ data.push({'update-queue': singleQueue});
+ }
+ }
+ }
+ if (updateData.hasOwnProperty('pendingActive')) {
+ for (let item in updateData['pendingActive']) {
+ if (updateData['pendingActive'].hasOwnProperty(item)) {
+ let singleQueue = {
+ 'queue-name': 'root.' + item,
+ 'params': {
+ 'entry': {
+ 'key': 'state',
+ 'value': 'RUNNING',
+ },
+ },
+ };
+ data.push({'update-queue': singleQueue});
+ }
+ }
+ }
+ if (updateData.hasOwnProperty('pendingRemove')) {
+ for (let item in updateData['pendingRemove']) {
+ if (updateData['pendingRemove'].hasOwnProperty(item)) {
+ data.push({'remove-queue': 'root.' + item});
+ }
+ }
+ }
+ return jsonBuilder.buildObject(data);
+ }
+
+ sendUpdateInfo(updateXml, callback) {
+ unirest.put(yarnConfig.yarnVcUpdatePath)
+ .headers(yarnConfig.webserviceUpdateQueueHeaders)
+ .send(updateXml)
+ .end((res) => {
+ if (res.ok) {
+ return callback(null);
+ } else {
+ return callback(createError('Internal Server Error', 'UnknownError', res.body));
+ }
+ });
+ }
+
+ getVc(vcName, callback) {
+ this.getVcList((vcList, err) => {
+ if (err) {
+ return callback(err);
+ } else if (!vcList) {
+ // Unreachable
+ logger.warn('list virtual clusters error, no virtual cluster found');
+ } else if (!vcList.hasOwnProperty(vcName)) {
+ return callback(null, createError('Not Found', 'NoVirtualClusterError', `Vc ${vcName} not found`));
+ } else {
+ return callback(vcList[vcName], null);
+ }
+ });
+ }
+
+ updateVc(vcName, capacity, callback) {
+ this.getVcList((vcList, err) => {
+ if (err) {
+ return callback(err);
+ } else if (!vcList) {
+ // Unreachable
+ logger.warn('list virtual clusters error, no virtual cluster found');
+ } else {
+ if (!vcList.hasOwnProperty('default')) {
+ return callback(createError('Not Found', 'NoVirtualClusterError', `No default vc found, can't allocate quota`));
+ } else {
+ let defaultQuotaIfUpdated = vcList['default']['capacity'] + (vcList[vcName] ? vcList[vcName]['capacity'] : 0) - capacity;
+ if (defaultQuotaIfUpdated < 0) {
+ return callback(createError('Forbidden', 'NoEnoughQuotaError', `No enough quota`));
+ }
+
+ let data = {'pendingAdd': {}, 'pendingUpdate': {}};
+ if (vcList.hasOwnProperty(vcName)) {
+ data['pendingUpdate'][vcName] = capacity;
+ } else {
+ data['pendingAdd'][vcName] = capacity;
+ }
+ data['pendingUpdate']['default'] = defaultQuotaIfUpdated;
+
+ // logger.debug('raw data to generate: ', data);
+ const vcdataXml = this.generateUpdateInfo(data);
+ // logger.debug('Xml send to yarn: ', vcdataXml);
+ this.sendUpdateInfo(vcdataXml, (err) => {
+ if (err) {
+ return callback(err);
+ } else {
+ return callback(null);
+ }
+ });
+ }
+ }
+ });
+ }
+
+ stopVc(vcName, callback) {
+ this.getVcList((vcList, err) => {
+ if (err) {
+ return callback(err);
+ } else if (!vcList) {
+ // Unreachable
+ logger.warn('list virtual clusters error, no virtual cluster found');
+ } else {
+ if (!vcList.hasOwnProperty(vcName)) {
+ return callback(createError('Not Found', 'NoVirtualClusterError', `Vc ${vcName} not found, can't stop`));
+ } else {
+ let data = {'pendingStop': {}};
+ data['pendingStop'][vcName] = null;
+
+ // logger.debug('raw data to generate: ', data);
+ const vcdataXml = this.generateUpdateInfo(data);
+ // logger.debug('Xml send to yarn: ', vcdataXml);
+ this.sendUpdateInfo(vcdataXml, (err) => {
+ if (err) {
+ return callback(err);
+ } else {
+ return callback(null);
+ }
+ });
+ }
+ }
+ });
+ }
+
+ activeVc(vcName, callback) {
+ this.getVcList((vcList, err) => {
+ if (err) {
+ return callback(err);
+ } else if (!vcList) {
+ // Unreachable
+ logger.warn('list virtual clusters error, no virtual cluster found');
+ } else {
+ if (!vcList.hasOwnProperty(vcName)) {
+ return callback(createError('Not Found', 'NoVirtualClusterError', `Vc ${vcName} not found, can't active`));
+ } else {
+ let data = {'pendingActive': {}};
+ data['pendingActive'][vcName] = null;
+
+ // logger.debug('raw data to generate: ', data);
+ const vcdataXml = this.generateUpdateInfo(data);
+ // logger.debug('Xml send to yarn: ', vcdataXml);
+ this.sendUpdateInfo(vcdataXml, (err) => {
+ if (err) {
+ return callback(err);
+ } else {
+ return callback(null);
+ }
+ });
+ }
+ }
+ });
+ }
+
+ removeVc(vcName, callback) {
+ this.getVcList((vcList, err) => {
+ if (err) {
+ return callback(err);
+ } else if (!vcList) {
+ // Unreachable
+ logger.warn('list virtual clusters error, no virtual cluster found');
+ } else {
+ if (!vcList.hasOwnProperty('default')) {
+ return callback(createError('Not Found', 'NoVirtualClusterError', `No default vc found, can't free quota`));
+ } else if (!vcList.hasOwnProperty(vcName)) {
+ return callback(createError('Not Found', 'NoVirtualClusterError', `Can't delete a nonexistent vc ${vcName}`));
+ } else {
+ this.stopVc(vcName, (err) => {
+ if (err) {
+ return callback(err);
+ } else {
+ let defaultQuotaIfUpdated = vcList['default']['capacity'] + vcList[vcName]['capacity'];
+ let data = {'pendingRemove': {}, 'pendingUpdate': {}};
+ data['pendingUpdate'][vcName] = 0;
+ data['pendingUpdate']['default'] = defaultQuotaIfUpdated;
+ data['pendingRemove'][vcName] = null;
+ // logger.debug('Raw data to generate: ', data);
+ const vcdataXml = this.generateUpdateInfo(data);
+ // logger.debug('Xml send to yarn: ', vcdataXml);
+ this.sendUpdateInfo(vcdataXml, (err) => {
+ if (err) {
+ this.activeVc(vcName, (errInfo) => {
+ if (errInfo) {
+ return callback(errInfo);
+ } else {
+ return callback(err);
+ }
+ });
+ } else {
+ return callback(null);
+ }
+ });
+ }
+ });
+ }
+ }
+ });
+ }
}
// module exports
diff --git a/src/rest-server/src/routes/vc.js b/src/rest-server/src/routes/vc.js
index ca68b1537e..8900aee636 100644
--- a/src/rest-server/src/routes/vc.js
+++ b/src/rest-server/src/routes/vc.js
@@ -16,6 +16,9 @@
// module dependencies
const express = require('express');
const vcController = require('../controllers/vc');
+const token = require('../middlewares/token');
+const param = require('../middlewares/parameter');
+const vcConfig = require('../config/vc');
const router = new express.Router();
@@ -23,12 +26,22 @@ router.route('/')
/** GET /api/v1/virtual-clusters - Return cluster virtual cluster info */
.get(vcController.list);
+
router.route('/:vcName')
/** GET /api/v1/virtual-clusters/vcName - Return cluster specified virtual cluster info */
- .get(vcController.get);
+ .get(vcController.get)
+ /** PUT /api/v1/virtual-clusters/vcName - Create a vc */
+ .put(token.check, param.validate(vcConfig.vcPutInputSchema), vcController.update)
+ /** DELETE /api/v1/virtual-clusters/vcName - Remove a vc */
+ .delete(token.check, vcController.remove);
+
+
+router.route('/:vcName/status')
+ /** PUT /api/v1/virtual-clusters/vcName - Change vc status (running or stopped) */
+ .put(token.check, param.validate(vcConfig.vcStatusPutInputSchema), vcController.updateStatus);
+
-/** Load virtual cluster when API with vcName route parameter is hit */
-router.param('vcName', vcController.load);
+router.param('vcName', vcController.validate);
// module exports
module.exports = router;
diff --git a/src/rest-server/src/util/error.d.ts b/src/rest-server/src/util/error.d.ts
index 5e91e5239c..963d4e1281 100644
--- a/src/rest-server/src/util/error.d.ts
+++ b/src/rest-server/src/util/error.d.ts
@@ -29,6 +29,7 @@ declare type Code =
'BadConfigurationError' |
'ConflictJobError' |
'ConflictUserError' |
+ 'ConflictVcError' |
'ForbiddenUserError' |
'IncorrectPasswordError' |
'InvalidParametersError' |
@@ -41,6 +42,7 @@ declare type Code =
'ReadOnlyJobError' |
'RemoveAdminError' |
'UnauthorizedUserError' |
+ 'NoEnoughQuotaError' |
'UnknownError';
declare function createError(status: Status, code: Code, message: string): HttpError;
diff --git a/src/rest-server/test/jobSubmission.js b/src/rest-server/test/jobSubmission.js
index 1fdf854bae..b5845b2687 100644
--- a/src/rest-server/test/jobSubmission.js
+++ b/src/rest-server/test/jobSubmission.js
@@ -126,16 +126,24 @@ describe('Submit job: POST /api/v1/user/:username/jobs', () => {
'queueName': 'default',
'state': 'RUNNING',
'type': 'capacitySchedulerLeafQueueInfo',
+ "absoluteCapacity": 30.000002,
+ "absoluteMaxCapacity": 100,
},
{
'queueName': 'vc1',
'state': 'RUNNING',
'type': 'capacitySchedulerLeafQueueInfo',
+ "capacity": 50.000002,
+ "absoluteCapacity": 0,
+ "absoluteMaxCapacity": 100,
},
{
'queueName': 'vc2',
'state': 'RUNNING',
'type': 'capacitySchedulerLeafQueueInfo',
+ "capacity": 19.999996,
+ "absoluteCapacity": 0,
+ "absoluteMaxCapacity": 100,
}
]
},
@@ -165,16 +173,24 @@ describe('Submit job: POST /api/v1/user/:username/jobs', () => {
'queueName': 'default',
'state': 'RUNNING',
'type': 'capacitySchedulerLeafQueueInfo',
+ "absoluteCapacity": 30.000002,
+ "absoluteMaxCapacity": 100,
},
{
'queueName': 'vc1',
'state': 'RUNNING',
'type': 'capacitySchedulerLeafQueueInfo',
+ "capacity": 50.000002,
+ "absoluteCapacity": 0,
+ "absoluteMaxCapacity": 100,
},
{
'queueName': 'vc2',
'state': 'RUNNING',
'type': 'capacitySchedulerLeafQueueInfo',
+ "capacity": 19.999996,
+ "absoluteCapacity": 0,
+ "absoluteMaxCapacity": 100,
}
]
},
@@ -208,35 +224,43 @@ describe('Submit job: POST /api/v1/user/:username/jobs', () => {
{}
);
- nock(yarnUri)
- .get('/ws/v1/cluster/scheduler')
- .reply(200, {
- 'scheduler': {
- 'schedulerInfo': {
- 'queues': {
- 'queue': [
- {
- 'queueName': 'default',
- 'state': 'RUNNING',
- 'type': 'capacitySchedulerLeafQueueInfo',
- },
- {
- 'queueName': 'vc1',
- 'state': 'RUNNING',
- 'type': 'capacitySchedulerLeafQueueInfo',
- },
- {
- 'queueName': 'vc2',
- 'state': 'RUNNING',
- 'type': 'capacitySchedulerLeafQueueInfo',
- }
- ]
- },
- 'type': 'capacityScheduler',
- 'usedCapacity': 0.0
- }
+ nock(yarnUri)
+ .get('/ws/v1/cluster/scheduler')
+ .reply(200, {
+ 'scheduler': {
+ 'schedulerInfo': {
+ 'queues': {
+ 'queue': [
+ {
+ 'queueName': 'default',
+ 'state': 'RUNNING',
+ 'type': 'capacitySchedulerLeafQueueInfo',
+ "absoluteCapacity": 30.000002,
+ "absoluteMaxCapacity": 100,
+ },
+ {
+ 'queueName': 'vc1',
+ 'state': 'RUNNING',
+ 'type': 'capacitySchedulerLeafQueueInfo',
+ "capacity": 50.000002,
+ "absoluteCapacity": 0,
+ "absoluteMaxCapacity": 100,
+ },
+ {
+ 'queueName': 'vc2',
+ 'state': 'RUNNING',
+ 'type': 'capacitySchedulerLeafQueueInfo',
+ "capacity": 19.999996,
+ "absoluteCapacity": 0,
+ "absoluteMaxCapacity": 100,
+ }
+ ]
+ },
+ 'type': 'capacityScheduler',
+ 'usedCapacity': 0.0
}
- });
+ }
+ });
//
// Mock etcd return result
diff --git a/src/rest-server/test/userManagement.js b/src/rest-server/test/userManagement.js
index ec88f7d794..2e274476e1 100644
--- a/src/rest-server/test/userManagement.js
+++ b/src/rest-server/test/userManagement.js
@@ -100,16 +100,24 @@ describe('Add new user: put /api/v1/user', () => {
'queueName': 'default',
'state': 'RUNNING',
'type': 'capacitySchedulerLeafQueueInfo',
+ "absoluteCapacity": 30.000002,
+ "absoluteMaxCapacity": 100,
},
{
'queueName': 'vc1',
'state': 'RUNNING',
'type': 'capacitySchedulerLeafQueueInfo',
+ "capacity": 50.000002,
+ "absoluteCapacity": 0,
+ "absoluteMaxCapacity": 100,
},
{
'queueName': 'vc2',
'state': 'RUNNING',
'type': 'capacitySchedulerLeafQueueInfo',
+ "capacity": 19.999996,
+ "absoluteCapacity": 0,
+ "absoluteMaxCapacity": 100,
}
]
},
@@ -587,16 +595,24 @@ describe('update user virtual cluster : put /api/v1/user/:username/virtualCluste
'queueName': 'default',
'state': 'RUNNING',
'type': 'capacitySchedulerLeafQueueInfo',
+ "absoluteCapacity": 30.000002,
+ "absoluteMaxCapacity": 100,
},
{
'queueName': 'vc1',
'state': 'RUNNING',
'type': 'capacitySchedulerLeafQueueInfo',
+ "capacity": 50.000002,
+ "absoluteCapacity": 0,
+ "absoluteMaxCapacity": 100,
},
{
'queueName': 'vc2',
'state': 'RUNNING',
'type': 'capacitySchedulerLeafQueueInfo',
+ "capacity": 19.999996,
+ "absoluteCapacity": 0,
+ "absoluteMaxCapacity": 100,
}
]
},
diff --git a/src/rest-server/test/vc.js b/src/rest-server/test/vc.js
index bb5be39515..de94dbdaf3 100644
--- a/src/rest-server/test/vc.js
+++ b/src/rest-server/test/vc.js
@@ -15,270 +15,546 @@
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-// test
-describe('VC API /api/v1/virtual-clusters', () => {
- // Mock yarn rest api
- beforeEach(() => {
- nock(yarnUri)
- .get('/ws/v1/cluster/scheduler')
- .reply(200, {
- "scheduler": {
- "schedulerInfo": {
- "capacity": 100.0,
- "maxCapacity": 100.0,
+
+const yarnDefaultResponse = {
+ "scheduler": {
+ "schedulerInfo": {
+ "type": "capacityScheduler",
+ "capacity": 100,
+ "usedCapacity": 0,
+ "maxCapacity": 100,
"queueName": "root",
"queues": {
- "queue": [
- {
- "absoluteCapacity": 10.5,
- "absoluteMaxCapacity": 50.0,
- "absoluteUsedCapacity": 0.0,
- "capacity": 10.5,
- "maxCapacity": 50.0,
- "numApplications": 0,
- "queueName": "a",
- "queues": {
- "queue": [
- {
- "absoluteCapacity": 3.15,
- "absoluteMaxCapacity": 25.0,
- "absoluteUsedCapacity": 0.0,
+ "queue": [
+ {
+ "type": "capacitySchedulerLeafQueueInfo",
"capacity": 30.000002,
- "maxCapacity": 50.0,
+ "usedCapacity": 0,
+ "maxCapacity": 100,
+ "absoluteCapacity": 30.000002,
+ "absoluteMaxCapacity": 100,
+ "absoluteUsedCapacity": 0,
"numApplications": 0,
- "queueName": "a1",
- "queues": {
- "queue": [
- {
- "absoluteCapacity": 2.6775,
- "absoluteMaxCapacity": 25.0,
- "absoluteUsedCapacity": 0.0,
- "capacity": 85.0,
- "maxActiveApplications": 1,
- "maxActiveApplicationsPerUser": 1,
- "maxApplications": 267,
- "maxApplicationsPerUser": 267,
- "maxCapacity": 100.0,
- "numActiveApplications": 0,
- "numApplications": 0,
- "numContainers": 0,
- "numPendingApplications": 0,
- "queueName": "a1a",
- "resourcesUsed": {
- "memory": 0,
- "vCores": 0
- },
- "state": "RUNNING",
- "type": "capacitySchedulerLeafQueueInfo",
- "usedCapacity": 0.0,
- "usedResources": "",
- "userLimit": 100,
- "userLimitFactor": 1.0,
- "users": null
- },
- {
- "absoluteCapacity": 0.47250003,
- "absoluteMaxCapacity": 25.0,
- "absoluteUsedCapacity": 0.0,
- "capacity": 15.000001,
- "maxActiveApplications": 1,
- "maxActiveApplicationsPerUser": 1,
- "maxApplications": 47,
- "maxApplicationsPerUser": 47,
- "maxCapacity": 100.0,
- "numActiveApplications": 0,
- "numApplications": 0,
- "numContainers": 0,
- "numPendingApplications": 0,
- "queueName": "a1b",
- "resourcesUsed": {
- "memory": 0,
- "vCores": 0
- },
- "state": "RUNNING",
- "type": "capacitySchedulerLeafQueueInfo",
- "usedCapacity": 0.0,
- "usedResources": "",
- "userLimit": 100,
- "userLimitFactor": 1.0,
- "users": null
- }
- ]
- },
+ "queueName": "a",
+ "state": "RUNNING",
"resourcesUsed": {
- "memory": 0,
- "vCores": 0
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "hideReservationQueues": false,
+ "nodeLabels": [
+ "*"
+ ],
+ "allocatedContainers": 0,
+ "reservedContainers": 0,
+ "pendingContainers": 0,
+ "capacities": {
+ "queueCapacitiesByPartition": [
+ {
+ "partitionName": "",
+ "capacity": 30.000002,
+ "usedCapacity": 0,
+ "maxCapacity": 100,
+ "absoluteCapacity": 30.000002,
+ "absoluteUsedCapacity": 0,
+ "absoluteMaxCapacity": 100,
+ "maxAMLimitPercentage": 100
+ }
+ ]
+ },
+ "resources": {
+ "resourceUsagesByPartition": [
+ {
+ "partitionName": "",
+ "used": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "reserved": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "pending": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "amUsed": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "amLimit": {
+ "memory": 15360,
+ "vCores": 8,
+ "GPUs": 0
+ }
+ }
+ ]
},
- "state": "RUNNING",
- "usedCapacity": 0.0,
- "usedResources": ""
- },
- {
- "absoluteCapacity": 7.35,
- "absoluteMaxCapacity": 50.0,
- "absoluteUsedCapacity": 0.0,
- "capacity": 70.0,
- "maxActiveApplications": 1,
- "maxActiveApplicationsPerUser": 100,
- "maxApplications": 735,
- "maxApplicationsPerUser": 73500,
- "maxCapacity": 100.0,
"numActiveApplications": 0,
- "numApplications": 0,
- "numContainers": 0,
"numPendingApplications": 0,
- "queueName": "a2",
- "resourcesUsed": {
+ "numContainers": 0,
+ "maxApplications": 3000,
+ "maxApplicationsPerUser": 3000,
+ "userLimit": 100,
+ "users": null,
+ "userLimitFactor": 1,
+ "AMResourceLimit": {
+ "memory": 15360,
+ "vCores": 8,
+ "GPUs": 0
+ },
+ "usedAMResource": {
"memory": 0,
- "vCores": 0
+ "vCores": 0,
+ "GPUs": 0
},
- "state": "RUNNING",
+ "userAMResourceLimit": {
+ "memory": 15360,
+ "vCores": 8,
+ "GPUs": 0
+ },
+ "preemptionDisabled": false,
+ "defaultPriority": 0
+ },
+ {
"type": "capacitySchedulerLeafQueueInfo",
- "usedCapacity": 0.0,
- "usedResources": "",
+ "capacity": 70,
+ "usedCapacity": 0,
+ "maxCapacity": 100,
+ "absoluteCapacity": 70,
+ "absoluteMaxCapacity": 100,
+ "absoluteUsedCapacity": 0,
+ "numApplications": 0,
+ "queueName": "default",
+ "state": "RUNNING",
+ "resourcesUsed": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "hideReservationQueues": false,
+ "nodeLabels": [
+ "*"
+ ],
+ "allocatedContainers": 0,
+ "reservedContainers": 0,
+ "pendingContainers": 0,
+ "capacities": {
+ "queueCapacitiesByPartition": [
+ {
+ "partitionName": "",
+ "capacity": 70,
+ "usedCapacity": 0,
+ "maxCapacity": 100,
+ "absoluteCapacity": 70,
+ "absoluteUsedCapacity": 0,
+ "absoluteMaxCapacity": 100,
+ "maxAMLimitPercentage": 100
+ }
+ ]
+ },
+ "resources": {
+ "resourceUsagesByPartition": [
+ {
+ "partitionName": "",
+ "used": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "reserved": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "pending": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "amUsed": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "amLimit": {
+ "memory": 15360,
+ "vCores": 8,
+ "GPUs": 0
+ }
+ }
+ ]
+ },
+ "numActiveApplications": 0,
+ "numPendingApplications": 0,
+ "numContainers": 0,
+ "maxApplications": 7000,
+ "maxApplicationsPerUser": 7000,
"userLimit": 100,
- "userLimitFactor": 100.0,
- "users": null
+ "users": null,
+ "userLimitFactor": 100,
+ "AMResourceLimit": {
+ "memory": 15360,
+ "vCores": 8,
+ "GPUs": 0
+ },
+ "usedAMResource": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "userAMResourceLimit": {
+ "memory": 15360,
+ "vCores": 8,
+ "GPUs": 0
+ },
+ "preemptionDisabled": false,
+ "defaultPriority": 0
}
]
- },
- "resourcesUsed": {
- "memory": 0,
- "vCores": 0
- },
- "state": "RUNNING",
- "usedCapacity": 0.0,
- "usedResources": ""
+ },
+ "capacities": {
+ "queueCapacitiesByPartition": [
+ {
+ "partitionName": "",
+ "capacity": 100,
+ "usedCapacity": 0,
+ "maxCapacity": 100,
+ "absoluteCapacity": 100,
+ "absoluteUsedCapacity": 0,
+ "absoluteMaxCapacity": 100,
+ "maxAMLimitPercentage": 0
+ }
+ ]
+ },
+ "health": {
+ "lastrun": 1543912751445,
+ "operationsInfo": {
+ "entry": {
+ "key": "last-release",
+ "value": {
+ "nodeId": "N/A",
+ "containerId": "N/A",
+ "queue": "N/A"
+ }
+ }
},
- {
- "absoluteCapacity": 89.5,
- "absoluteMaxCapacity": 100.0,
- "absoluteUsedCapacity": 0.0,
- "capacity": 89.5,
- "maxCapacity": 100.0,
- "numApplications": 2,
- "queueName": "b",
- "queues": {
- "queue": [
- {
- "absoluteCapacity": 53.7,
- "absoluteMaxCapacity": 100.0,
- "absoluteUsedCapacity": 0.0,
- "capacity": 60.000004,
- "maxActiveApplications": 1,
- "maxActiveApplicationsPerUser": 100,
- "maxApplications": 5370,
- "maxApplicationsPerUser": 537000,
- "maxCapacity": 100.0,
- "numActiveApplications": 1,
- "numApplications": 2,
- "numContainers": 0,
- "numPendingApplications": 1,
- "queueName": "b1",
- "resourcesUsed": {
- "memory": 0,
- "vCores": 0
- },
- "state": "RUNNING",
- "type": "capacitySchedulerLeafQueueInfo",
- "usedCapacity": 0.0,
- "usedResources": "",
- "userLimit": 100,
- "userLimitFactor": 100.0,
- "users": {
- "user": [
- {
- "numActiveApplications": 0,
- "numPendingApplications": 1,
- "resourcesUsed": {
- "memory": 0,
- "vCores": 0
- },
- "username": "user2"
- },
- {
- "numActiveApplications": 1,
- "numPendingApplications": 0,
- "resourcesUsed": {
- "memory": 0,
- "vCores": 0
- },
- "username": "user1"
- }
- ]
+ "lastRunDetails": [
+ {
+ "operation": "releases",
+ "count": 0,
+ "resources": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ }
+ },
+ {
+ "operation": "allocations",
+ "count": 0,
+ "resources": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
}
},
- {
- "absoluteCapacity": 35.3525,
- "absoluteMaxCapacity": 100.0,
- "absoluteUsedCapacity": 0.0,
- "capacity": 39.5,
- "maxActiveApplications": 1,
- "maxActiveApplicationsPerUser": 100,
- "maxApplications": 3535,
- "maxApplicationsPerUser": 353500,
- "maxCapacity": 100.0,
- "numActiveApplications": 123,
+ {
+ "operation": "reservations",
+ "count": 0,
+ "resources": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ }
+ }
+ ]
+ }
+ }
+ }
+};
+
+const yarnErrorResponse = {
+ "scheduler": {
+ "schedulerInfo": {
+ "type": "capacityScheduler",
+ "capacity": 100,
+ "usedCapacity": 0,
+ "maxCapacity": 100,
+ "queueName": "root",
+ "queues": {
+ "queue": [
+ {
+ "type": "capacitySchedulerLeafQueueInfo",
+ "capacity": 30.000002,
+ "usedCapacity": 0,
+ "maxCapacity": 100,
+ "absoluteCapacity": 30.000002,
+ "absoluteMaxCapacity": 100,
+ "absoluteUsedCapacity": 0,
"numApplications": 0,
- "numContainers": 0,
- "numPendingApplications": 0,
- "queueName": "b2",
+ "queueName": "a",
+ "state": "RUNNING",
"resourcesUsed": {
- "memory": 0,
- "vCores": 0
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "hideReservationQueues": false,
+ "nodeLabels": [
+ "*"
+ ],
+ "allocatedContainers": 0,
+ "reservedContainers": 0,
+ "pendingContainers": 0,
+ "capacities": {
+ "queueCapacitiesByPartition": [
+ {
+ "partitionName": "",
+ "capacity": 30.000002,
+ "usedCapacity": 0,
+ "maxCapacity": 100,
+ "absoluteCapacity": 30.000002,
+ "absoluteUsedCapacity": 0,
+ "absoluteMaxCapacity": 100,
+ "maxAMLimitPercentage": 100
+ }
+ ]
+ },
+ "resources": {
+ "resourceUsagesByPartition": [
+ {
+ "partitionName": "",
+ "used": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "reserved": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "pending": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "amUsed": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "amLimit": {
+ "memory": 15360,
+ "vCores": 8,
+ "GPUs": 0
+ }
+ }
+ ]
},
- "state": "RUNNING",
- "type": "capacitySchedulerLeafQueueInfo",
- "usedCapacity": 0.0,
- "usedResources": "",
- "userLimit": 100,
- "userLimitFactor": 100.0,
- "users": null
- },
- {
- "absoluteCapacity": 0.4475,
- "absoluteMaxCapacity": 100.0,
- "absoluteUsedCapacity": 0.0,
- "capacity": 0.5,
- "maxActiveApplications": 1,
- "maxActiveApplicationsPerUser": 100,
- "maxApplications": 44,
- "maxApplicationsPerUser": 4400,
- "maxCapacity": 100.0,
"numActiveApplications": 0,
- "numApplications": 0,
- "numContainers": 0,
"numPendingApplications": 0,
- "queueName": "b3",
- "resourcesUsed": {
- "memory": 0,
- "vCores": 0
+ "numContainers": 0,
+ "maxApplications": 3000,
+ "maxApplicationsPerUser": 3000,
+ "userLimit": 100,
+ "users": null,
+ "userLimitFactor": 1,
+ "AMResourceLimit": {
+ "memory": 15360,
+ "vCores": 8,
+ "GPUs": 0
},
- "state": "RUNNING",
+ "usedAMResource": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "userAMResourceLimit": {
+ "memory": 15360,
+ "vCores": 8,
+ "GPUs": 0
+ },
+ "preemptionDisabled": false,
+ "defaultPriority": 0
+ },
+ {
"type": "capacitySchedulerLeafQueueInfo",
- "usedCapacity": 0.0,
- "usedResources": "",
+ "capacity": 70,
+ "usedCapacity": 0,
+ "maxCapacity": 100,
+ "absoluteCapacity": 70,
+ "absoluteMaxCapacity": 100,
+ "absoluteUsedCapacity": 0,
+ "numApplications": 0,
+ "queueName": "b",
+ "state": "RUNNING",
+ "resourcesUsed": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "hideReservationQueues": false,
+ "nodeLabels": [
+ "*"
+ ],
+ "allocatedContainers": 0,
+ "reservedContainers": 0,
+ "pendingContainers": 0,
+ "capacities": {
+ "queueCapacitiesByPartition": [
+ {
+ "partitionName": "",
+ "capacity": 70,
+ "usedCapacity": 0,
+ "maxCapacity": 100,
+ "absoluteCapacity": 70,
+ "absoluteUsedCapacity": 0,
+ "absoluteMaxCapacity": 100,
+ "maxAMLimitPercentage": 100
+ }
+ ]
+ },
+ "resources": {
+ "resourceUsagesByPartition": [
+ {
+ "partitionName": "",
+ "used": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "reserved": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "pending": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "amUsed": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "amLimit": {
+ "memory": 15360,
+ "vCores": 8,
+ "GPUs": 0
+ }
+ }
+ ]
+ },
+ "numActiveApplications": 0,
+ "numPendingApplications": 0,
+ "numContainers": 0,
+ "maxApplications": 7000,
+ "maxApplicationsPerUser": 7000,
"userLimit": 100,
- "userLimitFactor": 100.0,
- "users": null
- }
- ]
- },
- "resourcesUsed": {
- "memory": 0,
- "vCores": 0
- },
- "state": "RUNNING",
- "usedCapacity": 0.0,
- "usedResources": ""
- }
- ]
+ "users": null,
+ "userLimitFactor": 100,
+ "AMResourceLimit": {
+ "memory": 15360,
+ "vCores": 8,
+ "GPUs": 0
+ },
+ "usedAMResource": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ },
+ "userAMResourceLimit": {
+ "memory": 15360,
+ "vCores": 8,
+ "GPUs": 0
+ },
+ "preemptionDisabled": false,
+ "defaultPriority": 0
+ }
+ ]
},
- "type": "capacityScheduler",
- "usedCapacity": 0.0
- }
+ "capacities": {
+ "queueCapacitiesByPartition": [
+ {
+ "partitionName": "",
+ "capacity": 100,
+ "usedCapacity": 0,
+ "maxCapacity": 100,
+ "absoluteCapacity": 100,
+ "absoluteUsedCapacity": 0,
+ "absoluteMaxCapacity": 100,
+ "maxAMLimitPercentage": 0
+ }
+ ]
+ },
+ "health": {
+ "lastrun": 1543912751445,
+ "operationsInfo": {
+ "entry": {
+ "key": "last-release",
+ "value": {
+ "nodeId": "N/A",
+ "containerId": "N/A",
+ "queue": "N/A"
+ }
+ }
+ },
+ "lastRunDetails": [
+ {
+ "operation": "releases",
+ "count": 0,
+ "resources": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ }
+ },
+ {
+ "operation": "allocations",
+ "count": 0,
+ "resources": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ }
+ },
+ {
+ "operation": "reservations",
+ "count": 0,
+ "resources": {
+ "memory": 0,
+ "vCores": 0,
+ "GPUs": 0
+ }
+ }
+ ]
+ }
}
- });
+ }
+};
+
+
+// test
+describe('VC API Get /api/v1/virtual-clusters', () => {
+ // Mock yarn rest api
+ beforeEach(() => {
+ nock(yarnUri)
+ .get('/ws/v1/cluster/scheduler')
+ .reply(200, yarnDefaultResponse);
+ });
+
+ afterEach(function() {
+ if (!nock.isDone()) {
+ nock.cleanAll();
+ throw new Error('Not all nock interceptors were used!');
+ }
});
// GET /api/v1/virtual-clusters
@@ -288,8 +564,8 @@ describe('VC API /api/v1/virtual-clusters', () => {
.end((err, res) => {
expect(res, 'status code').to.have.status(200);
expect(res, 'json response').be.json;
- expect(res.body).to.have.property('a1a');
- expect(res.body).to.nested.include({'b2.numActiveJobs': 123});
+ expect(res.body).to.have.property('a');
+ expect(res.body).to.nested.include({'default.status': "RUNNING"});
done();
});
});
@@ -298,11 +574,11 @@ describe('VC API /api/v1/virtual-clusters', () => {
// get exist vc info
it('should return virtual cluster info', (done) => {
chai.request(server)
- .get('/api/v1/virtual-clusters/b3')
+ .get('/api/v1/virtual-clusters/a')
.end((err, res) => {
expect(res, 'status code').to.have.status(200);
expect(res, 'json response').be.json;
- expect(res.body).to.have.property('capacity', 0.4475);
+ expect(res.body).to.have.property('capacity', 30);
done();
});
});
@@ -355,3 +631,410 @@ describe('VC API /api/v1/virtual-clusters', () => {
});
});
});
+
+
+describe('VC API PUT /api/v1/virtual-clusters', () => {
+
+ const userToken = jwt.sign({username: 'test_user', admin: false}, process.env.JWT_SECRET, {expiresIn: 60});
+ const adminToken = jwt.sign({username: 'test_admin', admin: true}, process.env.JWT_SECRET, {expiresIn: 60});
+ // Mock yarn rest api
+ beforeEach(() => {
+ nock.cleanAll();
+ nock(yarnUri)
+ .get('/ws/v1/cluster/scheduler')
+ .reply(200, yarnDefaultResponse);
+ });
+
+
+ it('[Positive] should add vc b', (done) => {
+ nock(yarnUri)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(200);
+
+ chai.request(server)
+ .put('/api/v1/virtual-clusters/b')
+ .set('Authorization', `Bearer ${adminToken}`)
+ .send({
+ 'vcCapacity': 30
+ })
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(201);
+ done();
+ });
+ });
+
+ it('[Positive] should update vc a', (done) => {
+ nock(yarnUri)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(200);
+
+ chai.request(server)
+ .put('/api/v1/virtual-clusters/a')
+ .set('Authorization', `Bearer ${adminToken}`)
+ .send({
+ 'vcCapacity': 40
+ })
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(201);
+ done();
+ });
+ });
+
+
+ it('[Negative] should not update vc default', (done) => {
+ chai.request(server)
+ .put('/api/v1/virtual-clusters/default')
+ .set('Authorization', `Bearer ${adminToken}`)
+ .send({
+ 'vcCapacity': 30
+ })
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(403);
+ expect(res.body).to.have.property('code', 'ForbiddenUserError');
+ done();
+ });
+ });
+
+ it('[Negative] Non-admin should not update vc', (done) => {
+ chai.request(server)
+ .put('/api/v1/virtual-clusters/b')
+ .set('Authorization', `Bearer ${userToken}`)
+ .send({
+ 'vcCapacity': 30
+ })
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(403);
+ expect(res.body).to.have.property('code', 'ForbiddenUserError');
+ done();
+ });
+ });
+
+ it('[Negative] Non-admin should not update vc', (done) => {
+ chai.request(server)
+ .put('/api/v1/virtual-clusters/b')
+ .set('Authorization', `Bearer ${userToken}`)
+ .send({
+ 'vcCapacity': 30
+ })
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(403);
+ expect(res.body).to.have.property('code', 'ForbiddenUserError');
+ done();
+ });
+ });
+
+ it('[Negative] should not update vc b with exceed quota', (done) => {
+ chai.request(server)
+ .put('/api/v1/virtual-clusters/b')
+ .set('Authorization', `Bearer ${adminToken}`)
+ .send({
+ 'vcCapacity': 80
+ })
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(403);
+ expect(res.body).to.have.property('code', 'NoEnoughQuotaError');
+ done();
+ });
+ });
+
+ it('[Negative] should not update vc if default vc doesn\'t exist', (done) => {
+ nock.cleanAll();
+ nock(yarnUri)
+ .get('/ws/v1/cluster/scheduler')
+ .reply(200, yarnErrorResponse);
+
+ chai.request(server)
+ .put('/api/v1/virtual-clusters/a')
+ .set('Authorization', `Bearer ${adminToken}`)
+ .send({
+ 'vcCapacity': 80
+ })
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(404);
+ expect(res.body).to.have.property('code', 'NoVirtualClusterError');
+ done();
+ });
+ });
+
+ it('[Negative] should not update vc if upstream error', (done) => {
+ nock(yarnUri)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(404, 'Error response in YARN');
+
+ chai.request(server)
+ .put('/api/v1/virtual-clusters/a')
+ .set('Authorization', `Bearer ${adminToken}`)
+ .send({
+ 'vcCapacity': 80
+ })
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(500);
+ expect(res.body).to.have.property('code', 'UnknownError');
+ expect(res.body).to.have.property('message', 'Error response in YARN');
+ done();
+ });
+ });
+});
+
+
+describe('VC API PUT /api/v1/virtual-clusters/:vcName/status', () => {
+
+ const userToken = jwt.sign({username: 'test_user', admin: false}, process.env.JWT_SECRET, {expiresIn: 60});
+ const adminToken = jwt.sign({username: 'test_admin', admin: true}, process.env.JWT_SECRET, {expiresIn: 60});
+ // Mock yarn rest api
+ beforeEach(() => {
+ nock.cleanAll();
+ nock(yarnUri)
+ .get('/ws/v1/cluster/scheduler')
+ .reply(200, yarnDefaultResponse);
+ });
+
+
+ it('[Positive] should change vc a to stopped', (done) => {
+ nock(yarnUri)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(200);
+
+ chai.request(server)
+ .put('/api/v1/virtual-clusters/a/status')
+ .set('Authorization', `Bearer ${adminToken}`)
+ .send({
+ 'vcStatus': "stopped"
+ })
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(201);
+ done();
+ });
+ });
+
+ it('[Positive] should change vc a to running', (done) => {
+ nock(yarnUri)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(200);
+
+ chai.request(server)
+ .put('/api/v1/virtual-clusters/a/status')
+ .set('Authorization', `Bearer ${adminToken}`)
+ .send({
+ 'vcStatus': "running"
+ })
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(201);
+ done();
+ });
+ });
+
+ it('[Negative] should not change default vc status', (done) => {
+ nock(yarnUri)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(200);
+
+ chai.request(server)
+ .put('/api/v1/virtual-clusters/default/status')
+ .set('Authorization', `Bearer ${adminToken}`)
+ .send({
+ 'vcStatus': "running"
+ })
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(403);
+ expect(res.body).to.have.property('code', 'ForbiddenUserError');
+ done();
+ });
+ });
+
+ it('[Negative] Non-admin should not change vc status', (done) => {
+ nock(yarnUri)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(200);
+
+ chai.request(server)
+ .put('/api/v1/virtual-clusters/a/status')
+ .set('Authorization', `Bearer ${userToken}`)
+ .send({
+ 'vcStatus': "stopped"
+ })
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(403);
+ expect(res.body).to.have.property('code', 'ForbiddenUserError');
+ done();
+ });
+ });
+
+ it('[Negative] should not change a non-exist vc b', (done) => {
+ nock(yarnUri)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(200);
+
+ chai.request(server)
+ .put('/api/v1/virtual-clusters/b/status')
+ .set('Authorization', `Bearer ${adminToken}`)
+ .send({
+ 'vcStatus': 'running'
+ })
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(404);
+ expect(res.body).to.have.property('code', 'NoVirtualClusterError');
+ done();
+ });
+ });
+
+
+ it('[Negative] should not change vc if upstream error', (done) => {
+ nock(yarnUri)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(404, 'Error response in YARN');
+
+ chai.request(server)
+ .put('/api/v1/virtual-clusters/a/status')
+ .set('Authorization', `Bearer ${adminToken}`)
+ .send({
+ 'vcStatus': 'stopped'
+ })
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(500);
+ expect(res.body).to.have.property('code', 'UnknownError');
+ expect(res.body).to.have.property('message', 'Error response in YARN');
+ done();
+ });
+ });
+});
+
+
+describe('VC API DELETE /api/v1/virtual-clusters', () => {
+
+ const userToken = jwt.sign({username: 'test_user', admin: false}, process.env.JWT_SECRET, {expiresIn: 60});
+ const adminToken = jwt.sign({username: 'test_admin', admin: true}, process.env.JWT_SECRET, {expiresIn: 60});
+ // Mock yarn rest api
+ before(() => {
+ nock.cleanAll();
+ nock(yarnUri)
+ .get('/ws/v1/cluster/scheduler')
+ .reply(200, yarnDefaultResponse);
+ });
+
+ beforeEach(() => {
+ nock(yarnUri)
+ .get('/ws/v1/cluster/scheduler')
+ .reply(200, yarnDefaultResponse);
+ });
+
+ afterEach(() => {
+ nock.cleanAll();
+ });
+
+
+ it('[Positive] should delete vc a', (done) => {
+ nock(yarnUri)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(200)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(200);
+
+ chai.request(server)
+ .delete('/api/v1/virtual-clusters/a')
+ .set('Authorization', `Bearer ${adminToken}`)
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(201);
+ done();
+ });
+ });
+
+ it('[Negative] Non-admin should not delete vc a', (done) => {
+ nock(yarnUri)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(200)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(200);
+
+ chai.request(server)
+ .delete('/api/v1/virtual-clusters/a')
+ .set('Authorization', `Bearer ${userToken}`)
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(403);
+ expect(res.body).to.have.property('code', 'ForbiddenUserError');
+ done();
+ });
+ });
+
+ it('[Negative] should not delete vc default', (done) => {
+ nock(yarnUri)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(200)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(200);
+
+ chai.request(server)
+ .delete('/api/v1/virtual-clusters/default')
+ .set('Authorization', `Bearer ${adminToken}`)
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(403);
+ expect(res.body).to.have.property('code', 'ForbiddenUserError');
+ done();
+ });
+ });
+
+ it('[Negative] should not delete a non-exist vc b', (done) => {
+ nock(yarnUri)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(200)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(200);
+
+ chai.request(server)
+ .delete('/api/v1/virtual-clusters/b')
+ .set('Authorization', `Bearer ${adminToken}`)
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(404);
+ expect(res.body).to.have.property('code', 'NoVirtualClusterError');
+ done();
+ });
+ });
+
+ it('[Negative] should not delete a vc when stop queue successfully but fail to delete', (done) => {
+ nock(yarnUri)
+ .get('/ws/v1/cluster/scheduler')
+ .reply(200, yarnDefaultResponse)
+ .get('/ws/v1/cluster/scheduler')
+ .reply(200, yarnDefaultResponse)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(200)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(403, 'Error in YARN when delete queue')
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(200);
+
+ chai.request(server)
+ .delete('/api/v1/virtual-clusters/a')
+ .set('Authorization', `Bearer ${adminToken}`)
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(500);
+ expect(res.body).to.have.property('code', 'UnknownError');
+ expect(res.body).to.have.property('message', 'Error in YARN when delete queue');
+ done();
+ });
+ });
+
+ it('[Negative] Stop queue successfully but fail to delete, then fail to reactive it', (done) => {
+ nock(yarnUri)
+ .get('/ws/v1/cluster/scheduler')
+ .reply(200, yarnDefaultResponse)
+ .get('/ws/v1/cluster/scheduler')
+ .reply(200, yarnDefaultResponse)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(200)
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(403, 'Error in YARN when delete queue')
+ .put('/ws/v1/cluster/scheduler-conf')
+ .reply(404, 'Error in YARN when reactive queue');
+
+ chai.request(server)
+ .delete('/api/v1/virtual-clusters/a')
+ .set('Authorization', `Bearer ${adminToken}`)
+ .end((err, res) => {
+ expect(res, 'status code').to.have.status(500);
+ expect(res.body).to.have.property('code', 'UnknownError');
+ expect(res.body).to.have.property('message', 'Error in YARN when reactive queue');
+ done();
+ });
+ });
+});