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 /lib | |
parent | d53e0cf57fc289259b6c9de5bfad224f23cd2988 (diff) |
Init dcae.orch-dispatcher
Change-Id: I52aa696bd5d1d5ed3bc6e03a3c994dc0b3a71062
Signed-off-by: lj1412 <lji@research.att.com>
Diffstat (limited to 'lib')
-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 |
14 files changed, 1530 insertions, 0 deletions
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(); +}; |