Replaced all tabs with spaces in java and pom.xml
[so.git] / adapters / mso-adapter-utils / src / main / java / org / onap / so / cloudify / utils / MsoCloudifyUtils.java
1 /*-
2  * ============LICENSE_START=======================================================
3  * ONAP - SO
4  * ================================================================================
5  * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Copyright (C) 2018 Nokia.
8  * ================================================================================
9  * Modifications Copyright (c) 2019 Samsung
10  * ================================================================================
11  * Licensed under the Apache License, Version 2.0 (the "License");
12  * you may not use this file except in compliance with the License.
13  * You may obtain a copy of the License at
14  * 
15  *      http://www.apache.org/licenses/LICENSE-2.0
16  * 
17  * Unless required by applicable law or agreed to in writing, software
18  * distributed under the License is distributed on an "AS IS" BASIS,
19  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20  * See the License for the specific language governing permissions and
21  * limitations under the License.
22  * ============LICENSE_END=========================================================
23  */
24
25 package org.onap.so.cloudify.utils;
26
27 import com.fasterxml.jackson.core.JsonParseException;
28 import com.fasterxml.jackson.databind.JsonNode;
29 import com.fasterxml.jackson.databind.ObjectMapper;
30 import java.io.ByteArrayInputStream;
31 import java.io.ByteArrayOutputStream;
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.util.HashMap;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Optional;
38 import java.util.zip.ZipEntry;
39 import java.util.zip.ZipOutputStream;
40 import org.onap.so.adapters.vdu.CloudInfo;
41 import org.onap.so.adapters.vdu.PluginAction;
42 import org.onap.so.adapters.vdu.VduArtifact;
43 import org.onap.so.adapters.vdu.VduArtifact.ArtifactType;
44 import org.onap.so.adapters.vdu.VduException;
45 import org.onap.so.adapters.vdu.VduInstance;
46 import org.onap.so.adapters.vdu.VduModelInfo;
47 import org.onap.so.adapters.vdu.VduPlugin;
48 import org.onap.so.adapters.vdu.VduStateType;
49 import org.onap.so.adapters.vdu.VduStatus;
50 import org.onap.so.cloud.CloudConfig;
51 import org.onap.so.cloudify.base.client.CloudifyBaseException;
52 import org.onap.so.cloudify.base.client.CloudifyClientTokenProvider;
53 import org.onap.so.cloudify.base.client.CloudifyConnectException;
54 import org.onap.so.cloudify.base.client.CloudifyRequest;
55 import org.onap.so.cloudify.base.client.CloudifyResponseException;
56 import org.onap.so.cloudify.beans.DeploymentInfo;
57 import org.onap.so.cloudify.beans.DeploymentInfoBuilder;
58 import org.onap.so.cloudify.beans.DeploymentStatus;
59 import org.onap.so.cloudify.exceptions.MsoCloudifyException;
60 import org.onap.so.cloudify.exceptions.MsoCloudifyManagerNotFound;
61 import org.onap.so.cloudify.exceptions.MsoDeploymentAlreadyExists;
62 import org.onap.so.cloudify.v3.client.BlueprintsResource.GetBlueprint;
63 import org.onap.so.cloudify.v3.client.BlueprintsResource.UploadBlueprint;
64 import org.onap.so.cloudify.v3.client.Cloudify;
65 import org.onap.so.cloudify.v3.client.DeploymentsResource.CreateDeployment;
66 import org.onap.so.cloudify.v3.client.DeploymentsResource.DeleteDeployment;
67 import org.onap.so.cloudify.v3.client.DeploymentsResource.GetDeployment;
68 import org.onap.so.cloudify.v3.client.DeploymentsResource.GetDeploymentOutputs;
69 import org.onap.so.cloudify.v3.client.ExecutionsResource.CancelExecution;
70 import org.onap.so.cloudify.v3.client.ExecutionsResource.GetExecution;
71 import org.onap.so.cloudify.v3.client.ExecutionsResource.ListExecutions;
72 import org.onap.so.cloudify.v3.client.ExecutionsResource.StartExecution;
73 import org.onap.so.cloudify.v3.model.AzureConfig;
74 import org.onap.so.cloudify.v3.model.Blueprint;
75 import org.onap.so.cloudify.v3.model.CancelExecutionParams;
76 import org.onap.so.cloudify.v3.model.CloudifyError;
77 import org.onap.so.cloudify.v3.model.CreateDeploymentParams;
78 import org.onap.so.cloudify.v3.model.Deployment;
79 import org.onap.so.cloudify.v3.model.DeploymentOutputs;
80 import org.onap.so.cloudify.v3.model.Execution;
81 import org.onap.so.cloudify.v3.model.Executions;
82 import org.onap.so.cloudify.v3.model.OpenstackConfig;
83 import org.onap.so.cloudify.v3.model.StartExecutionParams;
84 import org.onap.so.config.beans.PoConfig;
85 import org.onap.so.db.catalog.beans.CloudSite;
86 import org.onap.so.db.catalog.beans.CloudifyManager;
87 import org.onap.so.db.catalog.beans.HeatTemplateParam;
88 import org.onap.so.logger.ErrorCode;
89 import org.onap.so.logger.MessageEnum;
90 import org.onap.so.openstack.exceptions.MsoAdapterException;
91 import org.onap.so.openstack.exceptions.MsoCloudSiteNotFound;
92 import org.onap.so.openstack.exceptions.MsoException;
93 import org.onap.so.openstack.exceptions.MsoExceptionCategory;
94 import org.onap.so.openstack.exceptions.MsoIOException;
95 import org.onap.so.openstack.exceptions.MsoOpenstackException;
96 import org.onap.so.openstack.utils.MsoCommonUtils;
97 import org.onap.so.utils.CryptoUtils;
98 import org.slf4j.Logger;
99 import org.slf4j.LoggerFactory;
100 import org.springframework.beans.factory.annotation.Autowired;
101 import org.springframework.core.env.Environment;
102 import org.springframework.stereotype.Component;
103
104 @Component
105 public class MsoCloudifyUtils extends MsoCommonUtils implements VduPlugin {
106
107     private static final String CLOUDIFY = "Cloudify";
108     private static final String CREATE_DEPLOYMENT = "CreateDeployment";
109     private static final String DELETE_DEPLOYMENT = "DeleteDeployment";
110     private static final String TERMINATED = "terminated";
111     private static final String CANCELLED = "cancelled";
112
113     // Fetch cloud configuration each time (may be cached in CloudConfig class)
114     @Autowired
115     protected CloudConfig cloudConfig;
116
117     @Autowired
118     private Environment environment;
119
120     @Autowired
121     private PoConfig poConfig;
122
123     private static final Logger logger = LoggerFactory.getLogger(MsoCloudifyUtils.class);
124
125     // Properties names and variables (with default values)
126     protected String createPollIntervalProp = "org.onap.so.adapters.po.pollInterval";
127     private String deletePollIntervalProp = "org.onap.so.adapters.po.pollInterval";
128
129     protected String createPollIntervalDefault = "15";
130     private String deletePollIntervalDefault = "15";
131
132     private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
133
134     /**
135      * Create a new Deployment from a specified blueprint, and install it in the specified cloud location and tenant.
136      * The blueprint identifier and parameter map are passed in as arguments, along with the cloud access credentials.
137      * The blueprint should have been previously uploaded to Cloudify.
138      *
139      * It is expected that parameters have been validated and contain at minimum the required parameters for the given
140      * template with no extra (undefined) parameters..
141      *
142      * The deployment ID supplied by the caller must be unique in the scope of the Cloudify tenant (not the Openstack
143      * tenant). However, it should also be globally unique, as it will be the identifier for the resource going forward
144      * in Inventory. This latter is managed by the higher levels invoking this function.
145      *
146      * This function executes the "install" workflow on the newly created workflow. Cloudify will be polled for
147      * completion unless the client requests otherwise.
148      *
149      * An error will be thrown if the requested Deployment already exists in the specified Cloudify instance.
150      *
151      * @param cloudSiteId The cloud (may be a region) in which to create the stack.
152      * @param tenantId The Openstack ID of the tenant in which to create the Stack
153      * @param deploymentId The identifier (name) of the deployment to create
154      * @param blueprintId The blueprint from which to create the deployment.
155      * @param inputs A map of key/value inputs
156      * @param pollForCompletion Indicator that polling should be handled in Java vs. in the client
157      * @param timeoutMinutes Timeout after which the "install" will be cancelled
158      * @param backout Flag to delete deployment on install Failure - defaulted to True
159      * @return A DeploymentInfo object
160      * @throws MsoCloudifyException Thrown if the Cloudify API call returns an exception.
161      * @throws MsoIOException Thrown on Cloudify connection errors.
162      */
163
164     public DeploymentInfo createAndInstallDeployment(String cloudSiteId, String tenantId, String deploymentId,
165             String blueprintId, Map<String, ? extends Object> inputs, boolean pollForCompletion, int timeoutMinutes,
166             boolean backout) throws MsoException {
167         // Obtain the cloud site information where we will create the stack
168         Optional<CloudSite> cloudSite = cloudConfig.getCloudSite(cloudSiteId);
169         if (!cloudSite.isPresent()) {
170             throw new MsoCloudSiteNotFound(cloudSiteId);
171         }
172
173         Cloudify cloudify = getCloudifyClient(cloudSite.get());
174
175         logger.debug("Ready to Create Deployment ({}) with input params: {}", deploymentId, inputs);
176
177         // Build up the inputs, including:
178         // - from provided "environment" file
179         // - passed in by caller
180         // - special input for cloud-specific Credentials
181         Map<String, Object> expandedInputs = new HashMap<>(inputs);
182
183         String platform = cloudSite.get().getPlatform();
184         if (platform == null || platform.equals("") || platform.equalsIgnoreCase("OPENSTACK")) {
185             // Create the Cloudify OpenstackConfig with the credentials
186             OpenstackConfig openstackConfig = getOpenstackConfig(cloudSite.get(), tenantId);
187             expandedInputs.put("openstack_config", openstackConfig);
188         } else if (platform.equalsIgnoreCase("AZURE")) {
189             // Create Cloudify AzureConfig with the credentials
190             AzureConfig azureConfig = getAzureConfig(cloudSite.get(), tenantId);
191             expandedInputs.put("azure_config", azureConfig);
192         }
193
194         // Build up the parameters to create a new deployment
195         CreateDeploymentParams deploymentParams = new CreateDeploymentParams();
196         deploymentParams.setBlueprintId(blueprintId);
197         deploymentParams.setInputs(expandedInputs);
198
199         Deployment deployment = null;
200         try {
201             CreateDeployment createDeploymentRequest = cloudify.deployments().create(deploymentId, deploymentParams);
202             logger.debug(createDeploymentRequest.toString());
203
204             deployment = executeAndRecordCloudifyRequest(createDeploymentRequest);
205         } catch (CloudifyResponseException e) {
206             // Since this came on the 'Create Deployment' command, nothing was changed
207             // in the cloud. Return the error as an exception.
208             if (e.getStatus() == 409) {
209                 // Deployment already exists. Return a specific error for this case
210                 MsoException me = new MsoDeploymentAlreadyExists(deploymentId, cloudSiteId);
211                 me.addContext(CREATE_DEPLOYMENT);
212                 throw me;
213             } else {
214                 // Convert the CloudifyResponseException to an MsoException
215                 logger.debug("ERROR STATUS = {},\n{}\n{}", e.getStatus(), e.getMessage(), e.getLocalizedMessage());
216                 MsoException me = cloudifyExceptionToMsoException(e, CREATE_DEPLOYMENT);
217                 me.setCategory(MsoExceptionCategory.OPENSTACK);
218                 throw me;
219             }
220         } catch (CloudifyConnectException e) {
221             // Error connecting to Cloudify instance. Convert to an MsoException
222             throw cloudifyExceptionToMsoException(e, CREATE_DEPLOYMENT);
223         } catch (RuntimeException e) {
224             // Catch-all
225             throw runtimeExceptionToMsoException(e, CREATE_DEPLOYMENT);
226         }
227
228         /*
229          * It can take some time for Cloudify to be ready to execute a workflow on the deployment. Sleep 30 seconds
230          * based on observation of behavior in a Cloudify VM instance (delay due to "create_deployment_environment").
231          */
232         sleep(30000);
233
234         /*
235          * Next execute the "install" workflow. Note - this assumes there are no additional parameters required for the
236          * workflow.
237          */
238         int createPollInterval =
239                 Integer.parseInt(this.environment.getProperty(createPollIntervalProp, createPollIntervalDefault));
240         int pollTimeout = (timeoutMinutes * 60) + createPollInterval;
241
242         Execution installWorkflow = null;
243
244         try {
245             installWorkflow = executeWorkflow(cloudify, deploymentId, "install", null, pollForCompletion, pollTimeout,
246                     createPollInterval);
247
248             if (installWorkflow.getStatus().equals(TERMINATED)) {
249                 // Success!
250                 // Create and return a DeploymentInfo structure. Include the Runtime outputs
251                 return new DeploymentInfoBuilder().withId(deployment.getId())
252                         .withDeploymentInputs(deployment.getInputs())
253                         .withDeploymentOutputs(getDeploymentOutputs(cloudify, deploymentId).get())
254                         .fromExecution(installWorkflow).build();
255             } else {
256                 // The workflow completed with errors. Must try to back it out.
257                 if (!backout) {
258                     logger.warn("{} Deployment installation failed, backout deletion suppressed {} {}",
259                             MessageEnum.RA_CREATE_STACK_ERR, ErrorCode.BusinessProcesssError.getValue(),
260                             "Exception in Deployment Installation, backout suppressed");
261                 } else {
262                     // Poll on delete if we rollback - use same values for now
263                     int deletePollInterval = createPollInterval;
264                     int deletePollTimeout = pollTimeout;
265
266                     try {
267                         // Run the uninstall to undo the install
268                         Execution uninstallWorkflow = executeWorkflow(cloudify, deploymentId, "uninstall", null,
269                                 pollForCompletion, deletePollTimeout, deletePollInterval);
270
271                         if (uninstallWorkflow.getStatus().equals(TERMINATED)) {
272                             // The uninstall completed. Delete the deployment itself
273                             DeleteDeployment deleteRequest = cloudify.deployments().deleteByName(deploymentId);
274                             executeAndRecordCloudifyRequest(deleteRequest);
275                         } else {
276                             // Didn't uninstall successfully. Log this error
277                             logger.error("{} Create Deployment: Cloudify error rolling back deployment install: {} {}",
278                                     MessageEnum.RA_CREATE_STACK_ERR, installWorkflow.getError(),
279                                     ErrorCode.BusinessProcesssError.getValue());
280                         }
281                     } catch (Exception e) {
282                         // Catch-all for backout errors trying to uninstall/delete
283                         // Log this error, and return the original exception
284                         logger.error("{} Create Stack: Nested exception rolling back deployment install: {}",
285                                 MessageEnum.RA_CREATE_STACK_ERR, ErrorCode.BusinessProcesssError.getValue(), e);
286                     }
287                 }
288
289                 MsoCloudifyException me =
290                         new MsoCloudifyException(0, "Workflow Execution Failed", installWorkflow.getError());
291                 me.addContext(CREATE_DEPLOYMENT);
292
293                 throw me;
294             }
295         } catch (MsoException me) {
296             // Install failed. Unless requested otherwise, back out the deployment
297
298             if (!backout) {
299                 logger.warn("{} Deployment installation failed, backout deletion suppressed {}",
300                         MessageEnum.RA_CREATE_STACK_ERR, ErrorCode.BusinessProcesssError.getValue());
301             } else {
302                 // Poll on delete if we rollback - use same values for now
303                 int deletePollInterval = createPollInterval;
304                 int deletePollTimeout = pollTimeout;
305
306                 try {
307                     // Run the uninstall to undo the install.
308                     // Always try to run it, as it should be idempotent
309                     executeWorkflow(cloudify, deploymentId, "uninstall", null, pollForCompletion, deletePollTimeout,
310                             deletePollInterval);
311
312                     // Delete the deployment itself
313                     DeleteDeployment deleteRequest = cloudify.deployments().deleteByName(deploymentId);
314                     executeAndRecordCloudifyRequest(deleteRequest);
315                 } catch (Exception e) {
316                     // Catch-all for backout errors trying to uninstall/delete
317                     // Log this error, and return the original exception
318                     logger.error("{} Create Stack: Nested exception rolling back deployment install: {} ",
319                             MessageEnum.RA_CREATE_STACK_ERR, ErrorCode.BusinessProcesssError.getValue(), e);
320                 }
321             }
322
323             // Propagate the original exception from Stack Query.
324             me.addContext(CREATE_DEPLOYMENT);
325
326             throw me;
327         }
328     }
329
330
331     /*
332      * Get the runtime Outputs of a deployment. Return the Map of tag/value outputs.
333      */
334     private Optional<Map<String, Object>> getDeploymentOutputs(Cloudify cloudify, String deploymentId)
335             throws MsoException {
336         // Build and send the Cloudify request
337         DeploymentOutputs deploymentOutputs;
338         try {
339             GetDeploymentOutputs queryDeploymentOutputs = cloudify.deployments().outputsById(deploymentId);
340             logger.debug(queryDeploymentOutputs.toString());
341
342             deploymentOutputs = executeAndRecordCloudifyRequest(queryDeploymentOutputs);
343             if (deploymentOutputs != null) {
344                 return Optional.ofNullable(deploymentOutputs.getOutputs());
345             } else {
346                 return Optional.empty();
347             }
348         } catch (CloudifyConnectException ce) {
349             // Couldn't connect to Cloudify
350             logger.error("{} QueryDeploymentOutputs: Cloudify connection failure: {} ", MessageEnum.RA_CREATE_STACK_ERR,
351                     ErrorCode.BusinessProcesssError.getValue(), ce);
352             throw new MsoIOException(ce.getMessage(), ce);
353         } catch (CloudifyResponseException re) {
354             if (re.getStatus() == 404) {
355                 // No Outputs
356                 return Optional.empty();
357             }
358             throw new MsoCloudifyException(re.getStatus(), re.getMessage(), re.getLocalizedMessage(), re);
359         } catch (Exception e) {
360             // Catch-all
361             throw new MsoAdapterException(e.getMessage(), e);
362         }
363     }
364
365     /*
366      * Execute a workflow on a deployment. Handle polling for completion with timeout. Return the final Execution object
367      * with status. Throw an exception on Errors. Question - how does the client know whether rollback needs to be done?
368      */
369     private Execution executeWorkflow(Cloudify cloudify, String deploymentId, String workflowId,
370             Map<String, Object> workflowParams, boolean pollForCompletion, int timeout, int pollInterval)
371             throws MsoCloudifyException {
372         logger.debug("Executing '{}' workflow on deployment '{}'", workflowId, deploymentId);
373
374         StartExecutionParams executeParams = new StartExecutionParams();
375         executeParams.setWorkflowId(workflowId);
376         executeParams.setDeploymentId(deploymentId);
377         executeParams.setParameters(workflowParams);
378
379         Execution execution = null;
380         String executionId = null;
381         String command = "start";
382         Exception savedException = null;
383
384         try {
385             StartExecution executionRequest = cloudify.executions().start(executeParams);
386             logger.debug(executionRequest.toString());
387             execution = executeAndRecordCloudifyRequest(executionRequest);
388             executionId = execution.getId();
389
390             if (!pollForCompletion) {
391                 // Client did not request polling, so just return the Execution object
392                 return execution;
393             }
394
395             // Enter polling loop
396             boolean timedOut = false;
397             int pollTimeout = timeout;
398
399             String status = execution.getStatus();
400
401             // Create a reusable cloudify query request
402             GetExecution queryExecution = cloudify.executions().byId(executionId);
403             command = "query";
404
405             while (!timedOut && !(status.equals(TERMINATED) || status.equals("failed") || status.equals(CANCELLED))) {
406                 // workflow is still running; check for timeout
407                 if (pollTimeout <= 0) {
408                     logger.debug("workflow {} timed out on deployment {}", execution.getWorkflowId(),
409                             execution.getDeploymentId());
410                     timedOut = true;
411                     continue;
412                 }
413
414                 sleep(pollInterval * 1000L);
415
416                 pollTimeout -= pollInterval;
417                 logger.debug("pollTimeout remaining: " + pollTimeout);
418
419                 execution = queryExecution.execute();
420                 status = execution.getStatus();
421             }
422
423             // Broke the loop. Check again for a terminal state
424             if (status.equals(TERMINATED)) {
425                 // Success!
426                 logger.debug("Workflow '{}' completed successfully on deployment '{}'", workflowId, deploymentId);
427                 return execution;
428             } else if (status.equals("failed")) {
429                 // Workflow failed. Log it and return the execution object (don't throw exception here)
430                 logger.error("{} Cloudify workflow failure: {} {} Execute Workflow: Failed: {}",
431                         MessageEnum.RA_CREATE_STACK_ERR, execution.getError(),
432                         ErrorCode.BusinessProcesssError.getValue(), execution.getError());
433                 return execution;
434             } else if (status.equals(CANCELLED)) {
435                 // Workflow was cancelled, leaving the deployment in an indeterminate state. Log it and return the
436                 // execution object (don't throw exception here)
437                 logger.error("{} Cloudify workflow cancelled. Deployment is in an indeterminate state {} {} {}",
438                         MessageEnum.RA_CREATE_STACK_ERR, ErrorCode.BusinessProcesssError.getValue(),
439                         "Execute Workflow cancelled: ", workflowId);
440                 return execution;
441             } else {
442                 // Can only get here after a timeout
443                 logger.error("{} Cloudify workflow timeout {} Execute Workflow: Timed Out",
444                         MessageEnum.RA_CREATE_STACK_ERR, ErrorCode.BusinessProcesssError.getValue());
445             }
446         } catch (CloudifyConnectException ce) {
447             logger.error("{} {} Execute Workflow ({} {}): Cloudify connection failure {} ",
448                     MessageEnum.RA_CREATE_STACK_ERR, ErrorCode.BusinessProcesssError.getValue(), command, ce);
449             savedException = ce;
450         } catch (CloudifyResponseException re) {
451             logger.error("{} {} Execute Workflow ({}): Cloudify response error {} ", MessageEnum.RA_CREATE_STACK_ERR,
452                     ErrorCode.BusinessProcesssError.getValue(), command, re.getMessage(), re);
453             savedException = re;
454         } catch (RuntimeException e) {
455             // Catch-all
456             logger.error("{} {} Execute Workflow ({}): Internal error {}", MessageEnum.RA_CREATE_STACK_ERR,
457                     ErrorCode.BusinessProcesssError.getValue(), command, e.getMessage(), e);
458             savedException = e;
459         }
460
461         // Get to this point ONLY on an error or timeout
462         // The cloudify execution is still running (we've not received a terminal status),
463         // so try to Cancel it.
464         CancelExecutionParams cancelParams = new CancelExecutionParams();
465         cancelParams.setAction("cancel");
466         // TODO: Use force_cancel?
467
468         Execution cancelExecution = null;
469
470         try {
471             CancelExecution cancelRequest = cloudify.executions().cancel(executionId, cancelParams);
472             logger.debug(cancelRequest.toString());
473             cancelExecution = cancelRequest.execute();
474
475             // Enter polling loop
476             boolean timedOut = false;
477             int cancelTimeout = timeout; // TODO: For now, just use same timeout
478
479             String status = cancelExecution.getStatus();
480
481             // Poll for completion. Create a reusable cloudify query request
482             GetExecution queryExecution = cloudify.executions().byId(executionId);
483
484             while (!timedOut && !status.equals(CANCELLED)) {
485                 // workflow is still running; check for timeout
486                 if (cancelTimeout <= 0) {
487                     logger.debug("Cancel timeout for workflow {} on deployment {}", workflowId, deploymentId);
488                     timedOut = true;
489                     continue;
490                 }
491
492                 sleep(pollInterval * 1000L);
493
494                 cancelTimeout -= pollInterval;
495                 logger.debug("pollTimeout remaining: {}", cancelTimeout);
496
497                 execution = queryExecution.execute();
498                 status = execution.getStatus();
499             }
500
501             // Broke the loop. Check again for a terminal state
502             if (status.equals(CANCELLED)) {
503                 // Finished cancelling. Return the original exception
504                 logger.debug("Cancel workflow {} completed on deployment {}", workflowId, deploymentId);
505                 throw new MsoCloudifyException(-1, "", "", savedException);
506             } else {
507                 // Can only get here after a timeout
508                 logger.debug("Cancel workflow {} timeout out on deployment {}", workflowId, deploymentId);
509                 MsoCloudifyException exception = new MsoCloudifyException(-1, "", "", savedException);
510                 exception.setPendingWorkflow(true);
511                 throw exception;
512             }
513         } catch (Exception e) {
514             // Catch-all. Log the message and throw the original exception
515             logger.debug("Cancel workflow {} failed for deployment {} :", workflowId, deploymentId, e);
516             MsoCloudifyException exception = new MsoCloudifyException(-1, "", "", savedException);
517             exception.setPendingWorkflow(true);
518             throw exception;
519         }
520     }
521
522
523
524     /**
525      * Query for a Cloudify Deployment (by Name). This call will always return a DeploymentInfo object. If the
526      * deployment does not exist, an "empty" DeploymentInfo will be returned - containing only the deployment ID and a
527      * special status of NOTFOUND.
528      *
529      * @param tenantId The Openstack ID of the tenant in which to query
530      * @param cloudSiteId The cloud identifier (may be a region) in which to query
531      * @return A StackInfo object
532      * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception.
533      */
534     public DeploymentInfo queryDeployment(String cloudSiteId, String tenantId, String deploymentId)
535             throws MsoException {
536         logger.debug("Query Cloudify Deployment: {} in tenant {}", deploymentId, tenantId);
537
538         // Obtain the cloud site information where we will create the stack
539         Optional<CloudSite> cloudSite = cloudConfig.getCloudSite(cloudSiteId);
540         if (!cloudSite.isPresent()) {
541             throw new MsoCloudSiteNotFound(cloudSiteId);
542         }
543
544         Cloudify cloudify = getCloudifyClient(cloudSite.get());
545
546         // Build and send the Cloudify request
547         Deployment deployment = new Deployment();
548         try {
549             GetDeployment queryDeployment = cloudify.deployments().byId(deploymentId);
550             logger.debug(queryDeployment.toString());
551             deployment = executeAndRecordCloudifyRequest(queryDeployment);
552
553             // Next look for the latest execution
554             ListExecutions listExecutions =
555                     cloudify.executions().listFiltered("deployment_id=" + deploymentId, "-created_at");
556             Executions executions = listExecutions.execute();
557
558             // If no executions, does this give NOT_FOUND or empty set?
559             if (executions.getItems().isEmpty()) {
560                 return new DeploymentInfoBuilder().withId(deployment.getId())
561                         .withDeploymentInputs(deployment.getInputs()).build();
562             } else {
563                 return new DeploymentInfoBuilder().withId(deployment.getId())
564                         .withDeploymentInputs(deployment.getInputs())
565                         .withDeploymentOutputs(getDeploymentOutputs(cloudify, deploymentId).get())
566                         .fromExecution(executions.getItems().get(0)).build();
567             }
568         } catch (CloudifyConnectException ce) {
569             // Couldn't connect to Cloudify
570             logger.error("{} QueryDeployment: Cloudify connection failure: {} ", MessageEnum.RA_CREATE_STACK_ERR,
571                     ErrorCode.BusinessProcesssError.getValue(), ce);
572             throw new MsoIOException(ce.getMessage(), ce);
573         } catch (CloudifyResponseException re) {
574             if (re.getStatus() == 404) {
575                 // Got a NOT FOUND error. React differently based on deployment vs. execution
576                 if (deployment != null) {
577                     // Got NOT_FOUND on the executions. Assume this is a valid "empty" set
578                     return new DeploymentInfoBuilder().withId(deployment.getId())
579                             .withDeploymentInputs(deployment.getInputs())
580                             .withDeploymentOutputs(getDeploymentOutputs(cloudify, deploymentId).get()).build();
581                 } else {
582                     // Deployment not found. Default status of a DeploymentInfo object is NOTFOUND
583                     return new DeploymentInfoBuilder().withId(deploymentId).build();
584                 }
585             }
586             throw new MsoCloudifyException(re.getStatus(), re.getMessage(), re.getLocalizedMessage(), re);
587         } catch (Exception e) {
588             // Catch-all
589             throw new MsoAdapterException(e.getMessage(), e);
590         }
591     }
592
593
594     /**
595      * Delete a Cloudify deployment (by ID). If the deployment is not found, it will be considered a successful
596      * deletion. The return value is a DeploymentInfo object which contains the last deployment status.
597      *
598      * There is no rollback from a successful deletion. A deletion failure will also result in an undefined deployment
599      * state - the components may or may not have been all or partially deleted, so the resulting deployment must be
600      * considered invalid.
601      *
602      * @param tenantId The Openstack ID of the tenant in which to perform the delete
603      * @param cloudSiteId The cloud identifier (may be a region) from which to delete the stack.
604      * @return A StackInfo object
605      * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception.
606      * @throws MsoCloudSiteNotFound
607      */
608     public DeploymentInfo uninstallAndDeleteDeployment(String cloudSiteId, String tenantId, String deploymentId,
609             int timeoutMinutes) throws MsoException {
610         // Obtain the cloud site information where we will create the stack
611         Optional<CloudSite> cloudSite = cloudConfig.getCloudSite(cloudSiteId);
612         if (!cloudSite.isPresent()) {
613             throw new MsoCloudSiteNotFound(cloudSiteId);
614         }
615
616         Cloudify cloudify = getCloudifyClient(cloudSite.get());
617
618         logger.debug("Ready to Uninstall/Delete Deployment ({})", deploymentId);
619
620         // Query first to save the trouble if deployment not found
621         try {
622             GetDeployment queryDeploymentRequest = cloudify.deployments().byId(deploymentId);
623             logger.debug(queryDeploymentRequest.toString());
624
625             // deployment = executeAndRecordCloudifyRequest (queryDeploymentRequest);
626         } catch (CloudifyResponseException e) {
627             // Since this came on the 'Create Deployment' command, nothing was changed
628             // in the cloud. Return the error as an exception.
629             if (e.getStatus() == 404) {
630                 // Deployment doesn't exist. Return a "NOTFOUND" DeploymentInfo object
631                 // TODO: Should return NULL?
632                 logger.debug("Deployment requested for deletion does not exist: {}", deploymentId);
633                 return new DeploymentInfoBuilder().withId(deploymentId).withStatus(DeploymentStatus.NOTFOUND).build();
634             } else {
635                 // Convert the CloudifyResponseException to an MsoOpenstackException
636                 logger.debug("ERROR STATUS = {}, \n {}\n {}\n {}", e.getStatus(), e.getMessage(),
637                         e.getLocalizedMessage(), e);
638                 MsoException me = cloudifyExceptionToMsoException(e, DELETE_DEPLOYMENT);
639                 me.setCategory(MsoExceptionCategory.INTERNAL);
640                 throw me;
641             }
642         } catch (CloudifyConnectException e) {
643             // Error connecting to Cloudify instance. Convert to an MsoException
644             throw cloudifyExceptionToMsoException(e, DELETE_DEPLOYMENT);
645         } catch (RuntimeException e) {
646             // Catch-all
647             throw runtimeExceptionToMsoException(e, DELETE_DEPLOYMENT);
648         }
649
650         /*
651          * Query the outputs before deleting so they can be returned as well
652          */
653         // DeploymentOutputs outputs = getDeploymentOutputs (cloudify, deploymentId);
654
655         /*
656          * Next execute the "uninstall" workflow. Note - this assumes there are no additional parameters required for
657          * the workflow.
658          */
659         // TODO: No deletePollInterval that I'm aware of. Use the create interval
660         int deletePollInterval =
661                 Integer.parseInt(this.environment.getProperty(deletePollIntervalProp, deletePollIntervalDefault));
662         int pollTimeout = (timeoutMinutes * 60) + deletePollInterval;
663
664         Execution uninstallWorkflow = null;
665
666         try {
667             uninstallWorkflow =
668                     executeWorkflow(cloudify, deploymentId, "uninstall", null, true, pollTimeout, deletePollInterval);
669
670             if (uninstallWorkflow.getStatus().equals(TERMINATED)) {
671                 // Successful uninstall.
672                 logger.debug("Uninstall successful for deployment {}", deploymentId);
673             } else {
674                 // The uninstall workflow completed with an error. Must fail the request, but will
675                 // leave the deployment in an indeterminate state, as cloud resources may still exist.
676                 MsoCloudifyException me =
677                         new MsoCloudifyException(0, "Uninstall Workflow Failed", uninstallWorkflow.getError());
678                 me.addContext(DELETE_DEPLOYMENT);
679
680                 throw me;
681             }
682         } catch (MsoException me) {
683             // Uninstall workflow has failed.
684             // Must fail the deletion... may leave the deployment in an inconclusive state
685             me.addContext(DELETE_DEPLOYMENT);
686
687             throw me;
688         }
689
690         // At this point, the deployment has been successfully uninstalled.
691         // Next step is to delete the deployment itself
692         Deployment deployment;
693         try {
694             DeleteDeployment deleteRequest = cloudify.deployments().deleteByName(deploymentId);
695             logger.debug(deleteRequest.toString());
696
697             // The delete request returns the deleted deployment
698             deployment = deleteRequest.execute();
699
700         } catch (CloudifyConnectException ce) {
701             // Failed to delete. Must fail the request, but will leave the (uninstalled)
702             // deployment in Cloudify DB.
703             MsoCloudifyException me = new MsoCloudifyException(0, "Deployment Delete Failed", ce.getMessage(), ce);
704             me.addContext(DELETE_DEPLOYMENT);
705
706             throw me;
707         } catch (CloudifyResponseException re) {
708             // Failed to delete. Must fail the request, but will leave the (uninstalled)
709             // deployment in the Cloudify DB.
710             MsoCloudifyException me = new MsoCloudifyException(re.getStatus(), re.getMessage(), re.getMessage(), re);
711             me.addContext(DELETE_DEPLOYMENT);
712
713             throw me;
714         } catch (Exception e) {
715             // Catch-all
716             MsoAdapterException ae = new MsoAdapterException(e.getMessage(), e);
717             ae.addContext(DELETE_DEPLOYMENT);
718
719             throw ae;
720         }
721
722         // Return the deleted deployment info (with runtime outputs) along with the completed uninstall workflow status
723         return new DeploymentInfoBuilder().withId(deployment.getId()).withDeploymentInputs(deployment.getInputs())
724                 .withDeploymentOutputs(getDeploymentOutputs(cloudify, deploymentId).get())
725                 .fromExecution(uninstallWorkflow).build();
726     }
727
728
729     /**
730      * Check if a blueprint is available for use at a targeted cloud site. This requires checking the Cloudify Manager
731      * which is servicing that cloud site to see if the specified blueprint has been loaded.
732      *
733      * @param cloudSiteId The cloud site where the blueprint is needed
734      * @param blueprintId The ID for the blueprint in Cloudify
735      */
736     public boolean isBlueprintLoaded(String cloudSiteId, String blueprintId) throws MsoException {
737         // Obtain the cloud site information where we will load the blueprint
738         Optional<CloudSite> cloudSite = cloudConfig.getCloudSite(cloudSiteId);
739         if (!cloudSite.isPresent()) {
740             throw new MsoCloudSiteNotFound(cloudSiteId);
741         }
742
743         Cloudify cloudify = getCloudifyClient(cloudSite.get());
744
745         GetBlueprint getRequest = cloudify.blueprints().getMetadataById(blueprintId);
746         try {
747             Blueprint bp = getRequest.execute();
748             logger.debug("Blueprint exists: {}", bp.getId());
749             return true;
750         } catch (CloudifyResponseException ce) {
751             if (ce.getStatus() == 404) {
752                 return false;
753             } else {
754                 throw ce;
755             }
756         } catch (Exception e) {
757             throw e;
758         }
759     }
760
761     /**
762      * Upload a blueprint to the Cloudify Manager that is servicing a Cloud Site. The blueprint currently must be
763      * structured as a single directory with all of the required files. One of those files is designated the "main file"
764      * for the blueprint. Files are provided as byte arrays, though expect only text files will be distributed from ASDC
765      * and stored by MSO.
766      *
767      * Cloudify requires a single root directory in its blueprint zip files. The requested blueprint ID will also be
768      * used as the directory. All of the files will be added to this directory in the zip file.
769      */
770     public void uploadBlueprint(String cloudSiteId, String blueprintId, String mainFileName,
771             Map<String, byte[]> blueprintFiles, boolean failIfExists) throws MsoException {
772         // Obtain the cloud site information where we will load the blueprint
773         Optional<CloudSite> cloudSite = cloudConfig.getCloudSite(cloudSiteId);
774         if (!cloudSite.isPresent()) {
775             throw new MsoCloudSiteNotFound(cloudSiteId);
776         }
777
778         Cloudify cloudify = getCloudifyClient(cloudSite.get());
779
780         boolean blueprintUploaded = uploadBlueprint(cloudify, blueprintId, mainFileName, blueprintFiles);
781
782         if (!blueprintUploaded && failIfExists) {
783             throw new MsoAdapterException("Blueprint already exists");
784         }
785     }
786
787     /*
788      * Common method to load a blueprint. May be called from
789      */
790     protected boolean uploadBlueprint(Cloudify cloudify, String blueprintId, String mainFileName,
791             Map<String, byte[]> blueprintFiles) throws MsoException {
792         // Check if it already exists. If so, return false.
793         GetBlueprint getRequest = cloudify.blueprints().getMetadataById(blueprintId);
794         try {
795             Blueprint bp = getRequest.execute();
796             logger.debug("Blueprint {} already exists.", bp.getId());
797             return false;
798         } catch (CloudifyResponseException ce) {
799             if (ce.getStatus() == 404) {
800                 // This is the expected result.
801                 logger.debug("Verified that Blueprint doesn't exist yet");
802             } else {
803                 throw ce;
804             }
805         } catch (Exception e) {
806             throw e;
807         }
808
809         // Create a blueprint ZIP file in memory
810         ByteArrayOutputStream zipBuffer = new ByteArrayOutputStream();
811         ZipOutputStream zipOut = new ZipOutputStream(zipBuffer);
812
813         try {
814             // Put the root directory
815             String rootDir = blueprintId + ((blueprintId.endsWith("/") ? "" : "/"));
816             zipOut.putNextEntry(new ZipEntry(rootDir));
817             zipOut.closeEntry();
818
819             for (String fileName : blueprintFiles.keySet()) {
820                 ZipEntry ze = new ZipEntry(rootDir + fileName);
821                 zipOut.putNextEntry(ze);
822                 zipOut.write(blueprintFiles.get(fileName));
823                 zipOut.closeEntry();
824             }
825             zipOut.close();
826         } catch (IOException e) {
827             // Since we're writing to a byte array, this should never happen
828         }
829         logger.debug("Blueprint zip file size: {}", zipBuffer.size());
830
831         // Ready to upload the blueprint zip
832
833         try (InputStream blueprintStream = new ByteArrayInputStream(zipBuffer.toByteArray())) {
834             UploadBlueprint uploadRequest =
835                     cloudify.blueprints().uploadFromStream(blueprintId, mainFileName, blueprintStream);
836             Blueprint blueprint = uploadRequest.execute();
837             logger.debug("Successfully uploaded blueprint {}", blueprint.getId());
838         } catch (CloudifyResponseException | CloudifyConnectException e) {
839             throw cloudifyExceptionToMsoException(e, "UPLOAD_BLUEPRINT");
840         } catch (RuntimeException e) {
841             // Catch-all
842             throw runtimeExceptionToMsoException(e, "UPLOAD_BLUEPRINT");
843         } catch (IOException e) {
844             // for try-with-resources
845             throw ioExceptionToMsoException(e, "UPLOAD_BLUEPRINT");
846         }
847
848         return true;
849     }
850
851
852
853     // ---------------------------------------------------------------
854     // PRIVATE FUNCTIONS FOR USE WITHIN THIS CLASS
855
856     /**
857      * Get a Cloudify client for the specified cloud site. Everything that is required can be found in the Cloud Config.
858      *
859      * @param cloudSite
860      * @return a Cloudify object
861      */
862     public Cloudify getCloudifyClient(CloudSite cloudSite) throws MsoException {
863         CloudifyManager cloudifyConfig = cloudConfig.getCloudifyManager(cloudSite.getCloudifyId());
864         if (cloudifyConfig == null) {
865             throw new MsoCloudifyManagerNotFound(cloudSite.getId());
866         }
867
868         // Get a Cloudify client
869         // Set a Token Provider to fetch tokens from Cloudify itself.
870         String cloudifyUrl = cloudifyConfig.getCloudifyUrl();
871         Cloudify cloudify = new Cloudify(cloudifyUrl);
872         cloudify.setTokenProvider(new CloudifyClientTokenProvider(cloudifyUrl, cloudifyConfig.getUsername(),
873                 CryptoUtils.decryptCloudConfigPassword(cloudifyConfig.getPassword())));
874
875         return cloudify;
876     }
877
878
879     /*
880      * Query for a Cloudify Deployment. This function is needed in several places, so a common method is useful. This
881      * method takes an authenticated CloudifyClient (which internally identifies the cloud & tenant to search), and
882      * returns a Deployment object if found, Null if not found, or an MsoCloudifyException if the Cloudify API call
883      * fails.
884      *
885      * @param cloudifyClient an authenticated Cloudify client
886      *
887      * @param deploymentId the deployment to query
888      *
889      * @return a Deployment object or null if the requested deployment doesn't exist.
890      *
891      * @throws MsoCloudifyException Thrown if the Cloudify API call returns an exception
892      */
893     protected Deployment queryDeployment(Cloudify cloudify, String deploymentId) throws MsoException {
894         if (deploymentId == null) {
895             return null;
896         }
897         try {
898             GetDeployment request = cloudify.deployments().byId(deploymentId);
899             return executeAndRecordCloudifyRequest(request);
900         } catch (CloudifyResponseException e) {
901             if (e.getStatus() == 404) {
902                 logger.debug("queryDeployment - not found: {}", deploymentId);
903                 return null;
904             } else {
905                 // Convert the CloudifyResponseException to an MsoCloudifyException
906                 throw cloudifyExceptionToMsoException(e, "QueryDeployment");
907             }
908         } catch (CloudifyConnectException e) {
909             // Connection to Openstack failed
910             throw cloudifyExceptionToMsoException(e, "QueryDeployment");
911         }
912     }
913
914
915     public void copyStringOutputsToInputs(Map<String, String> inputs, Map<String, Object> otherStackOutputs,
916             boolean overWrite) {
917         if (inputs == null || otherStackOutputs == null)
918             return;
919
920         for (Map.Entry<String, Object> entry : otherStackOutputs.entrySet()) {
921             String key = entry.getKey();
922             Object value = entry.getValue();
923
924             if (value instanceof JsonNode) {
925                 // This is a bit of mess - but I think it's the least impacting
926                 // let's convert it BACK to a string - then it will get converted back later
927                 try {
928                     inputs.put(key, this.convertNode((JsonNode) value));
929                 } catch (Exception e) {
930                     logger.debug("WARNING: unable to convert JsonNode output value for {}", key);
931                     // effect here is this value will not have been copied to the inputs - and therefore will error out
932                     // downstream
933                 }
934             } else if (value instanceof java.util.LinkedHashMap) {
935                 logger.debug("LinkedHashMap - this is showing up as a LinkedHashMap instead of JsonNode");
936                 try {
937                     inputs.put(key, JSON_MAPPER.writeValueAsString(value));
938                 } catch (Exception e) {
939                     logger.debug("WARNING: unable to convert LinkedHashMap output value for {}", key);
940                 }
941             } else {
942                 // just try to cast it - could be an integer or some such
943                 try {
944                     inputs.put(key, (String) value);
945                 } catch (Exception e) {
946                     logger.debug("WARNING: unable to convert output value for {}", key);
947                     // effect here is this value will not have been copied to the inputs - and therefore will error out
948                     // downstream
949                 }
950             }
951         }
952         return;
953     }
954
955     /*
956      * Normalize an input value to an Object, based on the target parameter type. If the type is not recognized, it will
957      * just be returned unchanged (as a string).
958      */
959     public Object convertInputValue(Object inputValue, HeatTemplateParam templateParam) {
960         String type = templateParam.getParamType();
961         logger.debug("Parameter: {} is of type {}", templateParam.getParamName(), type);
962
963         if (type.equalsIgnoreCase("number")) {
964             try {
965                 return Integer.valueOf(inputValue.toString());
966             } catch (Exception e) {
967                 logger.debug("Unable to convert {} to an integer!", inputValue);
968                 return null;
969             }
970         } else if (type.equalsIgnoreCase("json")) {
971             try {
972                 if (inputValue instanceof String) {
973                     return JSON_MAPPER.readTree(inputValue.toString());
974                 }
975                 // will already marshal to json without intervention
976                 return inputValue;
977             } catch (Exception e) {
978                 logger.debug("Unable to convert {} to a JsonNode!", inputValue);
979                 return null;
980             }
981         } else if (type.equalsIgnoreCase("boolean")) {
982             return new Boolean(inputValue.toString());
983         }
984
985         // Nothing else matched. Return the original string
986         return inputValue;
987     }
988
989
990     private String convertNode(final JsonNode node) {
991         try {
992             final Object obj = JSON_MAPPER.treeToValue(node, Object.class);
993             return JSON_MAPPER.writeValueAsString(obj);
994         } catch (JsonParseException jpe) {
995             logger.debug("Error converting json to string {}", jpe);
996         } catch (Exception e) {
997             logger.debug("Error converting json to string {}", e);
998         }
999         return "[Error converting json to string]";
1000     }
1001
1002
1003     /*
1004      * Method to execute a Cloudify command and track its execution time. For the metrics log, a category of "Cloudify"
1005      * is used along with a sub-category that identifies the specific call (using the real cloudify-client classname of
1006      * the CloudifyRequest<T> parameter).
1007      */
1008
1009
1010     protected <T> T executeAndRecordCloudifyRequest(CloudifyRequest<T> request) {
1011
1012         String requestType;
1013         if (request.getClass().getEnclosingClass() != null) {
1014             requestType =
1015                     request.getClass().getEnclosingClass().getSimpleName() + "." + request.getClass().getSimpleName();
1016         } else {
1017             requestType = request.getClass().getSimpleName();
1018         }
1019
1020         int retryDelay = poConfig.getRetryDelay();
1021         int retryCount = poConfig.getRetryCount();
1022         String retryCodes = poConfig.getRetryCodes();
1023
1024         // Run the actual command. All exceptions will be propagated
1025         while (true) {
1026             try {
1027                 return request.execute();
1028             } catch (CloudifyResponseException e) {
1029                 boolean retry = false;
1030                 if (retryCodes != null) {
1031                     int code = e.getStatus();
1032                     logger.debug("Config values RetryDelay: {} RetryCount:{} RetryCodes:{} ResponseCode:{}", retryDelay,
1033                             retryCount, retryCodes, code);
1034                     for (String rCode : retryCodes.split(",")) {
1035                         try {
1036                             if (retryCount > 0 && code == Integer.parseInt(rCode)) {
1037                                 retryCount--;
1038                                 retry = true;
1039                                 logger.debug(
1040                                         "CloudifyResponseException ResponseCode:{} request:{} Retry indicated. Attempts remaining:{}",
1041                                         code, requestType, retryCount);
1042                                 break;
1043                             }
1044                         } catch (NumberFormatException e1) {
1045                             logger.error("{} No retries. Exception in parsing retry code in config:{} {}",
1046                                     MessageEnum.RA_CONFIG_EXC, rCode, ErrorCode.SchemaError.getValue());
1047                             throw e;
1048                         }
1049                     }
1050                 }
1051                 if (retry) {
1052                     sleep(retryDelay * 1000L);
1053                 } else
1054                     throw e; // exceeded retryCount or code is not retryable
1055             } catch (CloudifyConnectException e) {
1056                 // Connection to Cloudify failed
1057                 if (retryCount > 0) {
1058                     retryCount--;
1059                     logger.debug(" request: {} Retry indicated. Attempts remaining:{}", requestType, retryCount);
1060                     sleep(retryDelay * 1000L);
1061                 } else
1062                     throw e;
1063
1064             }
1065         }
1066     }
1067
1068     /*
1069      * Convert an Exception on a Cloudify call to an MsoCloudifyException. This method supports
1070      * CloudifyResponseException and CloudifyConnectException.
1071      */
1072     protected MsoException cloudifyExceptionToMsoException(CloudifyBaseException e, String context) {
1073         MsoException me = null;
1074
1075         if (e instanceof CloudifyResponseException) {
1076             CloudifyResponseException re = (CloudifyResponseException) e;
1077
1078             try {
1079                 // Failed Cloudify calls return an error entity body.
1080                 CloudifyError error = re.getResponse().getErrorEntity(CloudifyError.class);
1081                 logger.error("{} {} {} Exception - Cloudify Error on {}: {}", MessageEnum.RA_CONNECTION_EXCEPTION,
1082                         CLOUDIFY, ErrorCode.DataError.getValue(), context, error.getErrorCode());
1083                 String fullError = error.getErrorCode() + ": " + error.getMessage();
1084                 logger.debug(fullError);
1085                 me = new MsoCloudifyException(re.getStatus(), re.getMessage(), fullError);
1086             } catch (Exception e2) {
1087                 // Couldn't parse the body as a "CloudifyError". Report the original HTTP error.
1088                 logger.error("{} {} {} Exception - HTTP Error on {}: {}, {} ", MessageEnum.RA_CONNECTION_EXCEPTION,
1089                         CLOUDIFY, ErrorCode.DataError.getValue(), context, re.getStatus(), e.getMessage(), e2);
1090                 me = new MsoCloudifyException(re.getStatus(), re.getMessage(), "");
1091             }
1092
1093             // Add the context of the error
1094             me.addContext(context);
1095
1096             // Generate an alarm for 5XX and higher errors.
1097             if (re.getStatus() >= 500) {
1098
1099             }
1100         } else if (e instanceof CloudifyConnectException) {
1101             CloudifyConnectException ce = (CloudifyConnectException) e;
1102
1103             me = new MsoIOException(ce.getMessage());
1104             me.addContext(context);
1105
1106             // Generate an alarm for all connection errors.
1107
1108             logger.error("{} {} {} Cloudify connection error on {}: ", MessageEnum.RA_CONNECTION_EXCEPTION, CLOUDIFY,
1109                     ErrorCode.DataError.getValue(), context, e);
1110         }
1111
1112         return me;
1113     }
1114
1115
1116
1117     /*******************************************************************************
1118      *
1119      * Methods (and associated utilities) to implement the VduPlugin interface
1120      *
1121      *******************************************************************************/
1122
1123     /**
1124      * VduPlugin interface for instantiate function.
1125      *
1126      * This one is a bit more complex, in that it will first upload the blueprint if needed, then create the Cloudify
1127      * deployment and execute the install workflow.
1128      *
1129      * This implementation also merges any parameters defined in the ENV file with the other other input parameters for
1130      * any undefined parameters). The basic MsoCloudifyUtils separates blueprint management from deploument actions, but
1131      * the VduPlugin does not declare blueprint management operations.
1132      */
1133     @Override
1134     public VduInstance instantiateVdu(CloudInfo cloudInfo, String instanceName, Map<String, Object> inputs,
1135             VduModelInfo vduModel, boolean rollbackOnFailure) throws VduException {
1136         String cloudSiteId = cloudInfo.getCloudSiteId();
1137         String tenantId = cloudInfo.getTenantId();
1138
1139         // Translate the VDU ModelInformation structure to that which is needed for
1140         // creating and uploading a blueprint. Use the model customization UUID as
1141         // the blueprint identifier.
1142
1143         String blueprintId = vduModel.getModelCustomizationUUID();
1144
1145         try {
1146
1147             if (!isBlueprintLoaded(cloudSiteId, blueprintId)) {
1148                 logger.debug("Blueprint {} is not loaded.  Will upload it now.", blueprintId);
1149
1150                 // Prepare the blueprint inputs. Need the set of blueprint templates and files,
1151                 // plus the main blueprint name.
1152                 Map<String, byte[]> blueprintFiles = new HashMap<>();
1153                 String mainTemplate = "";
1154
1155                 // Add all of the blueprint artifacts from the VDU model
1156                 List<VduArtifact> vduArtifacts = vduModel.getArtifacts();
1157                 for (VduArtifact vduArtifact : vduArtifacts) {
1158                     // Add all artifacts to the blueprint, with one exception.
1159                     // ENVIRONMENT files will be processed later as additional parameters.
1160
1161                     ArtifactType artifactType = vduArtifact.getType();
1162                     if (artifactType != ArtifactType.ENVIRONMENT) {
1163                         blueprintFiles.put(vduArtifact.getName(), vduArtifact.getContent());
1164
1165                         if (artifactType == ArtifactType.MAIN_TEMPLATE) {
1166                             mainTemplate = vduArtifact.getName();
1167                         }
1168                     }
1169                 }
1170
1171                 // Upload the blueprint package
1172                 uploadBlueprint(cloudSiteId, blueprintId, mainTemplate, blueprintFiles, false);
1173             }
1174         } catch (Exception e) {
1175             throw new VduException("CloudifyUtils (instantiateVDU): blueprint Exception", e);
1176         }
1177
1178
1179         // Next, create and install a new deployment based on the blueprint.
1180         // For Cloudify, the deploymentId is specified by the client. Just use the instance name
1181         // as the ID.
1182
1183         try {
1184             // Query the Cloudify Deployment object and populate a VduInstance
1185             DeploymentInfo deployment =
1186                     createAndInstallDeployment(cloudSiteId, tenantId, instanceName, blueprintId, inputs, true, // (poll
1187                                                                                                                // for
1188                                                                                                                // completion)
1189                             vduModel.getTimeoutMinutes(), rollbackOnFailure);
1190
1191             return deploymentInfoToVduInstance(deployment);
1192         } catch (Exception e) {
1193             throw new VduException("CloudifyUtils (instantiateVDU): Create-and-install-deployment Exception", e);
1194         }
1195     }
1196
1197
1198     /**
1199      * VduPlugin interface for query function.
1200      */
1201     @Override
1202     public VduInstance queryVdu(CloudInfo cloudInfo, String instanceId) throws VduException {
1203         String cloudSiteId = cloudInfo.getCloudSiteId();
1204         String tenantId = cloudInfo.getTenantId();
1205
1206         try {
1207             // Query the Cloudify Deployment object and populate a VduInstance
1208             DeploymentInfo deployment = queryDeployment(cloudSiteId, tenantId, instanceId);
1209
1210             return deploymentInfoToVduInstance(deployment);
1211         } catch (Exception e) {
1212             throw new VduException("Query VDU Exception", e);
1213         }
1214     }
1215
1216
1217     /**
1218      * VduPlugin interface for delete function.
1219      */
1220     @Override
1221     public VduInstance deleteVdu(CloudInfo cloudInfo, String instanceId, int timeoutMinutes) throws VduException {
1222         String cloudSiteId = cloudInfo.getCloudSiteId();
1223         String tenantId = cloudInfo.getTenantId();
1224
1225         try {
1226             // Uninstall and delete the Cloudify Deployment
1227             DeploymentInfo deployment = uninstallAndDeleteDeployment(cloudSiteId, tenantId, instanceId, timeoutMinutes);
1228
1229             // Populate a VduInstance based on the deleted Cloudify Deployment object
1230             return deploymentInfoToVduInstance(deployment);
1231         } catch (Exception e) {
1232             throw new VduException("Delete VDU Exception", e);
1233         }
1234     }
1235
1236
1237     /**
1238      * VduPlugin interface for update function.
1239      *
1240      * Update is currently not supported in the MsoCloudifyUtils implementation. Just return a VduException.
1241      *
1242      */
1243     @Override
1244     public VduInstance updateVdu(CloudInfo cloudInfo, String instanceId, Map<String, Object> inputs,
1245             VduModelInfo vduModel, boolean rollbackOnFailure) throws VduException {
1246         throw new VduException("CloudifyUtils: updateVDU interface not supported");
1247     }
1248
1249
1250     /*
1251      * Convert the local DeploymentInfo object (Cloudify-specific) to a generic VduInstance object
1252      */
1253     protected VduInstance deploymentInfoToVduInstance(DeploymentInfo deployment) {
1254         VduInstance vduInstance = new VduInstance();
1255
1256         // only one ID in Cloudify, use for both VDU name and ID
1257         vduInstance.setVduInstanceId(deployment.getId());
1258         vduInstance.setVduInstanceName(deployment.getId());
1259
1260         // Copy inputs and outputs
1261         vduInstance.setInputs(deployment.getInputs());
1262         vduInstance.setOutputs(deployment.getOutputs());
1263
1264         // Translate the status elements
1265         vduInstance.setStatus(deploymentStatusToVduStatus(deployment));
1266
1267         return vduInstance;
1268     }
1269
1270     protected VduStatus deploymentStatusToVduStatus(DeploymentInfo deployment) {
1271         VduStatus vduStatus = new VduStatus();
1272
1273         // Determine the status based on last action & status
1274         // DeploymentInfo object should be enhanced to report a better status internally.
1275         DeploymentStatus status = deployment.getStatus();
1276
1277         if (status == null) {
1278             vduStatus.setState(VduStateType.UNKNOWN);
1279         } else if (status == DeploymentStatus.NOTFOUND) {
1280             vduStatus.setState(VduStateType.NOTFOUND);
1281         } else if (status == DeploymentStatus.INSTALLED) {
1282             vduStatus.setState(VduStateType.INSTANTIATED);
1283         } else if (status == DeploymentStatus.CREATED) {
1284             // Deployment exists but is not installed. This shouldn't really happen,
1285             // since create + install or uninstall + delete are always done together.
1286             // But account for it anyway, assuming the operation is still in progress.
1287             String lastAction = deployment.getLastAction();
1288             if (lastAction == null)
1289                 vduStatus.setState(VduStateType.INSTANTIATING);
1290             else
1291                 vduStatus.setState(VduStateType.DELETING);
1292         } else if (status == DeploymentStatus.FAILED) {
1293             vduStatus.setState(VduStateType.FAILED);
1294         } else {
1295             vduStatus.setState(VduStateType.UNKNOWN);
1296         }
1297
1298         vduStatus.setErrorMessage(deployment.getErrorMessage());
1299         vduStatus.setLastAction(new PluginAction(deployment.getLastAction(), deployment.getActionStatus(),
1300                 deployment.getErrorMessage()));
1301
1302         return vduStatus;
1303     }
1304
1305     /*
1306      * Return an OpenstackConfig object as expected by Cloudify Openstack Plug-in. Base the values on the CloudSite
1307      * definition.
1308      */
1309     protected OpenstackConfig getOpenstackConfig(CloudSite cloudSite, String tenantId) {
1310         OpenstackConfig openstackConfig = new OpenstackConfig();
1311         openstackConfig.setRegion(cloudSite.getRegionId());
1312         openstackConfig.setAuthUrl(cloudSite.getIdentityService().getIdentityUrl());
1313         openstackConfig.setUsername(cloudSite.getIdentityService().getMsoId());
1314         openstackConfig
1315                 .setPassword(CryptoUtils.decryptCloudConfigPassword(cloudSite.getIdentityService().getMsoPass()));
1316         openstackConfig.setTenantName(tenantId);
1317         return openstackConfig;
1318     }
1319
1320     /*
1321      * Return an Azure object as expected by Cloudify Azure Plug-in. Base the values on the CloudSite definition.
1322      */
1323     protected AzureConfig getAzureConfig(CloudSite cloudSite, String tenantId) {
1324         AzureConfig azureConfig = new AzureConfig();
1325         // TODO: Use adminTenant for now, instead of adding another element
1326         azureConfig.setSubscriptionId(cloudSite.getIdentityService().getAdminTenant());
1327         azureConfig.setTenantId(tenantId);
1328         azureConfig.setClientId(cloudSite.getIdentityService().getMsoId());
1329         azureConfig.setClientSecret(cloudSite.getIdentityService().getMsoPass());
1330         return azureConfig;
1331     }
1332
1333     private void sleep(long time) {
1334         try {
1335             Thread.sleep(time);
1336         } catch (InterruptedException e) {
1337             logger.debug("Thread interrupted while sleeping!", e);
1338             Thread.currentThread().interrupt();
1339         }
1340     }
1341 }