diff options
author | Jack Lucas <jflucas@research.att.com> | 2017-05-10 01:48:41 +0000 |
---|---|---|
committer | Jack Lucas <jflucas@research.att.com> | 2017-05-10 01:48:41 +0000 |
commit | ff6ba434b6d91b6a4a4e9b3a7fbb8cadced229ad (patch) | |
tree | 8ae919ab8d1b174944bd6bea6602ce29153be2f6 /lib | |
parent | 0806707cbc2dd6311228facabf0a6052862c51c7 (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.js | 10 | ||||
-rw-r--r-- | lib/cloudify.js | 231 | ||||
-rw-r--r-- | lib/config.js | 23 | ||||
-rw-r--r-- | lib/dcae-deployments.js | 242 | ||||
-rw-r--r-- | lib/deploy.js | 184 | ||||
-rw-r--r-- | lib/dispatcher-error.js | 53 | ||||
-rw-r--r-- | lib/events.js | 10 | ||||
-rw-r--r-- | lib/inventory.js | 213 | ||||
-rw-r--r-- | lib/logging.js | 128 | ||||
-rw-r--r-- | lib/middleware.js | 29 | ||||
-rw-r--r-- | lib/promise_request.js | 54 | ||||
-rw-r--r-- | lib/services.js | 20 | ||||
-rw-r--r-- | lib/utils.js | 44 |
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) { |