diff options
author | lj1412 <lji@research.att.com> | 2017-02-14 15:10:25 +0000 |
---|---|---|
committer | lj1412 <lji@research.att.com> | 2017-02-14 15:10:27 +0000 |
commit | f2ec39706a7a31017f5d219c44d54d40714d9a27 (patch) | |
tree | 0442ccf8420a388d264cca3bc4965a0c1035af0d | |
parent | d53e0cf57fc289259b6c9de5bfad224f23cd2988 (diff) |
Init dcae.orch-dispatcher
Change-Id: I52aa696bd5d1d5ed3bc6e03a3c994dc0b3a71062
Signed-off-by: lj1412 <lji@research.att.com>
-rw-r--r-- | .gitignore | 8 | ||||
-rw-r--r-- | .gitreview | 4 | ||||
-rw-r--r-- | Dockerfile | 18 | ||||
-rw-r--r-- | LICENSE.txt | 22 | ||||
-rw-r--r-- | README.md | 8 | ||||
-rw-r--r-- | dispatcher.js | 143 | ||||
-rw-r--r-- | dispatcherAPI.md | 130 | ||||
-rw-r--r-- | dispatcherAPI.yaml | 192 | ||||
-rw-r--r-- | etc/config.json.development | 11 | ||||
-rw-r--r-- | etc/log4js.json | 28 | ||||
-rw-r--r-- | lib/auth.js | 65 | ||||
-rw-r--r-- | lib/cloudify.js | 257 | ||||
-rw-r--r-- | lib/config.js | 140 | ||||
-rw-r--r-- | lib/deploy.js | 195 | ||||
-rw-r--r-- | lib/events.js | 79 | ||||
-rw-r--r-- | lib/info.js | 39 | ||||
-rw-r--r-- | lib/inventory.js | 229 | ||||
-rw-r--r-- | lib/logging.js | 31 | ||||
-rw-r--r-- | lib/middleware.js | 126 | ||||
-rw-r--r-- | lib/promise_request.js | 102 | ||||
-rw-r--r-- | lib/repeat.js | 54 | ||||
-rw-r--r-- | lib/services.js | 97 | ||||
-rw-r--r-- | lib/setup.js | 37 | ||||
-rw-r--r-- | lib/utils.js | 79 | ||||
-rw-r--r-- | package.json | 23 | ||||
-rw-r--r-- | version.js | 1 |
26 files changed, 2118 insertions, 0 deletions
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 + + +<a name="overview"></a> +## Overview +High-level API for deploying/deploying composed services using Cloudify Manager. + + +### Version information +*Version* : 2.0.0 + + + + +<a name="paths"></a> +## Paths + +<a name="get"></a> +### GET / + +#### Description +Get API version information, links to API operations, and location data + + +#### Responses + +|HTTP Code|Description|Schema| +|---|---|---| +|**200**|Success|[DispatcherInfo](#dispatcherinfo)| + +<a name="dispatcherinfo"></a> +**DispatcherInfo** + +|Name|Description|Schema| +|---|---|---| +|**apiVersion** <br>*optional*|version of API supported by this server|string| +|**links** <br>*optional*|Links to API resources|[links](#get-links)| +|**locations** <br>*optional*|Information about DCAE locations known to this dispatcher|object| +|**serverVersion** <br>*optional*|version of software running on this server|string| + +<a name="get-links"></a> +**links** + +|Name|Description|Schema| +|---|---|---| +|**dcaeServiceInstances** <br>*optional*|root of DCAE service instance resource tree|string| +|**status** <br>*optional*|link to server status information|string| + + +<a name="events-post"></a> +### POST /events + +#### Description +Signal an event that triggers deployment or undeployment of a DCAE service + + +#### Parameters + +|Type|Name|Description|Schema|Default| +|---|---|---|---|---| +|**Body**|**dcae_event** <br>*required*||[DCAEEvent](#dcaeevent)|| + + +#### Responses + +|HTTP Code|Description|Schema| +|---|---|---| +|**202**|Success: The content that was posted is valid, the dispatcher has<br> found the needed blueprint (for a deploy operation) or the existing deployment<br> (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<br>'application/json'|[DCAEErrorResponse](#dcaeerrorresponse)| +|**500**|Problem on the server side, possible with downstream systems. See the message<br>in the response for more details.|[DCAEErrorResponse](#dcaeerrorresponse)| + + +#### Consumes + +* `application/json` + + +#### Produces + +* `application/json` + + + + +<a name="definitions"></a> +## Definitions + +<a name="dcaeerrorresponse"></a> +### DCAEErrorResponse +Object reporting an error. + + +|Name|Description|Schema| +|---|---|---| +|**message** <br>*optional*|Human-readable description of the reason for the error|string| +|**status** <br>*required*|HTTP status code for the response|integer| + + +<a name="dcaeevent"></a> +### 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** <br>*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** <br>*required*|Indicates whether the event requires a DCAE service to be deployed or undeployed.<br>Valid values are 'deploy' and 'undeploy'.|string| +|**dcae_service_location** <br>*required*|The location at which the DCAE service is to be deployed or from which it is to be<br>undeployed.|string| +|**dcae_service_type** <br>*optional*|Identifier for the service of which the target entity is a part.|string| +|**dcae_target_name** <br>*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** <br>*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<br> be monitored. For 'deploy' operations, this value will be used to select the<br> service blueprint to deploy.|string| +|**event** <br>*required*|The original A&AI event object. <br>The data included here is available for populating deployment-specific values in the<br>service blueprint.|object| + + +<a name="dcaeeventresponse"></a> +### DCAEEventResponse +Response body for a POST to /events. + + +|Name|Description|Schema| +|---|---|---| +|**deploymentIds** <br>*required*|An array of deploymentIds, one for each service being deployed in response to this<br>event. A deploymentId uniquely identifies an attempt to deploy a service.|< string > array| +|**requestId** <br>*required*|A unique identifier assigned to the request. Useful for tracing a request through<br>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: <http status code>, body: <response 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"; |