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