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