From f2ec39706a7a31017f5d219c44d54d40714d9a27 Mon Sep 17 00:00:00 2001 From: lj1412 Date: Tue, 14 Feb 2017 15:10:25 +0000 Subject: Init dcae.orch-dispatcher Change-Id: I52aa696bd5d1d5ed3bc6e03a3c994dc0b3a71062 Signed-off-by: lj1412 --- .gitignore | 8 ++ .gitreview | 4 + Dockerfile | 18 ++++ LICENSE.txt | 22 ++++ README.md | 8 ++ dispatcher.js | 143 ++++++++++++++++++++++++ dispatcherAPI.md | 130 ++++++++++++++++++++++ dispatcherAPI.yaml | 192 +++++++++++++++++++++++++++++++++ etc/config.json.development | 11 ++ etc/log4js.json | 28 +++++ lib/auth.js | 65 +++++++++++ lib/cloudify.js | 257 ++++++++++++++++++++++++++++++++++++++++++++ lib/config.js | 140 ++++++++++++++++++++++++ lib/deploy.js | 195 +++++++++++++++++++++++++++++++++ lib/events.js | 79 ++++++++++++++ lib/info.js | 39 +++++++ lib/inventory.js | 229 +++++++++++++++++++++++++++++++++++++++ lib/logging.js | 31 ++++++ lib/middleware.js | 126 ++++++++++++++++++++++ lib/promise_request.js | 102 ++++++++++++++++++ lib/repeat.js | 54 ++++++++++ lib/services.js | 97 +++++++++++++++++ lib/setup.js | 37 +++++++ lib/utils.js | 79 ++++++++++++++ package.json | 23 ++++ version.js | 1 + 26 files changed, 2118 insertions(+) create mode 100644 .gitignore create mode 100644 .gitreview create mode 100644 Dockerfile create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 dispatcher.js create mode 100644 dispatcherAPI.md create mode 100644 dispatcherAPI.yaml create mode 100644 etc/config.json.development create mode 100644 etc/log4js.json create mode 100644 lib/auth.js create mode 100644 lib/cloudify.js create mode 100644 lib/config.js create mode 100644 lib/deploy.js create mode 100644 lib/events.js create mode 100644 lib/info.js create mode 100644 lib/inventory.js create mode 100644 lib/logging.js create mode 100644 lib/middleware.js create mode 100644 lib/promise_request.js create mode 100644 lib/repeat.js create mode 100644 lib/services.js create mode 100644 lib/setup.js create mode 100644 lib/utils.js create mode 100644 package.json create mode 100644 version.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8804e84 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.jshintrc +.project +.settings/ +.sublime-project +.tern-project +node_modules/ +work/ +log/ diff --git a/.gitreview b/.gitreview new file mode 100644 index 0000000..2198e9c --- /dev/null +++ b/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=gerrit.openecomp.org +port=29418 +project=dcae/orch-dispatcher.git diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..68811e3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM node:4.6.0 +MAINTAINER maintainer +ENV INSROOT /opt/app +ENV APPUSER dispatcher +RUN mkdir -p ${INSROOT}/${APPUSER}/lib \ + && mkdir -p ${INSROOT}/${APPUSER}/etc \ + && mkdir -p ${INSROOT}/${APPUSER}/log \ + && useradd -d ${INSROOT}/${APPUSER} ${APPUSER} +COPY *.js ${INSROOT}/${APPUSER}/ +COPY package.json ${INSROOT}/${APPUSER}/ +COPY lib ${INSROOT}/${APPUSER}/lib/ +COPY etc/config.json.development ${INSROOT}/${APPUSER}/etc/config.json +COPY etc/log4js.json ${INSROOT}/${APPUSER}/etc/log4js.json +WORKDIR ${INSROOT}/${APPUSER} +RUN npm install --production && chown -R ${APPUSER}:${APPUSER} ${INSROOT}/${APPUSER} && npm remove -g npm +USER ${APPUSER} +VOLUME ${INSROOT}/${APPUSER}/log +ENTRYPOINT ["/usr/local/bin/node", "dispatcher.js"] diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..ae12da2 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +/* + * ============LICENSE_START========================================== + * =================================================================== + * Copyright © 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. + * ============LICENSE_END============================================ + * + * ECOMP and OpenECOMP are trademarks + * and service marks of AT&T Intellectual Property. + * + */ diff --git a/README.md b/README.md new file mode 100644 index 0000000..1255389 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +### DCAE-Orch-Dispatcher + +The DCAE Dispatcher is one component of a future capability that allows +DCAE monitoring services to be deployed on demand in response to other +OpenECOMP events. + +Instructions for building the Dispatcher, configuring it, and integrating +with the rest of DCAE and OpenECOMP will come in a later release. diff --git a/dispatcher.js b/dispatcher.js new file mode 100644 index 0000000..f8f610f --- /dev/null +++ b/dispatcher.js @@ -0,0 +1,143 @@ +/* +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. +*/ + +/* Dispatcher main */ + +"use strict"; + +const API_VERSION = "2.0.0"; + +const fs = require('fs'); +const util = require('util'); +const http = require('http'); +const https = require('https'); +const express = require('express'); +const daemon = require('daemon'); +const conf = require('./lib/config'); +const req = require('./lib/promise_request'); + +/* Paths for API routes */ +const INFO_PATH = "/"; +const EVENTS_PATH = "/events"; + +/* Set up log and work directories */ +try { fs.mkdirSync("./log"); } catch (e) { } +try { fs.mkdirSync("./work"); } catch(e) { } + +/* Set up logging */ +const log4js = require('log4js'); +log4js.configure('etc/log4js.json'); +const logger = log4js.getLogger('dispatcher'); +logger.info("Starting dispatcher"); + +try { + /* Set configuration and make available to other modules */ + let config = conf.configure(process.argv[2] || "./etc/config.json"); + + config.locations = conf.getLocations(process.argv[3] || "./etc/locations.json"); + if (Object.keys(config.locations).length < 1) { + logger.warn('No locations specified') + } + + /* Set log level--config will supply a default of "INFO" if not explicitly set in config.json */ + log4js.setGlobalLogLevel(config.logLevel); + + config.logSource = log4js; + config.version = require('./version').version; + config.apiVersion = API_VERSION; + config.apiLinks = { + info: INFO_PATH, + events: EVENTS_PATH + }; + exports.config = config; + req.setLogger(log4js); + + /* Set up the application */ + const app = express(); + app.set('x-powered-by', false); + app.set('etag', false); + + /* Give each request a unique request ID */ + app.use(require('./lib/middleware').assignId); + + /* If authentication is set up, check it */ + app.use(require('./lib/auth').checkAuth); + + /* Set up API routes */ + app.use(EVENTS_PATH, require('./lib/events')); + app.use(INFO_PATH, require('./lib/info')); + + /* Set up error handling */ + app.use(require('./lib/middleware').handleErrors); + + /* Start the server */ + let server = null; + let usingTLS = false; + try { + if (config.ssl && config.ssl.pfx && config.ssl.passphrase && config.ssl.pfx.length > 0) { + /* Check for non-zero pfx length--DCAE config will deliver an empty pfx if no cert + * available for the host. */ + server = https.createServer({pfx: config.ssl.pfx, passphrase: config.ssl.passphrase}, app); + usingTLS = true; + } + else { + server = http.createServer(app); + } + } + catch (e) { + logger.fatal ('Could not create http(s) server--exiting: ' + e); + console.log ('Could not create http(s) server--exiting: ' + e); + process.exit(2); + } + + server.setTimeout(0); + + server.listen(config.listenPort, config.listenHost, function() { + let addr = server.address(); + logger.info("Dispatcher version " + config.version + + " listening on " + addr.address + ":" + addr.port + + " pid: " + process.pid + + (usingTLS ? " " : " not ") + "using TLS (HTTPS)"); + }); + + /* Daemonize */ + if (!config.foreground) { + daemon(); + } + + /* Set up handling for terminate signal */ + process.on('SIGTERM', function(){ + logger.info("Dispatcher API server shutting down."); + server.close(function() { + logger.info("Dispatcher API server shut down."); + }); + }); + + /* Log actual exit */ + /* logger.info() is asynchronous, so we will see + * another beforeExit event after it completes. + */ + let loggedExit = false; + process.on('beforeExit', function() { + if (!loggedExit) { + loggedExit = true; + logger.info("Dispatcher process exiting."); + } + }); +} +catch (e) { + logger.fatal("Dispatcher exiting due to start-up problems: " + e); +} diff --git a/dispatcherAPI.md b/dispatcherAPI.md new file mode 100644 index 0000000..1380337 --- /dev/null +++ b/dispatcherAPI.md @@ -0,0 +1,130 @@ +# Dispatcher API + + + +## Overview +High-level API for deploying/deploying composed services using Cloudify Manager. + + +### Version information +*Version* : 2.0.0 + + + + + +## Paths + + +### GET / + +#### Description +Get API version information, links to API operations, and location data + + +#### Responses + +|HTTP Code|Description|Schema| +|---|---|---| +|**200**|Success|[DispatcherInfo](#dispatcherinfo)| + + +**DispatcherInfo** + +|Name|Description|Schema| +|---|---|---| +|**apiVersion**
*optional*|version of API supported by this server|string| +|**links**
*optional*|Links to API resources|[links](#get-links)| +|**locations**
*optional*|Information about DCAE locations known to this dispatcher|object| +|**serverVersion**
*optional*|version of software running on this server|string| + + +**links** + +|Name|Description|Schema| +|---|---|---| +|**dcaeServiceInstances**
*optional*|root of DCAE service instance resource tree|string| +|**status**
*optional*|link to server status information|string| + + + +### POST /events + +#### Description +Signal an event that triggers deployment or undeployment of a DCAE service + + +#### Parameters + +|Type|Name|Description|Schema|Default| +|---|---|---|---|---| +|**Body**|**dcae_event**
*required*||[DCAEEvent](#dcaeevent)|| + + +#### Responses + +|HTTP Code|Description|Schema| +|---|---|---| +|**202**|Success: The content that was posted is valid, the dispatcher has
found the needed blueprint (for a deploy operation) or the existing deployment
(for an undeploy operation), and is initiating the necessary orchestration steps.|[DCAEEventResponse](#dcaeeventresponse)| +|**400**|Bad request: See the message in the response for details.|[DCAEErrorResponse](#dcaeerrorresponse)| +|**415**|Bad request: The Content-Type header does not indicate that the content is
'application/json'|[DCAEErrorResponse](#dcaeerrorresponse)| +|**500**|Problem on the server side, possible with downstream systems. See the message
in the response for more details.|[DCAEErrorResponse](#dcaeerrorresponse)| + + +#### Consumes + +* `application/json` + + +#### Produces + +* `application/json` + + + + + +## Definitions + + +### DCAEErrorResponse +Object reporting an error. + + +|Name|Description|Schema| +|---|---|---| +|**message**
*optional*|Human-readable description of the reason for the error|string| +|**status**
*required*|HTTP status code for the response|integer| + + + +### DCAEEvent +Data describing an event that should trigger a deploy or undeploy operation for one +or more DCAE services. + + +|Name|Description|Schema| +|---|---|---| +|**aai_additional_info**
*optional*|Additional information, not carried in the event, obtained from an A&AI query or set of queries. Data in this object is available for populating deployment-specific values in the blueprint.|object| +|**dcae_service_action**
*required*|Indicates whether the event requires a DCAE service to be deployed or undeployed.
Valid values are 'deploy' and 'undeploy'.|string| +|**dcae_service_location**
*required*|The location at which the DCAE service is to be deployed or from which it is to be
undeployed.|string| +|**dcae_service_type**
*optional*|Identifier for the service of which the target entity is a part.|string| +|**dcae_target_name**
*required*|The name of the entity that's the target for monitoring by a DCAE service. This uniquely identifies the monitoring target. For 'undeploy' operations, this value will be used to select the specific DCAE service instance to be undeployed.|string| +|**dcae_target_type**
*required*|The type of the entity that's the target for monitoring by a DCAE service. In 1607, this field will have one of eight distinct values, based on which mobility VM is to
be monitored. For 'deploy' operations, this value will be used to select the
service blueprint to deploy.|string| +|**event**
*required*|The original A&AI event object.
The data included here is available for populating deployment-specific values in the
service blueprint.|object| + + + +### DCAEEventResponse +Response body for a POST to /events. + + +|Name|Description|Schema| +|---|---|---| +|**deploymentIds**
*required*|An array of deploymentIds, one for each service being deployed in response to this
event. A deploymentId uniquely identifies an attempt to deploy a service.|< string > array| +|**requestId**
*required*|A unique identifier assigned to the request. Useful for tracing a request through
logs.|string| + + + + + diff --git a/dispatcherAPI.yaml b/dispatcherAPI.yaml new file mode 100644 index 0000000..4350c4a --- /dev/null +++ b/dispatcherAPI.yaml @@ -0,0 +1,192 @@ + +swagger: '2.0' + +info: + version: "2.0.0" + title: Dispatcher API + description: | + High-level API for deploying/deploying composed services using Cloudify Manager. + +# Paths +paths: + /: + get: + description: | + Get API version information, links to API operations, and location data + + responses: + + 200: + description: Success + schema: + title: DispatcherInfo + type: object + properties: + apiVersion: + type: string + description: | + version of API supported by this server + serverVersion: + type: string + description: | + version of software running on this server + links: + type: object + description: | + Links to API resources + properties: + dcaeServiceInstances: + type: string + description: | + root of DCAE service instance resource tree + status: + type: string + description: | + link to server status information + locations: + type: object + description: | + Information about DCAE locations known to this dispatcher + /events: + post: + description: | + Signal an event that triggers deployment or undeployment of a DCAE service + + consumes: + - application/json + produces: + - application/json + + parameters: + - name: dcae_event + in: body + schema: + $ref: "#/definitions/DCAEEvent" + required: true + + responses: + + 202: + description: | + Success: The content that was posted is valid, the dispatcher has + found the needed blueprint (for a deploy operation) or the existing deployment + (for an undeploy operation), and is initiating the necessary orchestration steps. + schema: + $ref: "#/definitions/DCAEEventResponse" + + 400: + description: | + Bad request: See the message in the response for details. + schema: + $ref: "#/definitions/DCAEErrorResponse" + + 415: + description: | + Bad request: The Content-Type header does not indicate that the content is + 'application/json' + schema: + $ref: "#/definitions/DCAEErrorResponse" + + 500: + description: | + Problem on the server side, possible with downstream systems. See the message + in the response for more details. + schema: + $ref: "#/definitions/DCAEErrorResponse" + +definitions: + + DCAEEvent: + description: | + Data describing an event that should trigger a deploy or undeploy operation for one + or more DCAE services. + + type: object + required: [dcae_target_name, dcae_target_type, dcae_service_action, dcae_service_location, event] + + properties: + + dcae_target_name: + description: | + The name of the entity that's the target for monitoring by a DCAE service. This uniquely identifies the monitoring target. For 'undeploy' operations, this value will be used to select the specific DCAE service instance to be undeployed. + type: string + + dcae_target_type: + description: | + The type of the entity that's the target for monitoring by a DCAE service. In 1607, this field will have one of eight distinct values, based on which mobility VM is to + be monitored. For 'deploy' operations, this value will be used to select the + service blueprint to deploy. + type: string + + dcae_service_action: + description: | + Indicates whether the event requires a DCAE service to be deployed or undeployed. + Valid values are 'deploy' and 'undeploy'. + type: string + + dcae_service_location: + description: | + The location at which the DCAE service is to be deployed or from which it is to be + undeployed. + type: string + + dcae_service_type: + description: | + Identifier for the service of which the target entity is a part. + type: string + + event: + description: | + The original A&AI event object. + The data included here is available for populating deployment-specific values in the + service blueprint. + type: object + + aai_additional_info: + description: | + Additional information, not carried in the event, obtained from an A&AI query or set of queries. Data in this object is available for populating deployment-specific values in the blueprint. + type: object + + DCAEEventResponse: + description: | + Response body for a POST to /events. + + type: object + required: [requestId, deploymentIds] + + properties: + + requestId: + description: | + A unique identifier assigned to the request. Useful for tracing a request through + logs. + type: string + + deploymentIds: + description: | + An array of deploymentIds, one for each service being deployed in response to this + event. A deploymentId uniquely identifies an attempt to deploy a service. + type: array + items: + type: string + + DCAEErrorResponse: + description: | + Object reporting an error. + type: + object + required: [status] + + properties: + status: + description: HTTP status code for the response + type: integer + + message: + description: Human-readable description of the reason for the error + type: string + + + + + diff --git a/etc/config.json.development b/etc/config.json.development new file mode 100644 index 0000000..95e8c93 --- /dev/null +++ b/etc/config.json.development @@ -0,0 +1,11 @@ +{ + "listenPort" : 8444, + "cloudify": { + "url": "https://cm.example.com/api/v2", + "user": "admin", + "password": "admin" + }, + "inventory" : { + "url" : "http://inventory.example.com:8080" + } +} diff --git a/etc/log4js.json b/etc/log4js.json new file mode 100644 index 0000000..28d2970 --- /dev/null +++ b/etc/log4js.json @@ -0,0 +1,28 @@ +{ + "appenders": + [ + { + "type": "dateFile", + "filename": "log/access-dispatcher.log", + "pattern": "-yyyy-MM-dd", + "alwaysIncludePattern": false, + "category": ["access"], + "layout" : { + "type" : "pattern", + "pattern" : "%d{ISO8601} %m" + } + }, + { + "type": "categoryFilter", + "exclude": ["access"], + "appender": { + "type": "dateFile", + "filename": "log/dispatcher.log", + "pattern":"-yyyy-MM-dd", + "alwaysIncludePattern": false + + } + } + ] +} + diff --git a/lib/auth.js b/lib/auth.js new file mode 100644 index 0000000..54b08e2 --- /dev/null +++ b/lib/auth.js @@ -0,0 +1,65 @@ +/* +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. +*/ + +/* HTTP Basic Authentication */ + +"use strict"; + +/* Extract user name and password from the 'Authorization' header */ +const parseAuthHeader = function(authHeader){ + + let parsedHeader = {}; + + const authItems = authHeader.split(/\s+/); // Split on the white space between Basic and the base64 encoded user:password + + if (authItems[0].toLowerCase() === 'basic') { + if (authItems[1]) { + const authString = (new Buffer(authItems[1], 'base64')).toString(); + const userpass = authString.split(':'); + if (userpass.length > 1) { + parsedHeader = {user: userpass[0], password: userpass[1]}; + } + } + } + return parsedHeader; +}; + +/* Middleware function to check authentication */ +exports.checkAuth = function(req, res, next) { + const auth = process.mainModule.exports.config.auth; + if (auth) { + /* Authentication is configured */ + if (req.headers.authorization) { + const creds = parseAuthHeader(req.headers.authorization); + if (creds.user && creds.password && (creds.user in auth) && (auth[creds.user] === creds.password)) { + next(); + } + else { + let err = new Error('Authentication required'); + err.status = 403; + next(err); + } + } + else { + let err = new Error ('Authentication required'); + err.status = 403; + next(err); + } + } + else { + next(); // Nothing to do, no authentication required + } +}; \ No newline at end of file diff --git a/lib/cloudify.js b/lib/cloudify.js new file mode 100644 index 0000000..b1565c4 --- /dev/null +++ b/lib/cloudify.js @@ -0,0 +1,257 @@ +/* +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. +*/ + +/* Low-level routines for using the Cloudify Manager REST API */ + +"use strict"; + +const stream = require('stream'); +const targz = require('node-tar.gz'); + +const utils = require('./utils'); +const repeat = require('./repeat'); +const req = require('./promise_request'); +const doRequest = req.doRequest; + +var cfyAPI = null; +var cfyAuth = null; +var logger = null; + + +// Delay function--returns a promise that's resolved after 'dtime' +// milliseconds.` +var delay = function(dtime) { + return new Promise(function(resolve, reject) { + setTimeout(resolve, dtime); + }); +}; + +// 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); + }; + + // Function for testing if workflow is finished + // Expects the result of getExecutionStatus + 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 { + return res; + } + }); +}; + +// 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 = { + 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; + }); + + }); +}; + +// Creates a deployment from a blueprint +exports.createDeployment = function(dpid, bpid, inputs) { + + // Set up the HTTP PUT request + var reqOptions = { + method : "PUT", + uri : cfyAPI + "/deployments/" + dpid, + headers : { + "Content-Type" : "application/json", + "Accept" : "*/*" + } + }; + + if (cfyAuth) { + reqOptions.auth = cfyAuth; + } + var body = { + blueprint_id : bpid + }; + if (inputs) { + body.inputs = inputs; + } + + // Make the PUT request to create the deployment + return doRequest(reqOptions, JSON.stringify(body)); +}; + +// Executes a workflow against a deployment (use for install and uninstall) +exports.executeWorkflow = 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 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? + } + }); +}; + +// Retrieves outputs for a deployment +exports.getOutputs = function(dpid) { + var reqOptions = { + method : "GET", + uri : cfyAPI + "/deployments/" + dpid + "/outputs", + headers : { + "Accept" : "*/*" + } + }; + if (cfyAuth) { + reqOptions.auth = cfyAuth; + } + + return doRequest(reqOptions); +}; + +// Get the output descriptions for a deployment +exports.getOutputDescriptions = function(dpid) { + var reqOptions = { + method : "GET", + uri : cfyAPI + "/deployments/" + dpid + "?include=outputs", + headers : { + "Accept" : "*/*" + } + }; + if (cfyAuth) { + reqOptions.auth = cfyAuth; + } + + return doRequest(reqOptions); +}; + +// Deletes a deployment +exports.deleteDeployment = function(dpid) { + var reqOptions = { + method : "DELETE", + uri : cfyAPI + "/deployments/" + dpid + }; + if (cfyAuth) { + reqOptions.auth = cfyAuth; + } + + return doRequest(reqOptions); +}; + +// Deletes a blueprint +exports.deleteBlueprint = function(bpid) { + var reqOptions = { + method : "DELETE", + uri : cfyAPI + "/blueprints/" + bpid + }; + if (cfyAuth) { + reqOptions.auth = cfyAuth; + } + + return doRequest(reqOptions); +}; + +// Allow client to set the Cloudify API root address +exports.setAPIAddress = function(addr) { + cfyAPI = addr; +}; + +// Allow client to set Cloudify credentials +exports.setCredentials = function(user, password) { + cfyAuth = { + user : user, + password : password + }; +}; + +// Set a logger +exports.setLogger = function(logsource) { + logger = logsource.getLogger('cfyinterface'); +}; diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 0000000..f74faa9 --- /dev/null +++ b/lib/config.js @@ -0,0 +1,140 @@ +/* +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. +*/ + +/* + * Dispatcher configuration + * Configuration may come from environment variables, configuration file, or defaults, + * in that order of precedence. + * The configuration file is optional. + * If present, the configuration file is a UTF-8 serialization of a JSON object. + * + * -------------------------------------------------------------------------------------- + * | JSON property | Environment variable | Required? | Default | + * -------------------------------------------------------------------------------------- + * | foreground | FOREGROUND | Yes | true | + * -------------------------------------------------------------------------------------- + * | logLevel | LOG_LEVEL | Yes | "INFO" | + * -------------------------------------------------------------------------------------- + * | listenHost | LISTEN_HOST | Yes | "0.0.0.0" | + * -------------------------------------------------------------------------------------- + * | listenPort | LISTEN_PORT | Yes | 8443 | + * -------------------------------------------------------------------------------------- + * | ssl.pfxFile | SSL_PFX_FILE | No | none | + * -------------------------------------------------------------------------------------- + * | ssl.pfxPassFile | SSL_PFX_PASSFILE | No | none | + * -------------------------------------------------------------------------------------- + * | cloudify.url | CLOUDIFY_URL | Yes | none | + * -------------------------------------------------------------------------------------- + * | cloudify.user | CLOUDIFY_USER | No | none | + * -------------------------------------------------------------------------------------- + * | cloudify.password | CLOUDIFY_PASSWORD | No | none | + * -------------------------------------------------------------------------------------- + * | inventory.url | INVENTORY_URL | Yes | http://inventory:8080 | + * -------------------------------------------------------------------------------------- + * | inventory.user | INVENTORY_USER | No | none | + * -------------------------------------------------------------------------------------- + * | inventory.password | INVENTORY_PASSWORD | No | none | + * -------------------------------------------------------------------------------------- + * + */ +"use strict"; + +const fs = require("fs"); +const utils = require("./utils"); + +const DEFAULT_FOREGROUND = true; +const DEFAULT_LISTEN_PORT = 8443; +const DEFAULT_LISTEN_HOST = "0.0.0.0"; +const DEFAULT_LOG_LEVEL = "INFO"; +const DEFAULT_INVENTORY_URL = "http://inventory:8080"; + +/* Check configuration for completeness */ +const findMissingConfig = function(cfg) { + const requiredProps = ['logLevel', 'listenHost', 'listenPort', 'cloudify.url', 'inventory.url']; + return requiredProps.filter(function(p){return !utils.hasProperty(cfg,p);}); +}; + +/** Reads configuration-related files and returns a configuration object + * Sets some defaults + * Throws I/O errors, JSON parsing errors, and an error if required config elements are missing + */ +exports.configure = function(configFile) { + var cfg = {}; + + /* If there's a config file, read it */ + if (configFile) { + cfg = JSON.parse(fs.readFileSync(configFile, 'utf8').trim()); + } + + /* Set config values */ + cfg.foreground = process.env['FOREGROUND'] || cfg.foreground || DEFAULT_FOREGROUND; + cfg.logLevel = process.env['LOG_LEVEL'] || cfg.logLevel || DEFAULT_LOG_LEVEL; + cfg.listenHost = process.env['LISTEN_HOST'] || cfg.listenHost || DEFAULT_LISTEN_HOST; + cfg.listenPort = (process.env['LISTEN_PORT'] && parseInt(process.env['LISTEN_PORT'])) || cfg.listenPort || DEFAULT_LISTEN_PORT; + + if (process.env['SSL_PFX_FILE']) { + if (!cfg.ssl) { + cfg.ssl = {}; + } + cfg.ssl.pfxFile = process.env['SSL_PFX_FILE']; + cfg.ssl.pfxPassFile = process.env['SSL_PFX_PASS_FILE'] || cfg.ssl.pfxPassFile; + } + + 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; + + if (!cfg.inventory) { + cfg.inventory = {}; + } + cfg.inventory.url = process.env['INVENTORY_URL'] || cfg.inventory.url || 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.pfx = fs.readFileSync(cfg.ssl.pfxFile); + if (cfg.ssl.pfxPassFile) { + cfg.ssl.passphrase = fs.readFileSync(cfg.ssl.pfxPassFile,'utf8').trim(); + } + } + + const missing = findMissingConfig(cfg); + if (missing.length > 0) { + throw new Error ("Required configuration elements missing: " + missing.join(',')); + cfg = null; + } + + return cfg; +}; + +/** Read locations file +*/ +exports.getLocations = function(locationsFile) { + let locations = {}; + + try { + locations = JSON.parse(fs.readFileSync(locationsFile)); + } + catch (e) { + locations = {}; + } + return locations; +} \ No newline at end of file diff --git a/lib/deploy.js b/lib/deploy.js new file mode 100644 index 0000000..e807060 --- /dev/null +++ b/lib/deploy.js @@ -0,0 +1,195 @@ +/* +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"; + +/* Deploy and undeploy using Cloudify blueprints */ + +const config = process.mainModule.exports.config; + +/* Set delays between steps */ +const DELAY_INSTALL_WORKFLOW = 30000; +const DELAY_RETRIEVE_OUTPUTS = 5000; +const DELAY_DELETE_DEPLOYMENT = 30000; +const DELAY_DELETE_BLUEPRINT = 10000; + +/* Set up the Cloudify low-level interface library */ +var cfy = require("./cloudify.js"); +/* Set config for deploy module */ +cfy.setAPIAddress(config.cloudify.url); +cfy.setCredentials(config.cloudify.user, config.cloudify.password); +cfy.setLogger(config.logSource); + +/* Set up logging */ +var logger = config.logSource.getLogger("deploy"); + +// Try to parse a string as JSON +var parseContent = function(input) { + var res = {json: false, content: input}; + try { + var parsed = JSON.parse(input); + res.json = true; + res.content = parsed; + } + catch (pe) { + // Do nothing, just indicate it's not JSON and return content as is + } + return res; +}; + +// create a normalized representation of errors, whether they're a node.js Error or a Cloudify API error +var normalizeError = function (err) { + var e = {}; + if (err instanceof Error) { + e.message = err.message; + if (err.code) { + e.code = err.code; + } + if (err.status) { + e.status = err.status; + } + } + 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; + } + 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"; + } + else { + // if there's a body and we can't parse it just attach it as the message + e.message = err.body; + } + } + } + + return e; +}; + +// Augment the raw outputs from a deployment with the descriptions from the blueprint +var annotateOutputs = function (id, rawOutputs) { + return new Promise(function(resolve, reject) { + + var outItems = Object.keys(rawOutputs); + + if (outItems.length < 1) { + // No output items, so obviously no descriptions, just return empty object + resolve({}); + } + else { + // Call Cloudify to get the descriptions + cfy.getOutputDescriptions(id) + .then(function(res) { + // Assemble an outputs object with values from raw output and descriptions just obtained + var p = parseContent(res.body); + if (p.json && p.content.outputs) { + var outs = {}; + outItems.forEach(function(i) { + outs[i] = {value: rawOutputs[i]}; + if (p.content.outputs[i] && p.content.outputs[i].description) { + outs[i].description = p.content.outputs[i].description; + } + }); + resolve(outs); + } + else { + reject({code: "API_INVALID_RESPONSE", message: "Invalid response for output descriptions query"}); + } + }); + } + + }); +}; + +// Delay function--returns a promise that's resolved after 'dtime' milliseconds.` +var delay = function(dtime) { + return new Promise(function(resolve, reject){ + setTimeout(resolve, dtime); + }); +}; + +// Go through the Cloudify API call sequence to do a deployment +exports.deployBlueprint = function(id, blueprint, inputs) { + + logger.debug("deploymentId: " + id + " starting blueprint upload"); + // Upload blueprint + return cfy.uploadBlueprint(id, blueprint) + .then (function(result) { + logger.debug("deploymentId: " + id + " blueprint uploaded"); + // Create deployment + return cfy.createDeployment(id, id, inputs); + }) + .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');}); + }) + .then(function(result) { + logger.debug("deploymentId: " + id + " 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); }); + }) + .then(function(result) { + // We have the raw outputs from the deployment but not annotated with the descriptions + var rawOutputs = {}; + if (result.body) { + var p = parseContent(result.body); + if (p.json) { + if (p.content.outputs) { + rawOutputs = p.content.outputs; + } + } + } + logger.debug("output retrieval result for " + id + ": " + JSON.stringify(result)); + logger.info("deploymentId " + id + " successfully deployed"); + return annotateOutputs(id, rawOutputs); + }) + .catch(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 uninstall workflow + return cfy.executeWorkflow(id, 'uninstall', 0) + .then (function(result){ + logger.debug("deploymentId: " + id + " uninstall workflow completed"); + // Delete the deployment + return delay(DELAY_DELETE_DEPLOYMENT).then(function() {return cfy.deleteDeployment(id);}); + }) + .then (function(result){ + logger.debug("deploymentId: " + id + " deployment deleted"); + // Delete the blueprint + return delay(DELAY_DELETE_BLUEPRINT).then(function() {return cfy.deleteBlueprint(id);}); + }) + .then (function(result){ + logger.info("deploymentId: " + id + " successfully undeployed"); + return result; + }) + .catch (function(err){ + throw normalizeError(err); + }); +}; diff --git a/lib/events.js b/lib/events.js new file mode 100644 index 0000000..11a3ec0 --- /dev/null +++ b/lib/events.js @@ -0,0 +1,79 @@ +/* +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 /events API */ + +"use strict"; + +const router = require('express').Router(); +const bodyParser = require('body-parser'); +const deploy = require('./deploy'); +const middleware = require('./middleware'); +const inventory = require('./inventory'); +const logAccess = require('./logging').logAccess; +const services = require('./services'); + +/* Required properties for event POST */ +const requiredProps = ['dcae_target_name','dcae_target_type','dcae_service_action','dcae_service_location']; + +/* Pick up config exported by main */ +const config = process.mainModule.exports.config; +const logger = config.logSource.getLogger('events'); + +/* Set up middleware stack for initial processing of request */ +router.use(middleware.checkType('application/json')); // Validate type +router.use(bodyParser.json({strict: true})); // Parse body as JSON +router.use(middleware.checkProps(requiredProps)); // Make sure we have required properties +router.use(inventory.checkInventory); // Get template(s) (deploy) or services (undeploy) +router.use(middleware.checkLocation); // Check location and get location information +router.use(middleware.expandTemplates); // Expand any blueprint templates + + +/* Accept an incoming event */ +router.post('/', function(req, res, next) { + let response = {requestId: req.dcaeReqId, deploymentIds:[]}; + + if (req.body.dcae_service_action === 'deploy') { + + /* Deploy services for the VNF */ + + /* req.dcae_blueprints has been populated by the expandTemplates middleware */ + logger.info(req.dcaeReqId + " services to deploy: " + JSON.stringify(req.dcae_blueprints)); + logger.debug(JSON.stringify(req.dcae_shareables, null, '\t')); + 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); + response.deploymentIds = req.dcae_blueprints.map(function(s) {return s.deploymentId;}); + } + else { + + /* Undeploy services for the VNF */ + + /* req.dcae_services has been populated by the checkInventory middleware */ + 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); + req.dcae_services.forEach(undeployer); + response.deploymentIds = req.dcae_services.map(function(s) {return s.deploymentId;}); + } + res.status(202).json(response); + logAccess(req, 202); +}); + +module.exports = router; \ No newline at end of file diff --git a/lib/info.js b/lib/info.js new file mode 100644 index 0000000..0549de1 --- /dev/null +++ b/lib/info.js @@ -0,0 +1,39 @@ +/* +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 / API that provides API information */ + +"use strict"; + +const router = require('express').Router(); + +/* Pick up config exported by main */ +const config = process.mainModule.exports.config; + +/* Accept an incoming event */ +router.get('/', function(req, res) { + res.json( + { + apiVersion: config.apiVersion, + serverVersion: config.version, + links: config.apiLinks, + locations: config.locations + } + ); + require('./logging').logAccess(req, 200); +}); + +module.exports = router; \ No newline at end of file diff --git a/lib/inventory.js b/lib/inventory.js new file mode 100644 index 0000000..5124a98 --- /dev/null +++ b/lib/inventory.js @@ -0,0 +1,229 @@ +/* +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. +*/ + + /* Routines related to accessing DCAE inventory */ + + "use strict"; + + const req = require('./promise_request'); + + const INV_SERV_TYPES = '/dcae-service-types'; + const INV_SERVICES = '/dcae-services'; + + const config = process.mainModule.exports.config; + + /* + * Common error handling for inventory API calls + */ + const invError = function(err) { + if (err.status && err.status === 404) { + /* Map 404 to an empty list */ + return []; + } + else { + var newErr = new Error("Error response " + err.status + " from DCAE inventory: " + err.body); + newErr.status = 502; + newErr.code = 502; + throw newErr; + } + + }; + + /* + * Query the inventory for all of the blueprint templates that need to be + * deployed for a given target type. Returns a promise for an array of + * objects, each object having: - type: the service type name associated + * with the blueprint template - template: the blueprint template + */ + const findTemplates = function(targetType, location, serviceId) { + + /* Set up query string based on available parameters */ + var qs = {vnfType: targetType, serviceLocation: location }; + if (serviceId) { + qs.serviceId = serviceId; + } + + 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); + }; + + /* + * Query the inventory for all of the services running for a given target + * name. Returns a promise for an array of objects, each object having: - + * type: the service type name associated with the service - deploymentID: + * the deploymentID for the service + */ + const findServices = function(target_name) { + const reqOptions = { + method : "GET", + uri : config.inventory.url + INV_SERVICES, + qs: {vnfId: target_name} + }; + return req.doRequest(reqOptions) + .then(function(result) { + let services = []; + let 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};}); + } + return services; + }) + .catch(invError); + }; + + /* + * Find shareable components at 'location'. Return an object whose keys are + * component type names and whose values are component identifiers for + * components of the type. NOTE: if there are multiple shareable components + * with the same component type, the last one wins. + */ + const findShareables = function(location) { + const reqOptions = { + method : "GET", + uri : config.inventory.url + INV_SERVICES, + qs: {vnfLocation: location} + }; + return req.doRequest(reqOptions) + .then(function(result) { + let shareables = {}; + let content = JSON.parse(result.body); + if (content.items) { + content.items.forEach(function(s) { + s.components.filter(function(c) {return c.shareable === 1;}) + .forEach(function(c){ + shareables[c.componentType] = c.componentId; + }); + }); + } + return shareables; + }) + .catch(invError); + }; + + + /* + * 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. + */ + 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) + .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); + } + }) + .then(function(result) { + return findShareables(req.body.dcae_service_location); + }) + .then(function(shareables) { + req.dcae_shareables = shareables; + next(); + }) + .catch(function(err) { + next(err); + }); + } + else if (req.body.dcae_service_action.toLowerCase() === 'undeploy') { + findServices(req.body.dcae_target_name) + .then(function (services){ + if (services.length > 0) { + req.dcae_services = services; + next(); + } + else { + let err = new Error('"' + req.body.dcae_target_name + '" has no deployed DCAE services'); + err.status = 400; + next(err); + } + }) + .catch(function(err) { + next(err); + }); + } + 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); + } + }; + +/* + * Add a DCAE service to the inventory. Done after a deployment. + */ + exports.addService = function(deploymentId, serviceType, vnfId, vnfType, vnfLocation, outputs) { + + /* Create the service description */ + let serviceDescription = + { + "typeName" : serviceType, + "vnfId" : vnfId, + "vnfType" : vnfType, + "vnfLocation" : vnfLocation, + "deploymentRef" : deploymentId + }; + + // TODO create 'components' array using 'outputs'--for now, a dummy + serviceDescription.components = [ + { + componentType: "dummy_component", + componentId: "/components/dummy", + componentSource: "DCAEController", + shareable: 0 + } + ]; + + const reqOptions = { + method : "PUT", + uri : config.inventory.url + INV_SERVICES + "/" + deploymentId, + json: serviceDescription + }; + + return req.doRequest(reqOptions); + }; + +/* + * Remove a DCAE service from the inventory. Done after an undeployment. + */ + exports.deleteService = function(serviceId) { + return req.doRequest({method: "DELETE", uri: config.inventory.url + INV_SERVICES + "/" + serviceId}); + }; + \ No newline at end of file diff --git a/lib/logging.js b/lib/logging.js new file mode 100644 index 0000000..db168b3 --- /dev/null +++ b/lib/logging.js @@ -0,0 +1,31 @@ +/* +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"; +const config = process.mainModule.exports.config; +const accessLogger = config.logSource.getLogger('access'); + + +/* Logging */ + +exports.logAccess = function (req, status, extra) { + let entry = req.dcaeReqId + " " + req.connection.remoteAddress + " " + req.method + " " + req.originalUrl + " " + status; + if (extra) { + extra = extra.replace(/\n/g, " "); /* Collapse multi-line extra data to a single line */ + entry = entry + " <" + extra + ">"; + } + accessLogger.info(entry); +}; \ No newline at end of file diff --git a/lib/middleware.js b/lib/middleware.js new file mode 100644 index 0000000..567620f --- /dev/null +++ b/lib/middleware.js @@ -0,0 +1,126 @@ +/* +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. +*/ + +/* Middleware modules */ + +"use strict"; + +const ejs = require('ejs'); +const utils = require('./utils'); +const logAccess = require('./logging').logAccess; +const config = process.mainModule.exports.config; +const locations = config.locations; +const logger = config.logSource.getLogger("errhandler"); + +/* Assign a request ID to each incoming request */ +exports.assignId = function(req, res, next) { + req.dcaeReqId = utils.generateId(); + 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' + 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})); + } +}; + +/* Make sure Content-Type is correct for POST and PUT */ +exports.checkType = function(type){ + return function(req, res, next) { + const ctype = req.header('content-type'); + const method = req.method.toLowerCase(); + /* Content-Type matters only for POST and PUT */ + if (ctype === type || ['post','put'].indexOf(method) < 0) { + next(); + } + else { + let err = new Error ('Content-Type must be \'' + type +'\''); + err.status = 415; + next (err); + } + }; +}; + +/* Check that a JSON body has a set of properties */ +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(',')); + err.status = 400; + next(err); + } + else { + next(); + } + }; +}; + +/* Check that there is location information for this event */ +/* Appends locations to req.dcae_locations for later use */ +exports.checkLocation = function(req, res, next) { + if (req.body.dcae_service_location in locations) { + req.dcae_locations = {central: locations.central, local: locations[req.body.dcae_service_location]}; + next(); + } + else { + let err = new Error ('"' + req.body.dcae_service_location + '" is not a supported location'); + err.status = 400; + next(err); + } + +}; + +/* Expand blueprint templates into full blueprints. + * Expects req.dcae_templates to contain templates. + * Puts expanded blueprints into req.dcae_blueprints + */ +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 + 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) { + //TODO possibly compute intensive- is there a better way? + return { + blueprint: ejs.render(template.template, context), + type: template.type, + deploymentId: utils.generateId() // Assign ID now, so we can return it in response + }; + }); + req.dcae_blueprints = blueprints; + req.dcae_templates = null; // Make they'll get garbage-collected + } + next(); + } + catch (err) { + err.status = 400; + next(err); + } +}; + + diff --git a/lib/promise_request.js b/lib/promise_request.js new file mode 100644 index 0000000..906c16c --- /dev/null +++ b/lib/promise_request.js @@ -0,0 +1,102 @@ +/* +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. +*/ + + /* Promise-based HTTP request client */ + + "use strict"; + +/* + * Make an HTTP request using a string or a readable stream for the body + * of the request. + * Return a promise for result in the form + * {status: , body: } + */ + +const stream = require('stream'); +const request = require('request'); + +let logger = null; + +exports.doRequest = function(options, body) { + return new Promise(function(resolve, reject) { + + 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; + } + var req = request(options); + + // Reject promise if there's an error + req.on('error', function(error) { + logger.debug('Error in on error handler for request: ' + error); + reject(error); + }); + + // Capture the response + req.on('response', function(resp) { + + // Collect the body of the response + var rbody = ''; + resp.on('data', function(d) { + rbody += d; + }); + + // resolve or reject when finished + resp.on('end', function() { + + var result = { + status : resp.statusCode, + body : rbody + }; + + // Add a JSON version of the body if appropriate + if (rbody.length > 0) { + try { + var jbody = JSON.parse(rbody); + result.json = jbody; + } + catch (pe) { + // Do nothing, no json property added to the result object + } + } + + logger.debug(JSON.stringify(result)); + + if (resp.statusCode > 199 && resp.statusCode < 300) { + // HTTP status code indicates success - resolve the promise + logger.debug("do request resolve: " + JSON.stringify(result)); + resolve(result); + } + else { + // Reject the promise + logger.debug("do request reject: " + JSON.stringify(result)); + reject(result); + } + }); + }); + + // 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); + } + }); +}; + +exports.setLogger = function(logsource) { + logger = logsource.getLogger('httprequest'); +}; diff --git a/lib/repeat.js b/lib/repeat.js new file mode 100644 index 0000000..2ea0e14 --- /dev/null +++ b/lib/repeat.js @@ -0,0 +1,54 @@ +/* +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"; + +/** + * Returns a promise for running and re-running the specified action until the result meets a specific condition + * - action is a function that returns a promise + * - predicate is a function that takes a success result from action and returns true if the action should be rerun + * - maxTries is the total number of times to try the action + * - interval is the interval, in milliseconds, between tries, as approximated by setTimeout() + */ + +exports.repeatWhile = function(action, predicate, maxTries, interval) { + return new Promise(function(resolve, reject) { + + var count = 0; + + function makeAttempt() { + action() + .then (function(res) { + if (!predicate(res)) { + // We're done + resolve(res); + } + else { + if (++count < maxTries) { + // set up next attempt + setTimeout(makeAttempt, interval); + } + else { + // we've run out of retries or it's not retryable, so reject the promise + reject({message: "maximum repetions reached: " + count }); + } + } + }); + } + + makeAttempt(); + }); +}; diff --git a/lib/services.js b/lib/services.js new file mode 100644 index 0000000..5a2a7d7 --- /dev/null +++ b/lib/services.js @@ -0,0 +1,97 @@ +/* +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. +*/ + +/* Deploying and undeploying services based on incoming events */ + +"use strict"; + +const ejs = require('ejs'); +const deploy = require('./deploy'); +const inventory = require('./inventory'); +const config = process.mainModule.exports.config; + +/* Set up logging */ +var logger = config.logSource.getLogger("services"); + +/* Create a deployer function that can deploy a service from a + * blueprint template in the context of the event request 'req'. + * 'template' is a template object (with 'type' and 'template') + * created by the checkInventory middleware + */ +exports.createDeployer = function(req) { + + return function(blueprint) { + /* Generate a deploymentId */ + let deploymentId = blueprint.deploymentId; + + /* Attempt the deployment */ + logger.info(req.dcaeReqId + " " + "Attempting to deploy deploymentId " + deploymentId); + logger.debug(req.dcaeReqId + " deploymentId: " + deploymentId + " blueprint: " + blueprint.blueprint); + + deploy.deployBlueprint(deploymentId, blueprint.blueprint) + .then(function(outputs) { + logger.info(req.dcaeReqId + " Deployed deploymentId: " + deploymentId); + logger.debug (req.dcaeReqId + " deploymentId: " + deploymentId + " outputs: " + JSON.stringify(outputs)); + return outputs; + }) + .then(function(outputs) { + /* Update the inventory */ + return inventory.addService( + deploymentId, + blueprint.type, + req.body.dcae_target_name, + req.body.dcae_target_type, + req.body.dcae_service_location, + outputs + ); + }) + .then(function(result) { + logger.info(req.dcaeReqId + " Updated inventory for deploymentId: " + deploymentId); + }) + .catch(function(err) { + logger.error(req.dcaeReqId + " Failed to deploy deploymentId: " + deploymentId + " Error: " + JSON.stringify(err)); + //TODO try uninstall? + }); + }; +}; + +/* Create an undeployer function that can undeploy + * a previously deployed service. + */ +exports.createUndeployer = function(req) { + + return function(deployment) { + + /* Undeploy */ + deploy.undeployDeployment(deployment.deploymentId) + .then(function(result){ + logger.info(req.dcaeReqId + " Undeployed deploymentId: " + deployment.deploymentId); + return result; + }) + .then(function(result) { + /* Delete the service from the inventory */ + /* When we create service we set service id = deployment id */ + return inventory.deleteService(deployment.deploymentId); + }) + .then(function(result){ + logger.info(req.dcaeReqId + " Deleted service from inventory for deploymentId: " + deployment.deploymentId); + }) + .catch(function(err){ + logger.error(req.dcaeReqId + " Error undeploying " + deployment.deploymentId + ": " + JSON.stringify(err)); + }); + }; + +}; diff --git a/lib/setup.js b/lib/setup.js new file mode 100644 index 0000000..72674b0 --- /dev/null +++ b/lib/setup.js @@ -0,0 +1,37 @@ +/* +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"; + +var fs = require("fs"); + +/** Reads configuration file and parses into an object + * + */ +var cfg = null; + +exports.setConfig = function(configFile) { + + var cfg = null; + try { + cfg = JSON.parse(fs.readFileSync(configFile)); + } + catch (e) { + console.log ("Configuration error: " + e); + } + + return cfg; +}; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..bf582e8 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,79 @@ +/* +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"; + +// 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(); + } + + }); + } + }); + + }); +}; + +// 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(); + } + }); + }); +}; + +/* Does object 'o' have property 'key' */ +exports.hasProperty = function(o, key) { + return key.split('.').every(function(e){ + if (typeof(o) === 'object' && o !== null && (e in o) && (typeof o[e] !== 'undefined')) { + o = o[e]; + return true; + } + else { + return false; + } + }); +}; + +/* Generate a random ID string */ +exports.generateId = function() { + return uuid.v4(); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..4270f13 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "DCAE-Orch-Dispatcher", + "version": "2.0.0", + "description": "DCAE Orchestrator Dispatcher", + "main": "dispatcher.js", + "dependencies": { + "body-parser": "^1.15.0", + "daemon": "^1.1.0", + "ejs": "^2.4.1", + "express": "^4.13.4", + "log4js": "^0.6.33", + "node-tar.gz": "^1.0.0", + "node-uuid": "^1.4.3", + "request": "^2.61.0", + "rimraf": "^2.4.2" + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "author", + "license": "(Apache-2.0)" +} diff --git a/version.js b/version.js new file mode 100644 index 0000000..f2510a4 --- /dev/null +++ b/version.js @@ -0,0 +1 @@ +exports.version="2.0.0"; -- cgit 1.2.3-korg