server/rest.js
// (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP
import express from 'express';
let router = express.Router();
import ws from "ws";
import stringify from "json-stringify-pretty-compact";
import {
addResource, addSession, deleteResource,
getBackup, getCertificate, getItems, getPreferences, getResource,
getSession, getSettings, getStatus, getSupport, getUpdate,
setBackup, setCertificate, setPreferences, setSettings, setStatus,
setSupport, setUpdate, updateResource
} from './data';
import { addUtilization, generate } from './generator';
import {
filterFilter, filterQuery, filterUserQuery, sortItems
} from './filter';
import { buildMap } from './map';
import ldap from './ldap';
const RESPONSE_DELAY = 0;
const TASK_UPDATE_INTERVAL = 1000;
let latestGruToken = undefined;
// Actions
function filteredItems (items, queryParams) {
if (queryParams.userQuery) {
items = filterUserQuery(items, queryParams.userQuery);
}
if (queryParams.query) {
items = filterQuery(items, queryParams.query);
}
if (queryParams.filter) {
items = filterFilter(items, queryParams.filter);
}
return items;
}
function getIndex (url, queryParams) {
let items = getItems(queryParams.category) || [];
const unfilteredTotal = items.length;
items = filteredItems(items, queryParams);
if (queryParams.sort) {
sortItems(items, queryParams.sort);
}
let startIndex = +queryParams.start; // coerce to be a number
if (queryParams.referenceUri) {
items.some((item, index) => {
if (queryParams.referenceUri === item.uri) {
startIndex = Math.max(index - Math.floor(queryParams.count / 2), 0);
return true;
}
return false;
});
}
// prune for start+count
const total = items.length;
items = items.slice(startIndex, startIndex + queryParams.count);
return {
category: queryParams.category,
start: startIndex,
count: items.length,
total: total,
unfilteredTotal: unfilteredTotal,
items: items
};
}
function updateAggregateCounts (counts, resource, value, intervals) {
let count = null;
for (let i = 0; i < counts.length; i++) {
if (value === counts[i].value) {
count = counts[i];
break;
}
}
if (!count) {
count = {value: value, count: 0};
if (intervals) {
count.intervals = intervals.map(interval => ({ ...interval }));
}
counts.push(count);
}
count.count += 1;
if (count.intervals) {
count.intervals.forEach(interval => {
if (! interval.count) {
interval.count = 0;
}
if (resource.created >= interval.start &&
resource.created <= interval.stop) {
interval.count += 1;
}
});
}
}
function getAggregate (url, queryParams) {
let items = getItems(queryParams.category) || [];
items = filteredItems(items, queryParams);
let attributes;
if (Array.isArray(queryParams.attribute)) {
attributes = queryParams.attribute;
} else {
attributes = [queryParams.attribute];
}
const result = attributes.map(function(attribute) {
return {
attribute: attribute,
counts: []
};
});
let intervals = null;
if (queryParams.interval) {
intervals = [];
let stop = new Date();
stop.setHours(23, 59, 59, 999);
for (let i = 0; i < queryParams.count; i++) {
let start = new Date(stop.getTime() + 1);
start.setDate(start.getDate() - 1);
intervals.push({start: start.toISOString(), stop: stop.toISOString()});
stop = new Date(start.getTime() - 1);
}
}
items.some(resource => {
result.forEach(attributeResult => {
let value;
if (resource.hasOwnProperty(attributeResult.attribute)) {
value = resource[attributeResult.attribute];
} else if (resource.attributes &&
resource.attributes.hasOwnProperty(attributeResult.attribute)) {
value = resource.attributes[attributeResult.attribute];
}
if (undefined !== value) {
updateAggregateCounts(attributeResult.counts,
resource, value, intervals);
}
});
});
return result;
}
// WebSocket interaction
let socketServer;
let connections = [];
function closeConnection (connection) {
if (connection.ws) {
connection.ws.close();
connection.ws = null;
const index = connections.indexOf(connection);
connections.splice(index, 1);
}
}
const MAP_REGEXP = /^\/rest\/index\/trees\/aggregated(.+)$/;
function respondToRequest (connection, request) {
let response = {op: 'update', id: request.id};
try {
if ('/rest/index/resources' === request.uri) {
response.result = getIndex(request.uri, request.params);
} else if ('/rest/index/resources/aggregated' === request.uri) {
response.result = getAggregate(request.uri, request.params);
} else if (MAP_REGEXP.test(request.uri)) {
var uri = MAP_REGEXP.exec(request.uri)[1];
response.result = buildMap(uri);
} else {
let resource = getResource(request.uri);
if (resource) {
response.result = resource;
} else {
response.op = 'error';
response.result = `unknown uri ${request.uri}`;
}
}
} catch (e) {
console.log('!!! exception', e);
response.op = 'error';
response.result = e.message;
}
if (connection.ws) {
const serializedResponse = JSON.stringify(response);
console.log(response.op.toUpperCase(), request.uri,
stringify(request.params), serializedResponse.length);
setTimeout(function () {
connection.ws.send(serializedResponse);
}, RESPONSE_DELAY);
}
// if ('error' === response.op) {
// closeConnection(connection);
// }
}
function parseQuery(string) {
const params = string.split('&');
let result = {};
params.forEach(param => {
const parts = param.split('=');
// if we already have this parameter, it must be an array
if (result[parts[0]]) {
if (! Array.isArray(result[parts[0]])) {
result[parts[0]] = [result[parts[0]]];
}
result[parts[0]].push(decodeURIComponent(parts[1]));
} else {
result[parts[0]] = decodeURIComponent(parts[1]);
}
});
return result;
}
function onMessage (connection, request) {
if ('start' === request.op) {
// Split out query parameters
const parts = request.uri.split('?');
request.uri = parts[0];
request.params = parseQuery(parts[1] || '');
console.log('WATCH', request.uri, request.id, request.params);
connection.requests.push(request);
respondToRequest(connection, request);
} else if ('stop' === request.op) {
console.log('STOP', request.id);
// Remove request
connection.requests = connection.requests.filter(function (req) {
return (req.id !== request.id);
});
} else if ('ping' === request.op) {
const serializedResponse = JSON.stringify({ op: 'ping' });
setTimeout(function () {
connection.ws.send(serializedResponse);
}, RESPONSE_DELAY);
} else {
if (connection.ws) {
connection.ws.send({error: `unknown op ${request.op}`});
closeConnection(connection);
}
}
}
function onConnection (ws) {
const connection = {
ws: ws,
requests: []
};
connections.push(connection);
ws.on('message', function incoming(message) {
onMessage(connection, JSON.parse(message));
});
ws.on("close", function () {
closeConnection(connection);
});
}
function initializeSocket (server, prefix) {
const path = `${prefix}ws`;
socketServer = new ws.Server({server: server, path: path});
console.log(`Listening for web socket connections at ${path}`);
socketServer.on("connection", onConnection);
}
function requestMatches (request, events) {
// If the request category(ies) or the request url match the change, respond
let category;
let uri;
if (request.params && request.params.category) {
category = request.params.category;
} else {
uri = request.uri;
}
return events.some(function (event) {
if (category) {
return (category === event.category ||
(Array.isArray(category) &&
category.indexOf(event.category) !== -1));
} else {
return (uri === event.uri);
}
});
}
function onResourceChange (resource, task) {
let events = [];
if (resource) {
events.push({category: resource.category, uri: resource.uri});
}
if (task) {
events.push({category: task.category, uri: task.uri});
}
connections.forEach(function (connection) {
connection.requests.forEach(function (request) {
if (requestMatches(request, events)) {
respondToRequest(connection, request);
}
});
});
}
// REST interaction
router.get('/status', function(req, res) {
const status = getStatus();
if (status) {
res.json(status);
} else {
res.status(404).send();
}
});
router.put('/status', function(req, res) {
setStatus(req.body);
res.json(null);
});
function roleForUserName (userName) {
if (! userName) {
return null;
} else if ('u' === userName[0].toLowerCase()) {
return 'virtualization user';
} else if ('r' === userName[0].toLowerCase()) {
return 'read only';
} else {
return 'infrastucture administrator';
}
}
router.post('/login-sessions', (req, res) => {
if (! req.body.userName || ! req.body.password ||
'error' === req.body.userName ||
('Gru' === req.body.userName && 'freeze ray' !== req.body.password)) {
res.status(401, "Invalid username or password.").send();
} else {
const token = `${req.body.userName}-${(new Date()).getTime().toString(10)}`;
addSession(token, {
userName: req.body.userName,
role: roleForUserName(req.body.userName),
roles: [roleForUserName(req.body.userName)]
});
if ('Gru' === req.body.userName) {
latestGruToken = token;
}
const preferences = getPreferences(req.body.userName);
const settings = getSettings();
res.json({
sessionID: token,
needPasswordReset: (! settings.network && ! preferences)
});
}
});
router.delete('/login-sessions', (req, res) => {
res.json(null);
});
router.post('/reset-password', (req, res) => {
setPreferences(req.body.userName, 'users', {needPasswordReset: false});
res.json(null);
});
function returnSession (req, res) {
const token = req.headers.auth;
const session = getSession(token);
if (session) {
res.json(session);
} else {
res.status(404).send();
}
}
router.get('/sessions', returnSession);
router.get('/authz/role', returnSession);
router.get('/preferences/index', (req, res) => {
const preferences = getPreferences(req.headers.auth, req.query.category);
if (! preferences) {
res.status(404).send();
} else {
res.json(preferences);
}
});
router.get('/settings', (req, res) => {
const settings = getSettings();
if (settings) {
res.json(settings);
} else {
res.status(404).send();
}
});
router.put('/settings', (req, res) => {
const resource = getResource('/rest/appliances/1');
let settings = req.body;
settings.state = 'done';
settings.modified = (new Date()).toISOString();
// don't persist credentials
delete settings.nodesCommonUserName;
delete settings.nodesCommonPassword;
settings.nodes.forEach(function (node) {
delete node.userName;
delete node.password;
delete node.useCommonCredentials;
// convert manage nodes to managed
node.managed = node.manage;
delete node.manage;
if (! node.managed) {
delete node.address;
}
});
// don't persist errors
delete settings.errors;
setSettings(settings);
if (settings.hypervisor && settings.hypervisor.certifcate) {
setCertificate(settings.hypervisor.address,
settings.hypervisor.certificate);
}
if (settings.directory && settings.directory.certifcate) {
setCertificate(settings.directory.address,
settings.directory.certificate);
}
let task = createTask('appliances', 'Update', resource, req);
res.json({
taskUri: task.uri
});
runTask(task, null, () => {
// simulate an error if the name has "error" in it
if (settings.name.match(/invalid/)) {
task.status = 'Critical';
task.state = 'Error';
task.taskErrors = [{
message: "The appliance-1 IPv4 and IPv4 gateway addresses are not " +
"in the same subnet (255.255.240.0).",
recommendedActions: "Change either address or widen the subnet mask."
}];
settings.errors = {
network: [
{
message: "The appliance-1 IPv4 and IPv4 gateway addresses are " +
"not in the same subnet (255.255.240.0).",
resolution: "Change either address or widen the subnet mask."
},
{
message: "Supplied IP Address 10.0.0.1 is duplicate for one or " +
"more fields.",
resolution: "Specify unique IP Address for each IP Address field."
}
],
hypervisor: [
{
message: "Unable to authenticate vCenter at 10.0.0.1 with the " +
"supplied credentials.",
resolution: "Change the credentials and/or verify the IP address."
}
],
directory: [
{
message: "Unable to authenticate directory at ldap.my.com with " +
"the supplied credentials.",
resolution: "Change the credentials and/or verify the address."
}
],
nodes: [
{
message: "Unable to authenticate the node at 10.0.0.1 with the " +
"supplied credentials.",
resolution: "Change the credentials and/or verify the IP address."
}
]
};
setSettings(settings);
}
setStatus({state: 'ready'});
});
});
router.get('/certificate', (req, res) => {
const address = req.query.address;
let certificate = getCertificate(address);
if (certificate) {
res.json({
result: 'trusted',
certificate: certificate
});
} else {
// simulate delay
setTimeout(() => {
certificate = {
name: 'Hewlett Packard Enterprise Class 3 Certificate (simulated)',
issuedBy: 'Hewlett Packard Enterprise',
expires: '2018-12-20T07:34:00'
};
res.json({
result: 'not trusted',
certificate: certificate
});
}, TASK_UPDATE_INTERVAL);
}
});
router.post('/connection', (req, res) => {
// simulate delay
setTimeout(() => {
if ('error' === req.body.userName || ! req.body.userName ||
! req.body.password) {
res.status(400).json({
message: "Invalid username or password."
});
} else {
res.status(200).send();
}
}, TASK_UPDATE_INTERVAL);
});
router.get('/directory/search', (req, res) => {
ldap(req.query, (result) => {
res.json(result);
}, (error) => {
res.status(500).send();
});
});
// for now, this is the path for performing a restore operation
router.post('/settings', (req, res) => {
const resource = getResource('/rest/appliances/1');
const file = req.files.file;
const settings = JSON.parse(file.data.toString('utf-8', 0, file.size));
setSettings(settings);
let task = createTask('appliances', 'Restore', resource, req);
res.json({
taskUri: task.uri
});
runTask(task, null, () => {
setStatus({state: 'ready'});
});
});
function getOneFrom (path, func) {
router.get(path, (req, res) => {
var object = func();
if (object) {
res.json(object);
} else {
res.status(404).send();
}
});
}
getOneFrom('/update', getUpdate);
router.post('/update/upload', (req, res) => {
const resource = getResource('/rest/appliances/1');
const file = req.files.file;
let task = createTask('appliances', 'Upload software', resource, req);
// we start this task at 50% to account for the time taken uploading
task.percentComplete = 50;
res.json({
taskUri: task.uri
});
runTask(task, resource, () => {
// simulate file contents
const settings = getSettings();
const version =
(Math.round(parseFloat(settings.version, 10) * 10) + 1) / 10;
let update = {
version: version,
file: {name: file.name},
hypervisor: {version: 5.1},
releaseNotes: 'http://ferret.grommet.io/release-notes',
eula: 'http://ferret.grommet.io/eula',
writtenOffer: 'http://ferret.grommet.io/written-offer'
};
// simulate an error if the file name has "invalid" in it
if (update.file.name.match(/invalid/)) {
task.status = 'Critical';
task.state = 'Error';
task.taskErrors = [{
message: 'Simulated checksum or invalid file error.',
recommendedActions: 'Upload another file.'
}];
update.errors = [{
status: 'Critical',
message: 'Simulated checksum or invalid file error.',
resolution: 'Upload another file.',
action: 'upload'
}];
}
setUpdate(update);
});
});
router.post('/update', (req, res) => {
const update = getUpdate();
const resource = getResource('/rest/appliances/1');
// simulate update
setStatus({state: 'updating', percent: 0});
let settings = getSettings();
let task = createTask('appliances', 'Update software', resource, req);
let taskSteps = 2 + settings.nodes.length;
task.percentComplete = 0;
delete update.errors;
update.runningTaskUri = task.uri;
setUpdate(update);
res.json({
taskUri: task.uri
});
// delay just a bit to simulate the appliance task starting later
setTimeout(() => {
// Update involves updating this appliance and each node.
// Create tasks for each of these in the right sequence.
let applianceTask = createTask('appliances',
'Update hyperconverged management', resource, req);
applianceTask.parentTaskUri = task.uri;
// First, update this appliance.
let index = 0;
let timer = setInterval(() => {
index += 1;
if (index < 10) {
setStatus({state: 'updating',
percent: Math.floor((index / 10) * 100)});
applianceTask.percentComplete = Math.floor((index / 10) * 100);
} else {
settings.version = update.version;
setSettings(settings);
setStatus({state: 'ready'});
clearInterval(timer);
// update the applianceTask that also tracks this.
applianceTask.status = 'OK';
applianceTask.state = 'Completed';
applianceTask.percentComplete = undefined;
applianceTask.modified = (new Date()).toISOString();
task.percentComplete = Math.floor((1 / taskSteps) * 100);
// done with appliance update, do each node one by one
const nodeTasks = settings.nodes.map((node, index) => {
let nodeTask = createTask('appliances', `Update ${node.name}`,
resource, req);
nodeTask.parentTaskUri = task.uri;
nodeTask.nodeIndex = index; // This is a cheat to get the name below
return nodeTask;
});
runTasks(nodeTasks, (nodeTask) => {
task.percentComplete =
Math.floor(((2 + nodeTask.nodeIndex) / taskSteps) * 100);
const node = settings.nodes[nodeTask.nodeIndex];
if (update.file.name.match(/error/)) {
nodeTask.status = 'Critical';
nodeTask.state = 'Error';
nodeTask.taskErrors = [{
message: 'Simulated failure.',
recommendedActions: 'Try again'
}];
if (! update.errors) {
update.errors = [];
}
update.errors.push({
status: 'Critical',
message: `Simulated failure on ${node.name}.`,
resolution: 'Try again',
action: 'update'
});
setUpdate(update);
}
}, () => {
if (update.file.name.match(/error/)) {
task.status = 'Critical';
task.state = 'Error';
task.taskErrors = [{
message: 'Unable to fully complete the update.',
recommendedActions: 'Try again'
}];
if (! update.errors) {
update.errors = [];
}
update.errors.push({
status: 'Critical',
message: 'Unable to fully complete the update.',
resolution: 'Try again',
action: 'update'
});
delete update.runningTaskUri;
setUpdate(update);
} else {
setUpdate({});
task.status = 'OK';
task.state = 'Completed';
}
task.percentComplete = undefined;
task.modified = (new Date()).toISOString();
});
}
}, TASK_UPDATE_INTERVAL);
}, 10);
});
function deleteOneWith (path, func) {
router.delete(path, (req, res) => {
func({});
res.status(200).send();
});
}
deleteOneWith('/update', setUpdate);
router.post('/restart', (req, res) => {
// In a real product, this would create a task and preserve data.
// In this prototype, restarting means the same as 'factory reset'
// First, respond that we've got it.
res.status(200).send();
// Then, kill ourselves
process.exit(0);
});
router.post('/backup', (req, res) => {
const resource = getResource('/rest/appliances/1');
setBackup({}); // clear prior one
let task = createTask('appliances', 'Backup', resource, req);
res.json({
taskUri: task.uri
});
runTask(task, null, () => {
setBackup({
file: `/rest/backup/${(new Date()).getTime().toString(10)}`
});
});
});
getOneFrom('/backup', getBackup);
deleteOneWith('/backup', setBackup);
// How support dumps should work:
router.post('/support', (req, res) => {
const resource = getResource('/rest/appliances/1');
setSupport({}); // clear prior one
let task = createTask('appliances', 'Create support dump', resource, req);
res.json({
taskUri: task.uri
});
runTask(task, null, () => {
setSupport({
file: `/rest/support/${(new Date()).getTime().toString(10)}`
});
});
});
getOneFrom('/support', getSupport);
deleteOneWith('/support', setSupport);
// How Atlas server does suppprt dumps:
router.post('/support-dumps', (req, res) => {
// var resource = getResource('/rest/appliances/1');
setTimeout(() => {
res.json({
supportDumpFile: '/rest/support-dumps/' +
'phoenix-vm-nithya-CI-2016_02_19-10_15_42.795139.sdmp'
});
}, TASK_UPDATE_INTERVAL * 9); // The UI times out at 10s
});
router.post('/images', (req, res) => {
// Simulate taking a while to upload the image
setTimeout(() => {
const categoryName = 'images';
const file = req.files.file;
const now = new Date();
const resource = {
category: categoryName,
uri: `/rest/${categoryName}/${now.getTime()}`,
status: 'Unknown',
state: 'Offline',
created: now.toISOString(),
modified: now.toISOString(),
...req.body,
fileName: file.name
};
addResource(categoryName, resource);
let task = createTask(categoryName, 'Add', resource, req);
// we start at 50% complete since the upload took the first 50%
task.percentComplete = 50;
res.json({
taskUri: task.uri
});
runTask(task, resource, () => {
resource.status = "Disabled";
resource.state = "Offline";
// simulate an error if the file name has "invalid" in it
if (file.name.match(/invalid/)) {
task.status = 'Critical';
task.state = 'Error';
task.taskErrors = [{
message: 'Simulated checksum or invalid file error.',
recommendedActions: 'Upload another file.'
}];
}
});
}, TASK_UPDATE_INTERVAL * 5);
});
// router.put('/images/:id', function(req, res) {
// // Simulate taking a while to upload the image
// setTimeout(function () {
// var categoryName = req.params.categoryName;
// var resource = getResource('/rest' + req.url);
// var updatedResource = req.body;
// var now = new Date();
// updatedResource.modified = now.toISOString();
// var task = createTask(categoryName, 'Update', resource, req);
//
// res.json({
// taskUri: task.uri
// });
//
// runTask(task, resource, function () {
// if (updatedResource.name.match(/error/i)) {
// task.status = "Critical";
// task.state = "Error";
// task.taskErrors = [{
// message: 'There was an error with this task.',
// recommendedActions: "Don't use the term 'error' in the name."
// }];
// } else {
// updateResource(categoryName, updatedResource);
// }
// });
//
//
//
// var categoryName = 'images';
// var file = req.files.file;
// var now = new Date();
//
// var resource = _.extend({
// category: categoryName,
// uri: '/rest/' + categoryName + '/' + now.getTime(),
// status: 'Unknown',
// state: 'Offline',
// created: now.toISOString(),
// modified: now.toISOString()
// }, req.body, {fileName: file.name});
// addResource(categoryName, resource);
//
// var task = createTask(categoryName, 'Add', resource, req);
// // we start at 50% complete since the upload took the first 50%
// task.percentComplete = 50;
//
// res.json({
// taskUri: task.uri
// });
//
// runTask(task, resource, function () {
// resource.status = "Disabled";
// resource.state = "Offline";
//
// // simulate an error if the file name has "invalid" in it
// if (file.name.match(/invalid/)) {
// task.status = 'Critical';
// task.state = 'Error';
// task.taskErrors = [{
// message: 'Simulated checksum or invalid file error.',
// recommendedActions: 'Upload another file.'
// }];
// }
// });
// }, TASK_UPDATE_INTERVAL * 5);
// });
// Linked navigation
router.post('/route', (req, res) => {
const token = req.headers.auth;
if (token === latestGruToken) {
let resource = getResource('/rest/route/1');
const categoryName = 'route';
if (! resource) {
resource = {
category: categoryName,
uri: '/rest/route/1',
pathname: req.body.pathname
};
addResource(categoryName, resource);
} else {
resource.pathname = req.body.pathname;
updateResource(categoryName, resource);
}
res.status(200).json({});
onResourceChange(resource);
}
});
router.get('/route/1', (req, res) => {
const resource = getResource('/rest/route/1');
if (resource) {
res.json(resource);
} else {
res.status(404).send();
}
});
// index
router.get('/index/resources', (req, res) => {
const result = getIndex(`/rest${req.url}`, req.query);
res.json(result);
});
router.get('/index/resources/aggregated', (req, res) => {
const result = getAggregate(`/rest${req.url}`, req.query);
res.json(result);
});
router.get(/^\/index\/search-suggestions/, (req, res) => {
let items = getItems(req.query.category || null);
items = filteredItems(items, req.query);
const startIndex = +req.query.start; // coerce to be a number
// prune for start+count
items = items.slice(startIndex, startIndex + req.query.count);
res.json(items.map((item) => {
return {
name: item.name,
category: item.category,
uri: item.uri
};
}));
});
router.get(/^\/index\/trees\/aggregated(.+)$/, (req, res) => {
const uri = req.params[0];
res.json(buildMap(uri));
});
router.get(/^\/index\/trees(.+)$/, (req, res) => {
//var uri = req.params[0];
res.status(501).send();
});
router.get('/:categoryName/*', (req, res) => {
var resource = getResource(`/rest${req.url.split('?')[0]}`);
if (resource) {
res.json(resource);
} else {
res.status(404).send();
}
});
let taskIndex = 1;
function createTask (categoryName, action, resource, req) {
const now = new Date();
const token = req.headers.auth;
const session = getSession(token) || {userName: 'System'};
const task = {
category: 'tasks',
uri: `/rest/tasks/${now.getTime()}-${taskIndex}`,
name: action,
status: 'Unknown',
state: 'Running',
attributes: {
associatedResourceUri: resource.uri,
associatedResourceName: resource.name,
associatedResourceCategory: categoryName,
hidden: false,
owner: session.userName
},
created: now.toISOString(),
modified: now.toISOString()
};
addResource('tasks', task);
taskIndex += 1;
return task;
}
function runTask (task, resource, handler) {
// allow caller to preset the percent complete
if (! task.percentComplete) {
task.percentComplete = 0;
}
let timer = setInterval(() => {
task.percentComplete += 10;
task.modified = (new Date()).toISOString();
if (task.percentComplete >= 100) {
if (resource) {
resource.modified = task.modified;
}
task.percentComplete = undefined;
task.status = 'OK';
task.state = 'Completed';
clearInterval(timer);
if (handler) {
handler();
}
onResourceChange(resource, task);
}
onResourceChange(undefined, task);
}, TASK_UPDATE_INTERVAL);
setTimeout(() => {
onResourceChange(resource, task);
}, 1);
}
function runTasks (tasks, perTaskHandler, handler) {
let task = tasks.shift();
if (task) {
runTask(task, null, () => {
if (perTaskHandler) {
perTaskHandler(task);
}
runTasks (tasks, perTaskHandler, handler);
});
} else {
handler();
}
}
router.post('/:categoryName', (req, res) => {
const categoryName = req.params.categoryName;
const now = new Date();
const resource = {
category: categoryName,
uri: `/rest/${categoryName}/${now.getTime()}`,
status: 'Unknown',
state: 'Offline',
created: now.toISOString(),
modified: now.toISOString(),
...req.body
};
if ('virtual-machines' === categoryName) {
addUtilization(resource);
}
addResource(categoryName, resource);
let task = createTask(categoryName, 'Add', resource, req);
res.json({
taskUri: task.uri
});
runTask(task, resource, () => {
resource.status = "Disabled";
resource.state = "Offline";
});
});
function restTask (endpoint, taskName, endStatus, endState) {
router.post(`/:categoryName/*/${endpoint}`, (req, res) => {
const categoryName = req.params.categoryName;
let resource = getResource('/rest' +
req.url.slice(0, req.url.length - (endpoint.length + 1)), true);
resource.modified = (new Date()).toISOString();
let task = createTask(categoryName, taskName, resource, req);
res.json({
taskUri: task.uri
});
runTask(task, resource, () => {
resource.status = endStatus;
resource.state = endState;
addUtilization(resource);
});
});
}
restTask('on', 'Power on', 'OK', 'Online');
restTask('off', 'Power off', 'Disabled', 'Offline');
restTask('restart', 'Restart', 'OK', 'Online');
router.put('/:categoryName/*', (req, res) => {
const categoryName = req.params.categoryName;
const resource = getResource(`/rest${req.url}`);
let updatedResource = req.body;
const now = new Date();
updatedResource.modified = now.toISOString();
if ('alerts' === categoryName || 'tasks' === categoryName) {
updateResource(categoryName, updatedResource);
res.status(200).json({});
onResourceChange(updatedResource);
} else {
let task = createTask(categoryName, 'Update', resource, req);
res.json({
taskUri: task.uri
});
runTask(task, resource, () => {
if (updatedResource.name.match(/error/i)) {
task.status = "Critical";
task.state = "Error";
task.taskErrors = [{
message: 'There was an error with this task.',
recommendedActions: "Don't use the term 'error' in the name."
}];
} else {
updateResource(categoryName, updatedResource);
}
});
}
});
router.delete('/:categoryName/*', (req, res) => {
const categoryName = req.params.categoryName;
const resource = getResource(`/rest${req.url}`);
let task = createTask(categoryName, 'Remove', resource, req);
res.json({
taskUri: task.uri
});
// We run the delete task with no resource
runTask(task, { category: categoryName }, () => {
deleteResource(categoryName, `/rest${req.url}`);
// Delete associated alerts and tasks, except this task
const params = {
category: ['alerts', 'tasks'], start: 0, count: 100,
query: `associatedResourceUri:/rest${req.url}`
};
const items = getIndex(null, params).items;
items.forEach((item) => {
if (item.uri !== task.uri) {
deleteResource(item.category, item.uri);
}
});
});
});
module.exports = {
router: router,
setup: (server, prefix) => {
generate();
initializeSocket(server, prefix);
// generator.listen(onResourceChange);
}
};