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