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