aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore8
-rw-r--r--.gitreview4
-rw-r--r--Dockerfile18
-rw-r--r--LICENSE.txt22
-rw-r--r--README.md8
-rw-r--r--dispatcher.js143
-rw-r--r--dispatcherAPI.md130
-rw-r--r--dispatcherAPI.yaml192
-rw-r--r--etc/config.json.development11
-rw-r--r--etc/log4js.json28
-rw-r--r--lib/auth.js65
-rw-r--r--lib/cloudify.js257
-rw-r--r--lib/config.js140
-rw-r--r--lib/deploy.js195
-rw-r--r--lib/events.js79
-rw-r--r--lib/info.js39
-rw-r--r--lib/inventory.js229
-rw-r--r--lib/logging.js31
-rw-r--r--lib/middleware.js126
-rw-r--r--lib/promise_request.js102
-rw-r--r--lib/repeat.js54
-rw-r--r--lib/services.js97
-rw-r--r--lib/setup.js37
-rw-r--r--lib/utils.js79
-rw-r--r--package.json23
-rw-r--r--version.js1
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";