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