aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorJack Lucas <jflucas@research.att.com>2017-05-10 01:48:41 +0000
committerJack Lucas <jflucas@research.att.com>2017-05-10 01:48:41 +0000
commitff6ba434b6d91b6a4a4e9b3a7fbb8cadced229ad (patch)
tree8ae919ab8d1b174944bd6bea6602ce29153be2f6 /lib
parent0806707cbc2dd6311228facabf0a6052862c51c7 (diff)
Post-R1 API & other updates.
Change-Id: Id0e2e15b95a5713a25a746534fc40b56599a5f06 Signed-off-by: Jack Lucas <jflucas@research.att.com>
Diffstat (limited to 'lib')
-rw-r--r--lib/auth.js10
-rw-r--r--lib/cloudify.js231
-rw-r--r--lib/config.js23
-rw-r--r--lib/dcae-deployments.js242
-rw-r--r--lib/deploy.js184
-rw-r--r--lib/dispatcher-error.js53
-rw-r--r--lib/events.js10
-rw-r--r--lib/inventory.js213
-rw-r--r--lib/logging.js128
-rw-r--r--lib/middleware.js29
-rw-r--r--lib/promise_request.js54
-rw-r--r--lib/services.js20
-rw-r--r--lib/utils.js44
13 files changed, 971 insertions, 270 deletions
diff --git a/lib/auth.js b/lib/auth.js
index 54b08e2..9ddd7b3 100644
--- a/lib/auth.js
+++ b/lib/auth.js
@@ -21,7 +21,7 @@ See the License for the specific language governing permissions and limitations
/* Extract user name and password from the 'Authorization' header */
const parseAuthHeader = function(authHeader){
- let parsedHeader = {};
+ var parsedHeader = {};
const authItems = authHeader.split(/\s+/); // Split on the white space between Basic and the base64 encoded user:password
@@ -48,15 +48,15 @@ exports.checkAuth = function(req, res, next) {
next();
}
else {
- let err = new Error('Authentication required');
+ var err = new Error('Authentication required');
err.status = 403;
next(err);
}
}
else {
- let err = new Error ('Authentication required');
- err.status = 403;
- next(err);
+ var errx = new Error ('Authentication required');
+ errx.status = 403;
+ next(errx);
}
}
else {
diff --git a/lib/cloudify.js b/lib/cloudify.js
index b1565c4..145a10e 100644
--- a/lib/cloudify.js
+++ b/lib/cloudify.js
@@ -18,10 +18,8 @@ See the License for the specific language governing permissions and limitations
"use strict";
-const stream = require('stream');
-const targz = require('node-tar.gz');
+const admzip = require('adm-zip');
-const utils = require('./utils');
const repeat = require('./repeat');
const req = require('./promise_request');
const doRequest = req.doRequest;
@@ -39,83 +37,153 @@ var delay = function(dtime) {
});
};
+// Get current status of a workflow execution
+// Function for getting execution info
+const getExecutionStatus = function(executionId) {
+ var reqOptions = {
+ method : "GET",
+ uri : cfyAPI + "/executions/" + executionId
+ };
+ if (cfyAuth) {
+ reqOptions.auth = cfyAuth;
+ }
+ return doRequest(reqOptions);
+};
+
// Poll for the result of a workflow execution
var getWorkflowResult = function(execution_id) {
var finished = [ "terminated", "cancelled", "failed" ];
var retryInterval = 15000; // Every 15 seconds
var maxTries = 240; // Up to an hour
- logger.debug("Getting workflow status for execution id: " + execution_id);
-
- // Function for getting execution info
- var getExecutionStatus = function() {
- var reqOptions = {
- method : "GET",
- uri : cfyAPI + "/executions/" + execution_id
- };
- if (cfyAuth) {
- reqOptions.auth = cfyAuth;
- }
- return doRequest(reqOptions);
- };
+ logger.debug("Getting workflow result for execution id: " + execution_id);
// Function for testing if workflow is finished
- // Expects the result of getExecutionStatus
+ // Expects the result of getExecStatus
var checkStatus = function(res) {
logger.debug("Checking result: " + JSON.stringify(res) + " ==> " + (res.json && res.json.status && finished.indexOf(res.json.status) < 0));
return res.json && res.json.status && finished.indexOf(res.json.status) < 0;
};
-
- return repeat.repeatWhile(getExecutionStatus, checkStatus, maxTries,
- retryInterval).then(function(res) {
- if (res.json && res.json.status && res.json.status !== "terminated") {
- throw ("workflow failed!");
- } else {
+
+ // Create execution status checker function
+ var getExecStatus = function() { return getExecutionStatus(execution_id);};
+
+ return repeat.repeatWhile(getExecStatus, checkStatus, maxTries, retryInterval)
+ .then(
+
+ /* Handle fulfilled promise from repeatWhile */
+ function(res) {
+
+ logger.debug('workflow result: ' + JSON.stringify(res));
+
+ /* Successful completion */
+ if (res.json && res.json.status && res.json.status === 'terminated') {
return res;
}
+
+ /* If we get here, we don't have a success and we're going to throw something */
+
+ var error = {};
+
+ /* We expect a JSON object with a status */
+ if (res.json && res.json.status) {
+
+ /* Failure -- we need to return something that looks like the CM API failures */
+ if (res.json.status === 'failed') {
+ error.body = 'workflow failed: ' + execution_id + ' -- ' + (res.json.error ? JSON.stringify(res.json.error) : 'no error information');
+ }
+
+ /* Cancellation -- don't really expect this */
+ else if (res.json.status === 'canceled' || res.json.status === 'cancelled') {
+ error.body = 'workflow canceled: ' + execution_id;
+ }
+
+ /* Don't expect anything else -- but if we get it, it's not a success! */
+ else {
+ error.body = 'workflow--unexpected status ' + res.json.status + ' for ' + execution_id;
+ }
+ }
+
+ /* The body of the response from the API call to get execution status is not what we expect at all */
+ else {
+ error.body = 'workflow--unexpected result body getting execution status from CM for ' + execution_id;
+ }
+
+ throw error;
+ },
+
+ /* Handle rejection of promise from repeatWhile--don't use a catch because it would catch the error thrown above */
+ function(err) {
+ /* repeatWhile could fail and we get here because:
+ * -- repeatWhile explicitly rejects the promise because it has exhausted the retries
+ * -- repeatWhile propagates a system error (e.g., network problem) trying to access the API
+ * -- repeatWhile propagates a rejected promise due to a bad HTTP response status
+ * These should all get normalized in deploy.js--so we just rethrow the error.
+ */
+
+ throw err;
+
});
};
+//Initiate a workflow execution against a deployment
+const initiateWorkflowExecution = function(dpid, workflow) {
+ // Set up the HTTP POST request
+ var reqOptions = {
+ method : "POST",
+ uri : cfyAPI + "/executions",
+ headers : {
+ "Content-Type" : "application/json",
+ "Accept" : "*/*"
+ }
+ };
+ if (cfyAuth) {
+ reqOptions.auth = cfyAuth;
+ }
+ var body = {
+ deployment_id : dpid,
+ workflow_id : workflow
+ };
+
+ // Make the POST request
+ return doRequest(reqOptions, JSON.stringify(body))
+ .then(function(result) {
+ logger.debug("Result from POSTing workflow execution start: " + JSON.stringify(result));
+ if (result.json && result.json.id) {
+ return {deploymentId: dpid, workflowType: workflow, executionId: result.json.id};
+ }
+ else {
+ logger.debug("Did not get expected JSON body from POST to start workflow");
+ var err = new Error("POST to start workflow got success response but no body");
+ err.status = err.code = 502;
+ }
+ });
+};
+
// Uploads a blueprint via the Cloudify API
exports.uploadBlueprint = function(bpid, blueprint) {
+
// Cloudify API wants a gzipped tar of a directory, not the blueprint text
- // So we make a directory and feed a gzipped tar as the body of the PUT
- // request
- var workingDir = "./work/" + bpid;
-
- return utils.makeDirAndFile(workingDir, 'blueprint.yaml', blueprint)
-
- .then(function() {
- // Set up a read stream that presents tar'ed and gzipped data
- var src = targz().createReadStream(workingDir);
-
- // Set up the HTTP PUT request
- var reqOptions = {
+ var zip = new admzip();
+ zip.addFile('work/', new Buffer(0));
+ zip.addFile('work/blueprint.yaml', new Buffer(blueprint, 'utf8'));
+ var src = (zip.toBuffer());
+
+ // Set up the HTTP PUT request
+ var reqOptions = {
method : "PUT",
uri : cfyAPI + "/blueprints/" + bpid,
headers : {
"Content-Type" : "application/octet-stream",
"Accept" : "*/*"
}
- };
-
- if (cfyAuth) {
- reqOptions.auth = cfyAuth;
- }
- // Initiate PUT request and return the promise for a result
- return doRequest(reqOptions, src).then(
- // Cleaning up the working directory without perturbing the result is
- // messy!
- function(result) {
- utils.removeDir(workingDir);
- return result;
- }, function(err) {
- logger.debug("Problem on upload: " + JSON.stringify(err));
- utils.removeDir(workingDir);
- throw err;
- });
+ };
- });
+ if (cfyAuth) {
+ reqOptions.auth = cfyAuth;
+ }
+ // Initiate PUT request and return the promise for a result
+ return doRequest(reqOptions, src);
};
// Creates a deployment from a blueprint
@@ -145,41 +213,31 @@ exports.createDeployment = function(dpid, bpid, inputs) {
return doRequest(reqOptions, JSON.stringify(body));
};
-// Executes a workflow against a deployment (use for install and uninstall)
-exports.executeWorkflow = function(dpid, workflow) {
+// Initiate a workflow execution against a deployment
+exports.initiateWorkflowExecution = initiateWorkflowExecution;
- // Set up the HTTP POST request
- var reqOptions = {
- method : "POST",
- uri : cfyAPI + "/executions",
- headers : {
- "Content-Type" : "application/json",
- "Accept" : "*/*"
- }
- };
- if (cfyAuth) {
- reqOptions.auth = cfyAuth;
- }
- var body = {
- deployment_id : dpid,
- workflow_id : workflow
- };
+// Get the status of a workflow execution
+exports.getWorkflowExecutionStatus = getExecutionStatus;
- // Make the POST request
- return doRequest(reqOptions, JSON.stringify(body)).then(
- function(result) {
- logger.debug("Result from POSTing workflow start: " + JSON.stringify(result));
- if (result.json && result.json.id) {
- logger.debug("Waiting for workflow status: " + result.json.id);
- return getWorkflowResult(result.json.id);
- }
- else {
- logger.warn("Did not get expected JSON body from POST to start workflow");
- // TODO throw? we got an OK for workflow but no JSON?
- }
- });
+// Return a promise for the final result of a workflow execution
+exports.getWorkflowResult = getWorkflowResult;
+
+// Executes a workflow against a deployment and returns a promise for final result
+exports.executeWorkflow = function(dpid, workflow) {
+
+ // Initiate the workflow
+ return initiateWorkflowExecution(dpid, workflow)
+
+ // Wait for the result
+ .then (function(result) {
+ logger.debug("Result from initiating workflow: " + JSON.stringify(result));
+ return getWorkflowResult(result.executionId);
+ });
};
+// Wait for workflow to complete and get result
+exports.getWorkflowResult = getWorkflowResult;
+
// Retrieves outputs for a deployment
exports.getOutputs = function(dpid) {
var reqOptions = {
@@ -245,10 +303,7 @@ exports.setAPIAddress = function(addr) {
// Allow client to set Cloudify credentials
exports.setCredentials = function(user, password) {
- cfyAuth = {
- user : user,
- password : password
- };
+ cfyAuth = user + ':' + password;
};
// Set a logger
diff --git a/lib/config.js b/lib/config.js
index f74faa9..2c9b9fc 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -24,7 +24,7 @@ See the License for the specific language governing permissions and limitations
* --------------------------------------------------------------------------------------
* | JSON property | Environment variable | Required? | Default |
* --------------------------------------------------------------------------------------
- * | foreground | FOREGROUND | Yes | true |
+ * | foreground | FOREGROUND | Yes | false |
* --------------------------------------------------------------------------------------
* | logLevel | LOG_LEVEL | Yes | "INFO" |
* --------------------------------------------------------------------------------------
@@ -49,13 +49,19 @@ See the License for the specific language governing permissions and limitations
* | inventory.password | INVENTORY_PASSWORD | No | none |
* --------------------------------------------------------------------------------------
*
+ * cloudify.cfyManagerAddress allowed as synonym for cloudify.url
+ * cloudify.cfyUser allowed as synonym for cloudify.user
+ * cloudify.cfyPassword allowed as synonym for cloudify.password
+ * inventory.inventoryAddress allowed as synonym for inventory.url
+ * ssl.pfx-file allowed as synonym for ssl.pfxFile
+ * Note that we're using ssl.passphrase directly in the config file--i.e., we get the passphrase, not a file.
*/
"use strict";
const fs = require("fs");
const utils = require("./utils");
-const DEFAULT_FOREGROUND = true;
+const DEFAULT_FOREGROUND = false;
const DEFAULT_LISTEN_PORT = 8443;
const DEFAULT_LISTEN_HOST = "0.0.0.0";
const DEFAULT_LOG_LEVEL = "INFO";
@@ -96,19 +102,22 @@ exports.configure = function(configFile) {
if (!cfg.cloudify) {
cfg.cloudify = {};
}
- cfg.cloudify.url = process.env['CLOUDIFY_URL'] || cfg.cloudify.url;
- cfg.cloudify.user = process.env['CLOUDIFY_USER'] || cfg.cloudify.user;
- cfg.cloudify.password = process.env['CLOUDIFY_PASSWORD'] || cfg.cloudify.password;
+ cfg.cloudify.url = process.env['CLOUDIFY_URL'] || cfg.cloudify.url || cfg.cloudify.cfyManagerAddress;
+ cfg.cloudify.user = process.env['CLOUDIFY_USER'] || cfg.cloudify.user || cfg.cloudify.cfyUser;
+ cfg.cloudify.password = process.env['CLOUDIFY_PASSWORD'] || cfg.cloudify.password || cfg.cloudify.cfyPassword;
if (!cfg.inventory) {
cfg.inventory = {};
}
- cfg.inventory.url = process.env['INVENTORY_URL'] || cfg.inventory.url || DEFAULT_INVENTORY_URL;
+ cfg.inventory.url = process.env['INVENTORY_URL'] || cfg.inventory.url || cfg.inventory.inventoryAddress || DEFAULT_INVENTORY_URL;
cfg.inventory.user = process.env['INVENTORY_USER'] || cfg.inventory.user;
cfg.inventory.password = process.env['INVENTORY_PASSWORD'] || cfg.inventory.password;
cfg.locations = {};
/* If https params are present, read in the cert/passphrase */
+ if (cfg.ssl) {
+ cfg.ssl.pfxFile = cfg.ssl.pfxFile || cfg.ssl['pfx-file']; // Allow synonym
+ }
if (cfg.ssl && cfg.ssl.pfxFile) {
cfg.ssl.pfx = fs.readFileSync(cfg.ssl.pfxFile);
if (cfg.ssl.pfxPassFile) {
@@ -128,7 +137,7 @@ exports.configure = function(configFile) {
/** Read locations file
*/
exports.getLocations = function(locationsFile) {
- let locations = {};
+ var locations = {};
try {
locations = JSON.parse(fs.readFileSync(locationsFile));
diff --git a/lib/dcae-deployments.js b/lib/dcae-deployments.js
new file mode 100644
index 0000000..b50dee4
--- /dev/null
+++ b/lib/dcae-deployments.js
@@ -0,0 +1,242 @@
+/*
+Copyright(c) 2017 AT&T Intellectual Property. All rights reserved.
+
+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.
+*/
+
+/* Handle the /dcae-deployments API */
+
+"use strict";
+
+/* Set this code up as a "sub-app"--lets us get the mountpoint for creating links */
+const app = require('express')();
+app.set('x-powered-by', false);
+app.set('etag', false);
+
+const bodyParser = require('body-parser');
+const deploy = require('./deploy');
+const middleware = require('./middleware');
+const inventory = require('./inventory');
+const logging = require('./logging');
+const logAccess = logging.logAccess;
+const logError = logging.logError;
+const logWarn = logging.logWarn;
+
+/* Pick up config exported by main */
+const config = process.mainModule.exports.config;
+const logger = config.logSource.getLogger('dcae-deployments');
+
+/* Set up middleware stack for initial processing of request */
+app.use(middleware.checkType('application/json')); // Validate type
+app.use(bodyParser.json({strict: true})); // Parse body as JSON
+
+
+/* Return a promise for a blueprint for the given service type ID */
+const getBlueprint = function(serviceTypeId) {
+ return inventory.getBlueprintByType(serviceTypeId)
+ .then(function (blueprintInfo) {
+ if (!blueprintInfo.blueprint) {
+ var e = new Error("No service type with ID " + serviceTypeId);
+ e.status = 404;
+ throw e;
+ }
+ return blueprintInfo;
+ })
+};
+
+/* Generate self and status links object for responses */
+const createLinks = function(req, deploymentId, executionId) {
+ var baseURL = req.protocol + '://' + req.get('Host') + req.app.mountpath + '/' + deploymentId;
+ return {
+ self: baseURL,
+ status: baseURL + '/operation/' + executionId
+ };
+};
+
+/* Generate a success response body for PUT and DELETE operations */
+const createResponse = function(req, result) {
+ return {
+ requestId: req.dcaeReqId,
+ links: createLinks(req, result.deploymentId, result.executionId)
+ };
+};
+
+/* Look up running (or in process of deploying) instances of the given service type */
+app.get('/', function (req, res, next) {
+ var services = []
+
+
+ var searchTerm = {};
+
+ req.query['serviceTypeId'] && (searchTerm = {typeId: req.query['serviceTypeId']});
+
+ inventory.getServicesByType(searchTerm)
+ .then(function (result) {
+ var deployments = result.map(function(service){
+ return {
+ href: req.protocol + '://' + req.get('Host') + req.app.mountpath + '/' + service.deploymentId
+ };
+ })
+ res.status(200).json({requestId: req.dcaeReqId, deployments: deployments});
+ logAccess(req, 200);
+ })
+ .catch(next); /* Let the error handler send response and log the error */
+});
+
+/* Accept an incoming deployment request */
+app.put('/:deploymentId', function(req, res, next) {
+
+ /* Make sure there's a serviceTypeId in the body */
+ if (!req.body['serviceTypeId']) {
+ var e = new Error ('Missing required parameter serviceTypeId');
+ e.status = 400;
+ throw e;
+ }
+
+ /* Make sure the deploymentId doesn't already exist */
+ inventory.verifyUniqueDeploymentId(req.params['deploymentId'])
+
+ /* Get the blueprint for this service type */
+ .then(function(res) {
+ return getBlueprint(req.body['serviceTypeId']);
+ })
+
+ /* Add this new service instance to inventory
+ * Easier to remove from inventory if deployment fails than vice versa
+ * Also lets client check for deployed/deploying instances if client wants to limit number of instances
+ */
+ .then(function (blueprintInfo) {
+ req.dcaeBlueprint = blueprintInfo.blueprint;
+ return inventory.addService(req.params['deploymentId'], blueprintInfo.typeId, "dummyVnfId", "dummyVnfType", "dummyLocation");
+ })
+
+ /* Upload blueprint, create deployment and start install workflow (but don't wait for completion */
+ .then (function() {
+ req.dcaeAddedToInventory = true;
+ return deploy.launchBlueprint(req.params['deploymentId'], req.dcaeBlueprint, req.body['inputs']);
+ })
+
+ /* Send the HTTP response indicating workflow has started */
+ .then(function(result) {
+ res.status(202).json(createResponse(req, result));
+ logAccess(req, 202, "Execution ID: " + result.executionId);
+ return result;
+ })
+
+ /* Finish deployment--wait for the install workflow to complete, retrieve and annotate outputs */
+ .then(function(result) {
+ return deploy.finishInstallation(result.deploymentId, result.executionId);
+ })
+
+ /* Log completion in audit log */
+ .then (function(result) {
+ logAccess(req, 200, "Deployed id: " + req.params['deploymentId']);
+ })
+
+ /* All errors show up here */
+ .catch(function(error) {
+
+
+
+ /* If we haven't already sent a response, let the error handler send response and log the error */
+ if (!res.headersSent) {
+
+ /* If we made an inventory entry, remove it */
+ if (req.dcaeAddedToInventory) {
+ inventory.deleteService(req.params['deploymentId'])
+ .then(function() {
+ logger.info("deleted failed deployment from inventory");
+ })
+ .catch(function(error) {
+ logger.debug("failed to delete service " + req.params['deploymentId'] + " from inventory");
+ logError(error, req);
+ });
+ }
+
+ next(error);
+ }
+ else {
+ /* Already sent the response, so just log error */
+ /* Don't remove from inventory, because there is a deployment on CM that might need to be removed */
+ error.message = "Error deploying deploymentId " + req.params['deploymentId'] + ": " + error.message
+ logError(error, req);
+ logAccess(req, 500, error.message);
+ }
+
+ });
+});
+
+/* Delete a running service instance */
+app.delete('/:deploymentId', function(req, res, next) {
+
+ /* Launch the uninstall workflow */
+ deploy.launchUninstall(req.params['deploymentId'])
+
+ /* Delete the service from inventory */
+ .then(function(result) {
+ return inventory.deleteService(req.params['deploymentId'])
+ .then(function() {
+ logger.info("Deleted deployment ID " + req.params['deploymentId'] + " from inventory");
+ return result;
+ })
+ })
+
+ /* Send the HTTP response indicating workflow has started */
+ .then(function(result) {
+ res.status(202).send(createResponse(req, result));
+ logAccess(req, 202, "ExecutionId: " + result.executionId);
+ return result;
+ })
+
+ /* Finish the delete processing--wait for the uninstall to complete, delete deployment, delete blueprint */
+ .then(function(result) {
+ return deploy.finishUninstall(result.deploymentId, result.executionId);
+ })
+
+ /* TODO Log completion in audit log */
+ .then(function(result) {
+ logAccess(req, 200, "Undeployed id: " + req.params['deploymentId']);
+ })
+
+ /* All errors show up here */
+ .catch(function(error) {
+ /* If we haven't already sent a response, give it to the error handler to send response */
+ if (!res.headersSent) {
+ next(error);
+ }
+ else {
+ /* Error happened after we sent the response--log it */
+ error.message = "Error undeploying deploymentId " + req.params['deploymentId'] + ": " + error.message
+ logError(error, req);
+ logAccess(req, 500, error.message);
+ }
+ });
+});
+
+/* Get the status of a workflow execution */
+app.get('/:deploymentId/operation/:executionId', function(req, res, next){
+ deploy.getExecutionStatus(req.params['executionId'])
+
+ /* Send success response */
+ .then(function(result) {
+ result.requestId = req.dcaeReqId;
+ result.links = createLinks(req, req.params['deploymentId'], req.params['executionId']);
+ res.status(200).json(result);
+ logAccess(req, 200, "Workflow type: " + result.operationType + " -- execution status: " + result.status);
+ })
+
+ .catch(next); /* Let the error handler send the response and log the error */
+
+});
+
+module.exports = app; \ No newline at end of file
diff --git a/lib/deploy.js b/lib/deploy.js
index e807060..fcee43d 100644
--- a/lib/deploy.js
+++ b/lib/deploy.js
@@ -26,6 +26,8 @@ const DELAY_RETRIEVE_OUTPUTS = 5000;
const DELAY_DELETE_DEPLOYMENT = 30000;
const DELAY_DELETE_BLUEPRINT = 10000;
+const createError = require('./dispatcher-error').createDispatcherError;
+
/* Set up the Cloudify low-level interface library */
var cfy = require("./cloudify.js");
/* Set config for deploy module */
@@ -52,36 +54,37 @@ var parseContent = function(input) {
// create a normalized representation of errors, whether they're a node.js Error or a Cloudify API error
var normalizeError = function (err) {
- var e = {};
+ var e;
+
if (err instanceof Error) {
- e.message = err.message;
- if (err.code) {
- e.code = err.code;
- }
- if (err.status) {
- e.status = err.status;
- }
+ /* node.js system error */
+ e = createError("Error communicating with CM: " + err.message, 504, "system", 202, 'cloudify-manager');
}
else {
// Try to populate error with information from a Cloudify API error
// We expect to see err.body, which is a stringified JSON object
// We can parse it and extract message and error_code
- e.message = "unknown API error";
- e.code = "UNKNOWN";
- if (err.status) {
- e.status = err.status;
- }
+ var message = err.message || "unknown Cloudify Manager API error";
+ var status = err.status || 502;
+ var cfyCode = "UNKNOWN";
+ var cfyMessage;
+
if (err.body) {
var p = parseContent(err.body);
if (p.json) {
- e.message = p.content.message ? p.content.message : "unknown API error";
- e.code = p.content.error_code ? p.content.error_code : "UNKNOWN";
+ cfyMessage = p.content.message ? p.content.message : "unknown Cloudify API error";
+ cfyCode = p.content.error_code ? p.content.error_code : "UNKNOWN";
}
else {
// if there's a body and we can't parse it just attach it as the message
- e.message = err.body;
+ cfyMessage = err.body;
}
+ message = "Status " + status + " from CM API -- error code: " + cfyCode + " -- message: " + cfyMessage;
}
+
+ /* Pass through 400-level status, recast 500-level */
+ var returnStatus = (err.status > 499) ? 502 : err.status;
+ e = createError(message, returnStatus, "api", 502, 'cloudify-manager');
}
return e;
@@ -129,26 +132,43 @@ var delay = function(dtime) {
});
};
-// Go through the Cloudify API call sequence to do a deployment
-exports.deployBlueprint = function(id, blueprint, inputs) {
-
+// Go through the Cloudify API call sequence to upload blueprint, create deployment, and launch install workflow
+// (but don't wait for the workflow to finish)
+const launchBlueprint = function(id, blueprint, inputs) {
logger.debug("deploymentId: " + id + " starting blueprint upload");
// Upload blueprint
return cfy.uploadBlueprint(id, blueprint)
+
+ // Create deployment
.then (function(result) {
logger.debug("deploymentId: " + id + " blueprint uploaded");
// Create deployment
return cfy.createDeployment(id, id, inputs);
})
+
+ // Launch the workflow, but don't wait for it to complete
.then(function(result){
logger.debug("deploymentId: " + id + " deployment created");
- // Execute the install workflow
- return delay(DELAY_INSTALL_WORKFLOW).then(function(){ return cfy.executeWorkflow(id, 'install');});
+ return delay(DELAY_INSTALL_WORKFLOW)
+ .then(function(){
+ return cfy.initiateWorkflowExecution(id, 'install');
+ });
})
- .then(function(result) {
- logger.debug("deploymentId: " + id + " install workflow successfully executed");
+ .catch(function(error) {
+ logger.debug("Error: " + error + " for launch blueprint for deploymentId " + id);
+ throw normalizeError(error);
+ })
+};
+exports.launchBlueprint = launchBlueprint;
+
+// Finish installation launched with launchBlueprint
+const finishInstallation = function(deploymentId, executionId) {
+ logger.debug("finishInstallation: " + deploymentId + " -- executionId: " + executionId);
+ return cfy.getWorkflowResult(executionId)
+ .then (function(result){
+ logger.debug("deploymentId: " + deploymentId + " install workflow successfully executed");
// Retrieve the outputs from the deployment, as specified in the blueprint
- return delay(DELAY_RETRIEVE_OUTPUTS).then(function() { return cfy.getOutputs(id); });
+ return delay(DELAY_RETRIEVE_OUTPUTS).then(function() { return cfy.getOutputs(deploymentId); });
})
.then(function(result) {
// We have the raw outputs from the deployment but not annotated with the descriptions
@@ -161,35 +181,125 @@ exports.deployBlueprint = function(id, blueprint, inputs) {
}
}
}
- logger.debug("output retrieval result for " + id + ": " + JSON.stringify(result));
- logger.info("deploymentId " + id + " successfully deployed");
- return annotateOutputs(id, rawOutputs);
+ logger.debug("output retrieval result for " + deploymentId + ": " + JSON.stringify(result));
+ logger.info("deploymentId " + deploymentId + " successfully deployed");
+ return annotateOutputs(deploymentId, rawOutputs);
})
.catch(function(err) {
+ logger.debug("Error finishing install workflow: " + err + " -- " + JSON.stringify(err));
throw normalizeError(err);
});
-};
+}
+exports.finishInstallation = finishInstallation;
-// Go through the Cloudify API call sequence to do an undeployment of a previously deployed blueprint
-exports.undeployDeployment = function(id) {
- logger.debug("deploymentId: " + id + " starting uninstall workflow");
+// Initiate uninstall workflow against a deployment, but don't wait for workflow to finish
+const launchUninstall = function(deploymentId) {
+ logger.debug("deploymentId: " + deploymentId + " starting uninstall workflow");
// Run uninstall workflow
- return cfy.executeWorkflow(id, 'uninstall', 0)
+ return cfy.initiateWorkflowExecution(deploymentId, 'uninstall')
+ .then(function(result) {
+ return result;
+ })
+ .catch(function(err) {
+ logger.debug("Error initiating uninstall workflow: " + err + " -- " + JSON.stringify(err));
+ throw normalizeError(err);
+ });
+};
+exports.launchUninstall = launchUninstall;
+
+const finishUninstall = function(deploymentId, executionId) {
+ logger.debug("finishUninstall: " + deploymentId + " -- executionId: " + executionId);
+ return cfy.getWorkflowResult(executionId)
.then (function(result){
- logger.debug("deploymentId: " + id + " uninstall workflow completed");
+ logger.debug("deploymentId: " + deploymentId + " uninstall workflow successfully executed");
// Delete the deployment
- return delay(DELAY_DELETE_DEPLOYMENT).then(function() {return cfy.deleteDeployment(id);});
+ return delay(DELAY_DELETE_DEPLOYMENT).then(function() {return cfy.deleteDeployment(deploymentId);});
})
.then (function(result){
- logger.debug("deploymentId: " + id + " deployment deleted");
+ logger.debug("deploymentId: " + deploymentId + " deployment deleted");
// Delete the blueprint
- return delay(DELAY_DELETE_BLUEPRINT).then(function() {return cfy.deleteBlueprint(id);});
+ return delay(DELAY_DELETE_BLUEPRINT).then(function() {return cfy.deleteBlueprint(deploymentId);});
})
.then (function(result){
- logger.info("deploymentId: " + id + " successfully undeployed");
+ logger.info("deploymentId: " + deploymentId + " successfully undeployed");
return result;
})
.catch (function(err){
throw normalizeError(err);
});
+
};
+exports.finishUninstall = finishUninstall;
+
+// Get the status of a workflow execution
+exports.getExecutionStatus = function (exid) {
+ return cfy.getWorkflowExecutionStatus(exid)
+ .then(function(res){
+
+ var result = {
+ operationType: res.json.workflow_id
+ }
+
+ // Map execution status
+ if (res.json.status === "terminated") {
+ result.status = "succeeded";
+ }
+ else if (res.json.status === "failed") {
+ result.status = "failed";
+ }
+ else if (res.json.status === "cancelled" || res.stats === "canceled") {
+ result.status = "canceled";
+ }
+ else {
+ result.status = "processing";
+ }
+
+ if (res.json.error) {
+ result.error = res.json.error;
+ }
+ logger.debug("getExecutionStatus result: " + JSON.stringify(result));
+ return result;
+ })
+ .catch(function(error) {
+ throw normalizeError(error);
+ });
+};
+
+// Go through the Cloudify API call sequence to do a deployment
+exports.deployBlueprint = function(id, blueprint, inputs) {
+
+ // Upload blueprint, create deployment, and initiate install workflow
+ return launchBlueprint(id, blueprint, inputs)
+
+ // Wait for the workflow to complete
+ .then(
+
+ // launchBlueprint promise fulfilled -- finish installation
+ function(result){
+ return finishInstallation(result.deploymentId, result.executionId); // Will throw normalized error if it fails
+ },
+
+ // launchBlueprint promise rejected -- report error
+ function(err) {
+ throw normalizeError(err);
+ });
+};
+
+// Go through the Cloudify API call sequence to do an undeployment of a previously deployed blueprint
+exports.undeployDeployment = function(id) {
+ logger.debug("deploymentId: " + id + " starting uninstall workflow");
+
+ // Run launch uninstall workflow
+ return launchUninstall(id)
+
+ // launchUninstall promise fulfilled -- finish uninstall
+ .then (function(result){
+ return finishUninstall(result.deploymentId, result.executionId); // Will throw normalized error if it fails
+ },
+
+ // launchUninstall promise rejected -- report error
+ function(err){
+ throw normalizeError(err);
+ });
+};
+
diff --git a/lib/dispatcher-error.js b/lib/dispatcher-error.js
new file mode 100644
index 0000000..ae51fcc
--- /dev/null
+++ b/lib/dispatcher-error.js
@@ -0,0 +1,53 @@
+/*
+Copyright(c) 2017 AT&T Intellectual Property. All rights reserved.
+
+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.
+*/
+
+"use strict";
+
+/*
+ * Extend the standard Error type by appending fields to capture more information at the
+ * point of detection. The error information guides dispatcher's response to the incoming HTTP request
+ * that triggered the error and helps make the error log more specific and meaningful.
+ * This type of Error typically reports on problems encountered when attempting to use a downstream API.
+ *
+ * The standard Error has two fields:
+ * - name: the name of the Error, which is 'Error'
+ * - message: a text description of the error
+ *
+ * For dispatcher purposes, we add:
+ * - status: the HTTP status code that dispatcher should use in its response
+ * - type: "system" or "api" depending on whether the error was the result of a failed system call or
+ * an error reported by the downstream API.
+ * - logCode: the error code to use in the log entry.
+ * - target: the downstream system dispatcher was attempting to interact with
+ *
+ * Note that we're not defining a new class, just adding fields to the existing Error type. This pattern is
+ * used in Node for system errors.
+ */
+
+/* Create an error given the parameters */
+exports.createDispatcherError = function(message, status, type, logCode, target) {
+ var e = new Error();
+
+ e.message = message || 'no error information';
+ e.status = status || 500;
+ e.type = type;
+ e.logCode = logCode || 900;
+ e.target = target || '';
+
+ return e;
+};
+
+
diff --git a/lib/events.js b/lib/events.js
index 11a3ec0..743200d 100644
--- a/lib/events.js
+++ b/lib/events.js
@@ -44,7 +44,7 @@ router.use(middleware.expandTemplates); // Expand any blueprint templates
/* Accept an incoming event */
router.post('/', function(req, res, next) {
- let response = {requestId: req.dcaeReqId, deploymentIds:[]};
+ var response = {requestId: req.dcaeReqId, deploymentIds:[]};
if (req.body.dcae_service_action === 'deploy') {
@@ -56,8 +56,8 @@ router.post('/', function(req, res, next) {
logger.debug(JSON.stringify(req.dcae_locations, null, '\t'));
/* Create a deployer function and use it for each of the services */
- let deployer = services.createDeployer(req);
- let outputs = req.dcae_blueprints.map(deployer);
+ var deployer = services.createDeployer(req);
+ var outputs = req.dcae_blueprints.map(deployer);
response.deploymentIds = req.dcae_blueprints.map(function(s) {return s.deploymentId;});
}
else {
@@ -68,12 +68,12 @@ router.post('/', function(req, res, next) {
logger.info(req.dcaeReqId + " deployments to undeploy: " + JSON.stringify(req.dcae_services));
/* Create an undeployer function and use it for each of the services */
- let undeployer = services.createUndeployer(req);
+ var undeployer = services.createUndeployer(req);
req.dcae_services.forEach(undeployer);
response.deploymentIds = req.dcae_services.map(function(s) {return s.deploymentId;});
}
res.status(202).json(response);
- logAccess(req, 202);
+ logAccess(req, 202, req.body.dcae_service_action);
});
module.exports = router; \ No newline at end of file
diff --git a/lib/inventory.js b/lib/inventory.js
index 5124a98..fbf83df 100644
--- a/lib/inventory.js
+++ b/lib/inventory.js
@@ -19,6 +19,7 @@ See the License for the specific language governing permissions and limitations
"use strict";
const req = require('./promise_request');
+ const createError = require('./dispatcher-error').createDispatcherError;
const INV_SERV_TYPES = '/dcae-service-types';
const INV_SERVICES = '/dcae-services';
@@ -34,12 +35,20 @@ See the License for the specific language governing permissions and limitations
return [];
}
else {
- var newErr = new Error("Error response " + err.status + " from DCAE inventory: " + err.body);
- newErr.status = 502;
- newErr.code = 502;
+ var newErr;
+ var message;
+ if (err.status) {
+ /* Got a response from inventory indicating an error */
+ message = "Error response " + err.status + " from DCAE inventory: " + err.body;
+ newErr = createError(message, 502, "api", 501, "dcae-inventory");
+ }
+ else {
+ /* Problem connecting to inventory */
+ message = "Error communicating with inventory: " + err.message;
+ newErr = createError(message, 504, "system", 201, "dcae-inventory");
+ }
throw newErr;
- }
-
+ }
};
/*
@@ -48,30 +57,45 @@ See the License for the specific language governing permissions and limitations
* objects, each object having: - type: the service type name associated
* with the blueprint template - template: the blueprint template
*/
- const findTemplates = function(targetType, location, serviceId) {
+ const findTemplates = function(targetType, location, serviceId, asdcServiceId, asdcResourceId) {
/* Set up query string based on available parameters */
- var qs = {vnfType: targetType, serviceLocation: location };
+ var qs = {serviceLocation: location, onlyActive: true, onlyLatest: true};
+
if (serviceId) {
qs.serviceId = serviceId;
}
+
+ if (asdcResourceId) {
+ qs.asdcResourceId = asdcResourceId;
+ }
+
+ if (asdcServiceId){
+ qs.asdcServiceId = asdcServiceId;
+ }
+
+ /* We'll set vnfType in the query except when both asdcServiceId and asdcResourceId are populated */
+ if (!(asdcResourceId && asdcServiceId)) {
+ qs.vnfType = targetType;
+ }
+ /* Make the request to inventory */
const reqOptions = {
- method : "GET",
- uri : config.inventory.url + INV_SERV_TYPES,
- qs: qs
- };
- return req.doRequest(reqOptions)
- .then(function(result) {
- let templates = [];
- let content = JSON.parse(result.body);
- if (content.items) {
- /* Pick out the fields we want */
- templates = content.items.map(function(i) {return {type: i.typeName, template: i.blueprintTemplate};});
- }
- return templates;
- })
- .catch (invError);
+ method : "GET",
+ uri : config.inventory.url + INV_SERV_TYPES,
+ qs: qs
+ };
+ return req.doRequest(reqOptions)
+ .then(function(result) {
+ var templates = [];
+ var content = JSON.parse(result.body);
+ if (content.items) {
+ /* Pick out the fields we want */
+ templates = content.items.map(function(i) {return {type: i.typeId, template: i.blueprintTemplate};});
+ }
+ return templates;
+ })
+ .catch (invError);
};
/*
@@ -88,8 +112,8 @@ See the License for the specific language governing permissions and limitations
};
return req.doRequest(reqOptions)
.then(function(result) {
- let services = [];
- let content = JSON.parse(result.body);
+ var services = [];
+ var content = JSON.parse(result.body);
if(content.items) {
/* Pick out the fields we want */
services = content.items.map(function(i) { return {type: i.typeLink.title, deploymentId: i.deploymentRef};});
@@ -113,8 +137,8 @@ See the License for the specific language governing permissions and limitations
};
return req.doRequest(reqOptions)
.then(function(result) {
- let shareables = {};
- let content = JSON.parse(result.body);
+ var shareables = {};
+ var content = JSON.parse(result.body);
if (content.items) {
content.items.forEach(function(s) {
s.components.filter(function(c) {return c.shareable === 1;})
@@ -130,25 +154,42 @@ See the License for the specific language governing permissions and limitations
/*
- * Middleware-style function to check inventory. For 'deploy' operations,
- * finds blueprint templates and shareable components Attaches list of
- * templates to req.dcae_templates, object with shareable components to
- * req.dcae_shareables. For 'undeploy' operations, finds deployed services.
- * Attaches list of deployed services to req.dcae_services.
- */
+ * Middleware-style function to check inventory.
+ *
+ * For 'deploy' operations:
+ * - finds blueprint templates and shareable components
+ * - attaches list of templates to req.dcae_templates
+ * - attaches object with shareable components to req.dcae_shareables
+ *
+ * For 'undeploy' operations:
+ * - finds deployed services
+ * - attaches list of deployed services to req.dcae_services
+ */
exports.checkInventory = function(req, res, next) {
- if (req.body.dcae_service_action.toLowerCase() === 'deploy'){
- findTemplates(req.body.dcae_target_type, req.body.dcae_service_location, req.body.dcae_service_type)
+ if (req.body.dcae_service_action.toLowerCase() === 'deploy') {
+ findTemplates(
+ req.body.dcae_target_type,
+ req.body.dcae_service_location,
+ req.body.dcae_service_type,
+ req.body['dcae_service-instance_persona-model-id'],
+ req.body['dcae_generic-vnf_persona-model-id']
+ )
.then(function (templates) {
if (templates.length > 0) {
req.dcae_templates = templates;
return templates;
}
else {
- var paramList = [req.body.dcae_target_type, req.body.dcae_service_location, req.body.dcae_service_type ? req.body.dcae_service_type : "unspecified"].join('/');
- let err = new Error(paramList + ' has no associated DCAE service types');
- err.status = 400;
- next(err);
+ var paramList = [
+ req.body.dcae_target_type,
+ req.body.dcae_service_location,
+ req.body.dcae_service_type || "unspecified",
+ req.body['dcae_service-instance_persona-model-id'] || "unspecified",
+ req.body['dcae_generic-vnf_persona-model-id'] || "unspecified"
+ ].join('/');
+ var err0 = new Error(paramList + ' has no associated DCAE service types');
+ err0.status = 400;
+ next(err0);
}
})
.then(function(result) {
@@ -170,9 +211,9 @@ See the License for the specific language governing permissions and limitations
next();
}
else {
- let err = new Error('"' + req.body.dcae_target_name + '" has no deployed DCAE services');
- err.status = 400;
- next(err);
+ var err1 = new Error('"' + req.body.dcae_target_name + '" has no deployed DCAE services');
+ err1.status = 400;
+ next(err1);
}
})
.catch(function(err) {
@@ -180,9 +221,9 @@ See the License for the specific language governing permissions and limitations
});
}
else {
- let err = new Error ('"' + req.body.dcae_service_action + '" is not a valid service action. Valid actions: "deploy", "undeploy"');
- err.status = 400;
- next(err);
+ var err2 = new Error ('"' + req.body.dcae_service_action + '" is not a valid service action. Valid actions: "deploy", "undeploy"');
+ err2.status = 400;
+ next(err2);
}
};
@@ -192,12 +233,12 @@ See the License for the specific language governing permissions and limitations
exports.addService = function(deploymentId, serviceType, vnfId, vnfType, vnfLocation, outputs) {
/* Create the service description */
- let serviceDescription =
+ var serviceDescription =
{
- "typeName" : serviceType,
"vnfId" : vnfId,
"vnfType" : vnfType,
"vnfLocation" : vnfLocation,
+ "typeId" : serviceType,
"deploymentRef" : deploymentId
};
@@ -226,4 +267,82 @@ See the License for the specific language governing permissions and limitations
exports.deleteService = function(serviceId) {
return req.doRequest({method: "DELETE", uri: config.inventory.url + INV_SERVICES + "/" + serviceId});
};
- \ No newline at end of file
+
+ /*
+ * Find running/deploying instances of services (with a given type name, if specified)
+ */
+
+ exports.getServicesByType = function(query) {
+ var options = {
+ method: 'GET',
+ uri: config.inventory.url + INV_SERVICES,
+ qs: query || {}
+ };
+
+ return req.doRequest(options)
+ .then (function (result) {
+ var services = [];
+ var content = JSON.parse(result.body);
+ if(content.items) {
+ /* Pick out the fields we want */
+ services = content.items.map(function(i) { return { deploymentId: i.deploymentRef, serviceTypeId: i.typeId};});
+ }
+ return services;
+ })
+ .catch(invError);
+ };
+
+ /*
+ * Find a blueprint given the service type ID -- return blueprint and type ID
+ */
+ exports.getBlueprintByType = function(serviceTypeId) {
+ return req.doRequest({
+ method: "GET",
+ uri: config.inventory.url + INV_SERV_TYPES + '/' + serviceTypeId
+ })
+ .then (function(result) {
+ var blueprintInfo = {};
+ var content = JSON.parse(result.body);
+ blueprintInfo.blueprint = content.blueprintTemplate;
+ blueprintInfo.typeId = content.typeId;
+
+ return blueprintInfo;
+ })
+ .catch(invError);
+ };
+
+ /*
+ * Verify that the specified deployment ID does not already have
+ * an entry in inventory. This is needed to enforce the rule that
+ * creating a second instance of a deployment under the
+ * same ID as an existing deployment is not permitted.
+ * The function checks for a service in inventory using the
+ * deployment ID as service name. If it doesn't exist, the function
+ * resolves its promise. If it *does* exist, then it throws an error.
+ */
+ exports.verifyUniqueDeploymentId = function(deploymentId) {
+
+ return req.doRequest({
+ method: "GET",
+ uri: config.inventory.url + INV_SERVICES + "/" + deploymentId
+ })
+
+ /* Successful lookup -- the deployment exists, so throw an error */
+ .then(function(res) {
+ throw createError("Deployment " + deploymentId + " already exists", 409, "api", 501);
+ },
+
+ /* Error from the lookup -- either deployment ID doesn't exist or some other problem */
+ function (err) {
+
+ /* Inventory returns a 404 if it does not find the deployment ID */
+ if (err.status && err.status === 404) {
+ return true;
+ }
+
+ /* Some other error -- it really is an error and we can't continue */
+ else {
+ return invError(err);
+ }
+ });
+ }
diff --git a/lib/logging.js b/lib/logging.js
index db168b3..21e6d68 100644
--- a/lib/logging.js
+++ b/lib/logging.js
@@ -16,16 +16,128 @@ See the License for the specific language governing permissions and limitations
"use strict";
const config = process.mainModule.exports.config;
-const accessLogger = config.logSource.getLogger('access');
+const auditLogger = config.logSource.getLogger('audit');
+const defaultLogger = config.logSource.getLogger();
+/* Audit log fields */
+const AUDIT_BEGIN = 0;
+const AUDIT_END = 1;
+const AUDIT_REQID = 2;
+const AUDIT_SVCINST = 3;
+const AUDIT_THREAD = 4;
+const AUDIT_SRVNAME = 5;
+const AUDIT_SVCNAME = 6;
+const AUDIT_PARTNER = 7;
+const AUDIT_STATUSCODE = 8;
+const AUDIT_RESPCODE = 9;
+const AUDIT_RESPDESC = 10;
+const AUDIT_INSTUUID = 11;
+const AUDIT_CATLOGLEVEL = 12;
+const AUDIT_SEVERITY = 13;
+const AUDIT_SRVIP = 14;
+const AUDIT_ELAPSED = 15;
+const AUDIT_SERVER = 16;
+const AUDIT_CLIENTIP = 17;
+const AUDIT_CLASSNAME = 18;
+const AUDIT_UNUSED = 19;
+const AUDIT_PROCESSKEY = 20;
+const AUDIT_CUSTOM1 = 21;
+const AUDIT_CUSTOM2 = 22;
+const AUDIT_CUSTOM3 = 23;
+const AUDIT_CUSTOM4 = 24;
+const AUDIT_DETAILMSG = 25;
+const AUDIT_NFIELDS = 26;
-/* Logging */
+/* Error log fields */
+const ERROR_TIMESTAMP = 0;
+const ERROR_REQID = 1;
+const ERROR_THREAD = 2;
+const ERROR_SVCNAME = 3;
+const ERROR_PARTNER = 4;
+const ERROR_TGTENTITY = 5;
+const ERROR_TGTSVC = 6;
+const ERROR_CATEGORY = 7;
+const ERROR_CODE = 8;
+const ERROR_DESCRIPTION = 9;
+const ERROR_MESSAGE = 10;
+const ERROR_NFIELDS = 11;
-exports.logAccess = function (req, status, extra) {
- let entry = req.dcaeReqId + " " + req.connection.remoteAddress + " " + req.method + " " + req.originalUrl + " " + status;
+/* Error code -> description mapping */
+const descriptions = {
+
+ 201: 'Inventory communication error',
+ 202: 'Cloudify Manager communication error',
+
+ 501: 'Inventory API error',
+ 502: 'Cloudify Manager API error',
+
+ 551: 'HTTP(S) Server initialization error',
+ 552: 'Dispatcher start-up error',
+
+ 999: 'Unknown error'
+};
+
+/* Format audit record for an incoming API request */
+const formatAuditRecord = function(req, status, extra) {
+ var rec = new Array(AUDIT_NFIELDS);
+ const end = new Date();
+ rec[AUDIT_END] = end.toISOString();
+ rec[AUDIT_BEGIN] = req.startTime.toISOString();
+ rec[AUDIT_REQID] = req.dcaeReqId;
+ rec[AUDIT_SRVNAME] = req.hostname; // Use the value from the Host header
+ rec[AUDIT_SVCNAME] = req.method + ' ' + req.originalUrl; // Method and URL identify the operation being performed
+ rec[AUDIT_STATUSCODE] = (status < 300 ) ? "COMPLETE" : "ERROR";
+ rec[AUDIT_RESPCODE] = status; // Use the HTTP status code--does not match the table in the logging spec, but makes more sense
+ rec[AUDIT_CATLOGLEVEL] = "INFO"; // The audit records are informational, regardless of the outcome of the operation
+ rec[AUDIT_SRVIP] = req.socket.address().address;
+ rec[AUDIT_ELAPSED] = end - req.startTime;
+ rec[AUDIT_SERVER] = req.hostname // From the Host header, again
+ rec[AUDIT_CLIENTIP] = req.connection.remoteAddress;
+
if (extra) {
- extra = extra.replace(/\n/g, " "); /* Collapse multi-line extra data to a single line */
- entry = entry + " <" + extra + ">";
+ rec[AUDIT_DETAILMSG]= extra.replace(/\n/g, " "); /* Collapse multi-line extra data to a single line */
+ }
+ return rec.join('|');
+};
+
+/* Format error log record */
+const formatErrorRecord = function(category, code, detail, req, target) {
+ var rec = new Array(ERROR_NFIELDS);
+
+ /* Common fields */
+ rec[ERROR_TIMESTAMP] = (new Date()).toISOString();
+ rec[ERROR_CATEGORY] = category;
+ rec[ERROR_CODE] = code;
+ rec[ERROR_DESCRIPTION] = descriptions[code] || 'no description available';
+
+ /* Log error detail in a single line if provided */
+ if (detail) {
+ rec[ERROR_MESSAGE] = detail.replace(/\n/g, " ");
+ }
+
+ /* Fields available if the error happened during processing of an incoming API request */
+ if (req) {
+ rec[ERROR_REQID] = req.dcaeReqId;
+ rec[ERROR_SVCNAME] = req.method + ' ' + req.originalUrl; // Method and URL identify the operation being performed
+ rec[ERROR_PARTNER] = req.connection.remoteAddress; // We don't have the partner's name, but we know the remote IP address
}
- accessLogger.info(entry);
-}; \ No newline at end of file
+
+ /* Include information about the target entity/service if available */
+ if (target) {
+ rec[ERROR_TGTENTITY] = target.entity || '';
+ rec[ERROR_TGTSVC] = target.service || '';
+ }
+ return rec.join('|');
+};
+
+exports.logAccess = function(req, status, extra) {
+ auditLogger.info(formatAuditRecord(req, status, extra));
+};
+
+exports.logError = function(error, req) {
+ defaultLogger.error(formatErrorRecord("ERROR", error.logCode, error.message, req, {entity: error.target}));
+};
+
+exports.logWarn = function(error, req) {
+ defaultLogger.error(formatErrorRecord("WARN", error.logCode, error.message, req, {entity: error.target}));
+};
diff --git a/lib/middleware.js b/lib/middleware.js
index 567620f..5f78b20 100644
--- a/lib/middleware.js
+++ b/lib/middleware.js
@@ -20,26 +20,31 @@ See the License for the specific language governing permissions and limitations
const ejs = require('ejs');
const utils = require('./utils');
-const logAccess = require('./logging').logAccess;
+const logging = require('./logging');
+const logAccess = logging.logAccess;
+const logError = logging.logError;
+const logWarn = logging.logWarn;
const config = process.mainModule.exports.config;
const locations = config.locations;
-const logger = config.logSource.getLogger("errhandler");
-/* Assign a request ID to each incoming request */
+/* Assign a request ID and start time to each incoming request */
exports.assignId = function(req, res, next) {
- req.dcaeReqId = utils.generateId();
+ /* Use request ID from header if available, otherwise generate one */
+ req.dcaeReqId = req.get('X-ECOMP-RequestID') || utils.generateId();
+ req.startTime = new Date();
next();
};
+
/* Error handler -- send error with JSON body */
exports.handleErrors = function(err, req, res, next) {
- let status = err.status || 500;
- let msg = err.message || err.body || 'unknown error'
+ var status = err.status || 500;
+ var msg = err.message || err.body || 'unknown error'
res.status(status).type('application/json').send({status: status, message: msg });
logAccess(req, status, msg);
if (status >= 500) {
- logger.error(req.dcaeReqId + " Error: " + JSON.stringify({message: msg, code: err.code, status: status}));
+ logError(err, req);
}
};
@@ -53,7 +58,7 @@ exports.checkType = function(type){
next();
}
else {
- let err = new Error ('Content-Type must be \'' + type +'\'');
+ var err = new Error ('Content-Type must be \'' + type +'\'');
err.status = 415;
next (err);
}
@@ -65,7 +70,7 @@ exports.checkProps = function(props) {
return function (req, res, next) {
const missing = props.filter(function(p){return !utils.hasProperty(req.body,p);});
if (missing.length > 0) {
- let err = new Error ('Request missing required properties: ' + missing.join(','));
+ var err = new Error ('Request missing required properties: ' + missing.join(','));
err.status = 400;
next(err);
}
@@ -83,7 +88,7 @@ exports.checkLocation = function(req, res, next) {
next();
}
else {
- let err = new Error ('"' + req.body.dcae_service_location + '" is not a supported location');
+ var err = new Error ('"' + req.body.dcae_service_location + '" is not a supported location');
err.status = 400;
next(err);
}
@@ -97,14 +102,14 @@ exports.checkLocation = function(req, res, next) {
exports.expandTemplates = function(req, res, next) {
/* Build the context for rendering the template */
- let context = req.body; // start with the body of POST /events request
+ var context = req.body; // start with the body of POST /events request
context.locations = req.dcae_locations; // location information from the location "map" in config file
context.dcae_shareables = req.dcae_shareables; // local shareable components
/* Expand the templates */
try {
if (req.dcae_templates) { // There won't be any templates for an undeploy
- let blueprints = req.dcae_templates.map(function (template) {
+ var blueprints = req.dcae_templates.map(function (template) {
//TODO possibly compute intensive- is there a better way?
return {
blueprint: ejs.render(template.template, context),
diff --git a/lib/promise_request.js b/lib/promise_request.js
index 906c16c..5a97bba 100644
--- a/lib/promise_request.js
+++ b/lib/promise_request.js
@@ -19,26 +19,53 @@ See the License for the specific language governing permissions and limitations
"use strict";
/*
- * Make an HTTP request using a string or a readable stream for the body
+ * Make an HTTP request using a string for the body
* of the request.
* Return a promise for result in the form
* {status: <http status code>, body: <response body>}
*/
-const stream = require('stream');
-const request = require('request');
+const http = require('http');
+const https = require('https');
+const url = require('url');
+const querystring = require('querystring');
-let logger = null;
+var logger = null;
exports.doRequest = function(options, body) {
+
return new Promise(function(resolve, reject) {
+
+ var reqBody = null;
+ if (options.json) {
+ reqBody = JSON.stringify(options.json);
+ options.headers = options.headers || {};
+ options.headers['Content-Type'] = 'application/json';
+ }
+ else if (body) {
+ reqBody = body;
+ }
+
+ if (options.uri) {
+ var parsed = url.parse(options.uri);
+ options.protocol = parsed.protocol;
+ options.hostname = parsed.hostname;
+ options.port = parsed.port;
+ options.path = parsed.path;
+ if (options.qs) {
+ options.path += ('?' + querystring.stringify(options.qs));
+ }
+ }
logger.debug("doRequest: " + JSON.stringify(options));
- if (body && !(body instanceof stream.Readable)) {
- // Body is a string, just include it in the request
- options.body = body;
+
+ try {
+ var req = (options.protocol === 'https:' ? https.request(options) : http.request(options));
+ }
+ catch (e) {
+ logger.debug('Error constructing request: ' + e);
+ reject(e);
}
- var req = request(options);
// Reject promise if there's an error
req.on('error', function(error) {
@@ -88,13 +115,12 @@ exports.doRequest = function(options, body) {
}
});
});
-
- // If there's a readable stream for a body, pipe it to the request
- if (body && (body instanceof stream.Readable)) {
- // Pipe the body readable stream into the request
- body.pipe(req);
+
+ if (reqBody) {
+ req.write(reqBody, 'utf8');
}
- });
+ req.end();
+ });
};
exports.setLogger = function(logsource) {
diff --git a/lib/services.js b/lib/services.js
index 5a2a7d7..9508efd 100644
--- a/lib/services.js
+++ b/lib/services.js
@@ -22,6 +22,8 @@ const ejs = require('ejs');
const deploy = require('./deploy');
const inventory = require('./inventory');
const config = process.mainModule.exports.config;
+const logError = require('./logging').logError;
+const logAudit = require('./logging').logAccess;
/* Set up logging */
var logger = config.logSource.getLogger("services");
@@ -35,7 +37,7 @@ exports.createDeployer = function(req) {
return function(blueprint) {
/* Generate a deploymentId */
- let deploymentId = blueprint.deploymentId;
+ var deploymentId = blueprint.deploymentId;
/* Attempt the deployment */
logger.info(req.dcaeReqId + " " + "Attempting to deploy deploymentId " + deploymentId);
@@ -59,10 +61,14 @@ exports.createDeployer = function(req) {
);
})
.then(function(result) {
- logger.info(req.dcaeReqId + " Updated inventory for deploymentId: " + deploymentId);
+ logAudit(req, 200, "Deployed id: " + deploymentId);
})
.catch(function(err) {
- logger.error(req.dcaeReqId + " Failed to deploy deploymentId: " + deploymentId + " Error: " + JSON.stringify(err));
+ /* err should be a standard dispatcher error generated by the deploy or inventory modules */
+ /* Enrich the message with the deployment ID */
+ err.message = "Error deploying " + deploymentId + ": " + err.message;
+ logError(err, req);
+ logAudit(req, 500, err.message);
//TODO try uninstall?
});
};
@@ -87,10 +93,14 @@ exports.createUndeployer = function(req) {
return inventory.deleteService(deployment.deploymentId);
})
.then(function(result){
- logger.info(req.dcaeReqId + " Deleted service from inventory for deploymentId: " + deployment.deploymentId);
+ logAudit(req, 200, "Undeployed id: " + deployment.deploymentId);
})
.catch(function(err){
- logger.error(req.dcaeReqId + " Error undeploying " + deployment.deploymentId + ": " + JSON.stringify(err));
+ /* err should be a standard dispatcher error generated by the deploy or inventory modules */
+ /* Enrich the message with the deployment ID */
+ err.message = "Error undeploying " + deployment.deploymentId + ": " + err.message;
+ logError(err, req);
+ logAudit(req, 500, err.message);
});
};
diff --git a/lib/utils.js b/lib/utils.js
index bf582e8..856ace8 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -16,49 +16,9 @@ See the License for the specific language governing permissions and limitations
"use strict";
-// Utility functions
-
-var fs = require('fs');
-var rimraf = require('rimraf');
-var uuid = require('node-uuid');
-
-// Create a directory (named 'dirName') and write 'content' into a file (named 'fileName') in that directory.
-exports.makeDirAndFile = function(dirName, fileName, content){
-
- return new Promise(function(resolve, reject){
- fs.mkdir(dirName, function(err) {
- if (err) {
- reject(err);
- }
- else {
- fs.writeFile(dirName + "/" + fileName, content, function(err, fd) {
- if (err) {
- reject(err);
- }
- else {
- resolve();
- }
-
- });
- }
- });
-
- });
-};
+const uuid = require('node-uuid');
-// Remove directory and its contents
-exports.removeDir = function(dirName) {
- return new Promise(function(resolve, reject){
- rimraf(dirName, function(err) {
- if (err) {
- reject(err);
- }
- else {
- resolve();
- }
- });
- });
-};
+// Utility functions
/* Does object 'o' have property 'key' */
exports.hasProperty = function(o, key) {