300d06b5d196a9435be315dbdb553882a11f6a88
[so.git] / adapters / mso-adapter-utils / src / main / java / org / onap / so / openstack / utils / MsoHeatUtils.java
1 /*-
2  * ============LICENSE_START=======================================================
3  * ONAP - SO
4  * ================================================================================
5  * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
6  * Copyright (C) 2017 Huawei Technologies Co., Ltd. All rights reserved.
7  * ================================================================================
8  * Modifications Copyright (c) 2019 Samsung
9  * ================================================================================
10  * Licensed under the Apache License, Version 2.0 (the "License");
11  * you may not use this file except in compliance with the License.
12  * You may obtain a copy of the License at
13  *
14  *      http://www.apache.org/licenses/LICENSE-2.0
15  *
16  * Unless required by applicable law or agreed to in writing, software
17  * distributed under the License is distributed on an "AS IS" BASIS,
18  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19  * See the License for the specific language governing permissions and
20  * limitations under the License.
21  * ============LICENSE_END=========================================================
22  */
23
24 package org.onap.so.openstack.utils;
25
26 import com.fasterxml.jackson.databind.JsonNode;
27 import com.fasterxml.jackson.databind.ObjectMapper;
28 import com.woorea.openstack.base.client.OpenStackConnectException;
29 import com.woorea.openstack.base.client.OpenStackRequest;
30 import com.woorea.openstack.base.client.OpenStackResponseException;
31 import com.woorea.openstack.heat.Heat;
32 import com.woorea.openstack.heat.model.CreateStackParam;
33 import com.woorea.openstack.heat.model.Resources;
34 import com.woorea.openstack.heat.model.Stack;
35 import com.woorea.openstack.heat.model.Stack.Output;
36 import com.woorea.openstack.heat.model.Stacks;
37 import com.woorea.openstack.keystone.Keystone;
38 import com.woorea.openstack.keystone.model.Access;
39 import com.woorea.openstack.keystone.model.Authentication;
40 import com.woorea.openstack.keystone.utils.KeystoneUtils;
41
42 import java.io.IOException;
43 import java.util.ArrayList;
44 import java.util.Arrays;
45 import java.util.Calendar;
46 import java.util.HashMap;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Map.Entry;
50 import java.util.Set;
51 import org.onap.so.adapters.vdu.CloudInfo;
52 import org.onap.so.adapters.vdu.PluginAction;
53 import org.onap.so.adapters.vdu.VduArtifact;
54 import org.onap.so.adapters.vdu.VduArtifact.ArtifactType;
55 import org.onap.so.adapters.vdu.VduException;
56 import org.onap.so.adapters.vdu.VduInstance;
57 import org.onap.so.adapters.vdu.VduModelInfo;
58 import org.onap.so.adapters.vdu.VduPlugin;
59 import org.onap.so.adapters.vdu.VduStateType;
60 import org.onap.so.adapters.vdu.VduStatus;
61 import org.onap.so.cloud.CloudConfig;
62 import org.onap.so.cloud.authentication.AuthenticationMethodFactory;
63 import org.onap.so.cloud.authentication.KeystoneAuthHolder;
64 import org.onap.so.cloud.authentication.KeystoneV3Authentication;
65 import org.onap.so.cloud.authentication.ServiceEndpointNotFoundException;
66 import org.onap.so.db.catalog.beans.CloudIdentity;
67 import org.onap.so.db.catalog.beans.CloudSite;
68 import org.onap.so.db.catalog.beans.HeatTemplate;
69 import org.onap.so.db.catalog.beans.HeatTemplateParam;
70 import org.onap.so.db.catalog.beans.ServerType;
71 import org.onap.so.logger.ErrorCode;
72 import org.onap.so.logger.MessageEnum;
73 import org.onap.so.openstack.beans.HeatStatus;
74 import org.onap.so.openstack.beans.StackInfo;
75 import org.onap.so.openstack.exceptions.MsoAdapterException;
76 import org.onap.so.openstack.exceptions.MsoCloudSiteNotFound;
77 import org.onap.so.openstack.exceptions.MsoException;
78 import org.onap.so.openstack.exceptions.MsoIOException;
79 import org.onap.so.openstack.exceptions.MsoOpenstackException;
80 import org.onap.so.openstack.exceptions.MsoStackAlreadyExists;
81 import org.onap.so.openstack.exceptions.MsoTenantNotFound;
82 import org.onap.so.openstack.mappers.StackInfoMapper;
83 import org.onap.so.utils.CryptoUtils;
84 import org.slf4j.Logger;
85 import org.slf4j.LoggerFactory;
86 import org.springframework.beans.factory.annotation.Autowired;
87 import org.springframework.context.annotation.Primary;
88 import org.springframework.core.env.Environment;
89 import org.springframework.stereotype.Component;
90
91 @Primary
92 @Component
93 public class MsoHeatUtils extends MsoCommonUtils implements VduPlugin{
94
95     private static final String TOKEN_AUTH = "TokenAuth";
96
97     private static final String QUERY_ALL_STACKS = "QueryAllStacks";
98
99     private static final String DELETE_STACK = "DeleteStack";
100
101     protected static final String HEAT_ERROR = "HeatError";
102
103     protected static final String CREATE_STACK = "CreateStack";
104
105     // Fetch cloud configuration each time (may be cached in CloudConfig class)
106     @Autowired
107     protected CloudConfig cloudConfig;
108
109     @Autowired
110     private Environment environment;
111
112     @Autowired
113     private AuthenticationMethodFactory authenticationMethodFactory;
114
115     @Autowired
116     private MsoTenantUtilsFactory tenantUtilsFactory;
117
118     @Autowired
119     private KeystoneV3Authentication keystoneV3Authentication;
120
121     private static final Logger logger = LoggerFactory.getLogger(MsoHeatUtils.class);
122
123     // Properties names and variables (with default values)
124     protected String createPollIntervalProp = "org.onap.so.adapters.po.pollInterval";
125     private String deletePollIntervalProp = "org.onap.so.adapters.po.pollInterval";
126     private String deletePollTimeoutProp = "org.onap.so.adapters.po.pollTimeout";
127
128     protected static final String createPollIntervalDefault = "15";
129     private static final String deletePollIntervalDefault = "15";
130
131     private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
132
133     /**
134      * keep this old method signature here to maintain backwards compatibility. keep others as well.
135      * this method does not include environment, files, or heatFiles
136      */
137     public StackInfo createStack (String cloudSiteId,
138                                   String cloudOwner,
139                                   String tenantId,
140                                   String stackName,
141                                   String heatTemplate,
142                                   Map <String, ?> stackInputs,
143                                   boolean pollForCompletion,
144                                   int timeoutMinutes) throws MsoException {
145         // Just call the new method with the environment & files variable set to null
146         return this.createStack (cloudSiteId,
147                                  cloudOwner,
148                                  tenantId,
149                                  stackName,
150                                  null,
151                                  heatTemplate,
152                                  stackInputs,
153                                  pollForCompletion,
154                                  timeoutMinutes,
155                                  null,
156                                  null,
157                                  null,
158                                  true);
159     }
160
161     // This method has environment, but not files or heatFiles
162     public StackInfo createStack (String cloudSiteId,
163                                   String cloudOwner,
164                                   String tenantId,
165                                   String stackName,
166                                   String heatTemplate,
167                                   Map <String, ?> stackInputs,
168                                   boolean pollForCompletion,
169                                   int timeoutMinutes,
170                                   String environment) throws MsoException {
171         // Just call the new method with the files/heatFiles variables set to null
172         return this.createStack (cloudSiteId,
173                                  cloudOwner,
174                                  tenantId,
175                                  stackName,
176                                  null,
177                                  heatTemplate,
178                                  stackInputs,
179                                  pollForCompletion,
180                                  timeoutMinutes,
181                                  environment,
182                                  null,
183                                  null,
184                                  true);
185     }
186
187     // This method has environment and files, but not heatFiles.
188     public StackInfo createStack (String cloudSiteId,
189                                   String cloudOwner,
190                                   String tenantId,
191                                   String stackName,
192                                   String heatTemplate,
193                                   Map <String, ?> stackInputs,
194                                   boolean pollForCompletion,
195                                   int timeoutMinutes,
196                                   String environment,
197                                   Map <String, Object> files) throws MsoException {
198         return this.createStack (cloudSiteId,
199                                  cloudOwner,
200                                  tenantId,
201                                  stackName,
202                                  null,
203                                  heatTemplate,
204                                  stackInputs,
205                                  pollForCompletion,
206                                  timeoutMinutes,
207                                  environment,
208                                  files,
209                                  null,
210                                  true);
211     }
212
213     // This method has environment, files, heatfiles
214     public StackInfo createStack (String cloudSiteId,
215                                   String cloudOwner,
216                                   String tenantId,
217                                   String stackName,
218                                   String heatTemplate,
219                                   Map <String, ?> stackInputs,
220                                   boolean pollForCompletion,
221                                   int timeoutMinutes,
222                                   String environment,
223                                   Map <String, Object> files,
224                                   Map <String, Object> heatFiles) throws MsoException {
225         return this.createStack (cloudSiteId,
226                                  cloudOwner,
227                                  tenantId,
228                                  stackName,
229                                  null,
230                                  heatTemplate,
231                                  stackInputs,
232                                  pollForCompletion,
233                                  timeoutMinutes,
234                                  environment,
235                                  files,
236                                  heatFiles,
237                                  true);
238     }
239
240     /**
241      * Create a new Stack in the specified cloud location and tenant. The Heat template
242      * and parameter map are passed in as arguments, along with the cloud access credentials.
243      * It is expected that parameters have been validated and contain at minimum the required
244      * parameters for the given template with no extra (undefined) parameters..
245      *
246      * The Stack name supplied by the caller must be unique in the scope of this tenant.
247      * However, it should also be globally unique, as it will be the identifier for the
248      * resource going forward in Inventory. This latter is managed by the higher levels
249      * invoking this function.
250      *
251      * The caller may choose to let this function poll Openstack for completion of the
252      * stack creation, or may handle polling itself via separate calls to query the status.
253      * In either case, a StackInfo object will be returned containing the current status.
254      * When polling is enabled, a status of CREATED is expected. When not polling, a
255      * status of BUILDING is expected.
256      *
257      * An error will be thrown if the requested Stack already exists in the specified
258      * Tenant and Cloud.
259      *
260      * For 1510 - add "environment", "files" (nested templates), and "heatFiles" (get_files) as
261      * parameters for createStack. If environment is non-null, it will be added to the stack.
262      * The nested templates and get_file entries both end up being added to the "files" on the
263      * stack. We must combine them before we add them to the stack if they're both non-null.
264      *
265      * @param cloudSiteId The cloud (may be a region) in which to create the stack.
266      * @param cloudOwner the cloud owner of the cloud site in which to create the stack
267      * @param tenantId The Openstack ID of the tenant in which to create the Stack
268      * @param stackName The name of the stack to create
269      * @param vduModelInfo contains information about the vdu model (added for plugin adapter)
270      * @param heatTemplate The Heat template
271      * @param stackInputs A map of key/value inputs
272      * @param pollForCompletion Indicator that polling should be handled in Java vs. in the client
273      * @param environment An optional yaml-format string to specify environmental parameters
274      * @param files a Map<String, Object> that lists the child template IDs (file is the string, object is an int of
275      *        Template id)
276      * @param heatFiles a Map<String, Object> that lists the get_file entries (fileName, fileBody)
277      * @param backout Donot delete stack on create Failure - defaulted to True
278      * @return A StackInfo object
279      * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception.
280      */
281
282     @SuppressWarnings("unchecked")
283     public StackInfo createStack (String cloudSiteId,
284                                   String cloudOwner,
285                                   String tenantId,
286                                   String stackName,
287                                   VduModelInfo vduModel,
288                                   String heatTemplate,
289                                   Map <String, ?> stackInputs,
290                                   boolean pollForCompletion,
291                                   int timeoutMinutes,
292                                   String environment,
293                                   Map <String, Object> files,
294                                   Map <String, Object> heatFiles,
295                                   boolean backout) throws MsoException {
296
297         // Take out the multicloud inputs, if present.
298         for (String key : MsoMulticloudUtils.MULTICLOUD_INPUTS) {
299             if (stackInputs.containsKey(key)) {
300                 stackInputs.remove(key);
301                 if (stackInputs.isEmpty()) {
302                     break;
303                 }
304             }
305         }
306
307         CreateStackParam stack = createStackParam(stackName, heatTemplate, stackInputs, timeoutMinutes, environment, files, heatFiles);
308
309         // Obtain the cloud site information where we will create the stack
310         CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
311                 () -> new MsoCloudSiteNotFound(cloudSiteId));
312         logger.debug("Found: {}", cloudSite);
313         // Get a Heat client. They are cached between calls (keyed by tenantId:cloudId)
314         // This could throw MsoTenantNotFound or MsoOpenstackException (both propagated)
315         Heat heatClient = getHeatClient (cloudSite, tenantId);
316         logger.debug("Found: {}", heatClient);
317
318         logger.debug ("Ready to Create Stack ({}) with input params: {}", heatTemplate, stackInputs);
319
320         Stack heatStack = null;
321         try {
322             OpenStackRequest <Stack> request = heatClient.getStacks ().create (stack);
323             CloudIdentity cloudIdentity = cloudSite.getIdentityService();
324             request.header ("X-Auth-User", cloudIdentity.getMsoId ());
325             request.header ("X-Auth-Key", CryptoUtils.decryptCloudConfigPassword(cloudIdentity.getMsoPass ()));
326             heatStack = executeAndRecordOpenstackRequest (request);
327         } catch (OpenStackResponseException e) {
328             if (e.getStatus () == 409) {
329                 MsoStackAlreadyExists me = new MsoStackAlreadyExists (stackName, tenantId, cloudSiteId);
330                 me.addContext (CREATE_STACK);
331                 throw me;
332         } else {
333                 logger.debug("ERROR STATUS = {},\n{}\n{}", e.getStatus(), e.getMessage(), e.getLocalizedMessage());
334                 throw heatExceptionToMsoException (e, CREATE_STACK);
335             }
336         } catch (OpenStackConnectException e) {
337             throw heatExceptionToMsoException (e, CREATE_STACK);
338         } catch (RuntimeException e) {
339             throw runtimeExceptionToMsoException (e, CREATE_STACK);
340         }
341
342         // Subsequent access by the canonical name "<stack name>/<stack-id>".
343         // Otherwise, simple query by name returns a 302 redirect.
344         // NOTE: This is specific to the v1 Orchestration API.
345         String canonicalName = stackName + "/" + heatStack.getId ();
346
347         if (pollForCompletion) {
348             heatStack = pollStackForCompletion(cloudSiteId, tenantId, stackName, timeoutMinutes, backout, heatClient,
349                                         heatStack, canonicalName);
350         } else {
351             // Get initial status, since it will have been null after the create.
352             heatStack = queryHeatStack (heatClient, canonicalName);
353             logger.debug (heatStack.getStackStatus ());
354         }
355         return new StackInfoMapper(heatStack).map();
356     }
357
358         private Stack pollStackForCompletion(String cloudSiteId, String tenantId, String stackName, int timeoutMinutes,
359                         boolean backout, Heat heatClient, Stack heatStack, String canonicalName)
360                         throws MsoException, MsoOpenstackException {
361                 int createPollInterval = Integer.parseInt(this.environment.getProperty(createPollIntervalProp, createPollIntervalDefault));
362                 int pollTimeout = (timeoutMinutes * 60) + createPollInterval;
363                 int deletePollInterval = createPollInterval;
364                 int deletePollTimeout = pollTimeout;
365                 boolean createTimedOut = false;
366                 StringBuilder stackErrorStatusReason = new StringBuilder("");
367       logger.debug("createPollInterval={}, pollTimeout={}", createPollInterval, pollTimeout);
368
369                 while (true) {
370                     try {
371                         heatStack = queryHeatStack (heatClient, canonicalName);
372             logger.debug("{} ({})", heatStack.getStackStatus(), canonicalName);
373             try {
374                 logger.debug("Current stack {}", this.getOutputsAsStringBuilder(heatStack).toString());
375             } catch (Exception e) {
376                             logger.debug("an error occurred trying to print out the current outputs of the stack", e);
377                         }
378
379                         if ("CREATE_IN_PROGRESS".equals (heatStack.getStackStatus ())) {
380                             if (pollTimeout <= 0) {
381                     logger.error("{} Cloud site: {} Tenant: {} Stack: {} Stack status: {} {} Create stack timeout",
382                         MessageEnum.RA_CREATE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName,
383                         heatStack.getStackStatus(), ErrorCode.AvailabilityError.getValue());
384                     createTimedOut = true;
385                                 break;
386                             }
387                             sleep(createPollInterval * 1000L);
388                             pollTimeout -= createPollInterval;
389                 logger.debug("pollTimeout remaining: {}", pollTimeout);
390             } else {
391                                 stackErrorStatusReason.append("Stack error (" + heatStack.getStackStatus() + "): " + heatStack.getStackStatusReason());
392                             break;
393                         }
394                     } catch (MsoException me) {
395                         // Cannot query the stack status. Something is wrong.
396                         // Try to roll back the stack
397                         if (!backout)
398                         {
399               logger.warn("{} Exception in Create Stack, stack deletion suppressed {}",
400                   MessageEnum.RA_CREATE_STACK_ERR, ErrorCode.BusinessProcesssError.getValue());
401           }
402                         else
403                         {
404                                 try {
405                 logger.debug(
406                     "Create Stack error - unable to query for stack status - attempting to delete stack: {}"
407                         + " - This will likely fail and/or we won't be able to query to see if delete worked",
408                     canonicalName);
409                 OpenStackRequest <Void> request = heatClient.getStacks ().deleteByName (canonicalName);
410                                         executeAndRecordOpenstackRequest (request);
411                                         boolean deleted = false;
412                                         while (!deleted) {
413                                                 try {
414                                                         heatStack = queryHeatStack(heatClient, canonicalName);
415                                                         if (heatStack != null) {
416                       logger.debug(heatStack.getStackStatus());
417                       if ("DELETE_IN_PROGRESS".equals(heatStack.getStackStatus())) {
418                                                                         if (deletePollTimeout <= 0) {
419                             logger.error(
420                                 "{} Cloud site: {} Tenant: {} Stack: {} Stack status: {} {} Rollback: DELETE stack timeout",
421                                 MessageEnum.RA_CREATE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName,
422                                 heatStack.getStackStatus(), ErrorCode.AvailabilityError.getValue());
423                             break;
424                                                                         } else {
425                                                                                 sleep(deletePollInterval * 1000L);
426                                                                                 deletePollTimeout -= deletePollInterval;
427                                                                         }
428                                                                 } else if ("DELETE_COMPLETE".equals(heatStack.getStackStatus())){
429                           logger.debug("DELETE_COMPLETE for {}", canonicalName);
430                           deleted = true;
431                                                                         continue;
432                                                                 } else {
433                                                                         //got a status other than DELETE_IN_PROGRESS or DELETE_COMPLETE - so break and evaluate
434                                                                         break;
435                                                                 }
436                                                         } else {
437                                                                 // assume if we can't find it - it's deleted
438                       logger.debug("heatStack returned null - assume the stack {} has been deleted", canonicalName);
439                       deleted = true;
440                                                                 continue;
441                                                         }
442
443                                                 } catch (Exception e3) {
444                                                         // Just log this one. We will report the original exception.
445                     logger.error("{} Create Stack: Nested exception rolling back stack: {} ",
446                         MessageEnum.RA_CREATE_STACK_ERR, ErrorCode.BusinessProcesssError.getValue(), e3);
447                                                 }
448                                         }
449                                 } catch (Exception e2) {
450                                         // Just log this one. We will report the original exception.
451                 logger.error("{} Create Stack: Nested exception rolling back stack: {} ",
452                     MessageEnum.RA_CREATE_STACK_ERR, ErrorCode.BusinessProcesssError.getValue(), e2);
453             }
454                         }
455
456                         // Propagate the original exception from Stack Query.
457                         me.addContext (CREATE_STACK);
458                         throw me;
459                     }
460                 }
461
462                 if (!"CREATE_COMPLETE".equals (heatStack.getStackStatus ())) {
463         logger.error("{} Create Stack error:  Polling complete with non-success status: {}, {} {} ",
464             MessageEnum.RA_CREATE_STACK_ERR, heatStack.getStackStatus(), heatStack.getStackStatusReason(),
465             ErrorCode.BusinessProcesssError.getValue());
466
467                     // Rollback the stack creation, since it is in an indeterminate state.
468                     if (!backout)
469                     {
470             logger.warn(
471                 "{} Create Stack errored, stack deletion suppressed {} Create Stack error, stack deletion suppressed",
472                 MessageEnum.RA_CREATE_STACK_ERR, ErrorCode.BusinessProcesssError.getValue());
473         }
474                     else
475                     {
476                         try {
477               logger.debug("Create Stack errored - attempting to DELETE stack: {}", canonicalName);
478               logger.debug("deletePollInterval={}, deletePollTimeout={}", deletePollInterval, deletePollTimeout);
479               OpenStackRequest <Void> request = heatClient.getStacks ().deleteByName (canonicalName);
480                                 executeAndRecordOpenstackRequest (request);
481                                 boolean deleted = false;
482                                 while (!deleted) {
483                                         try {
484                                                 heatStack = queryHeatStack(heatClient, canonicalName);
485                                                 if (heatStack != null) {
486                     logger.debug("{} ({})", heatStack.getStackStatus(), canonicalName);
487                     if ("DELETE_IN_PROGRESS".equals(heatStack.getStackStatus())) {
488                                                                 if (deletePollTimeout <= 0) {
489                         logger.error(
490                             "{} Cloud site: {} Tenant: {} Stack: {} Stack status: {} {} Rollback: DELETE stack timeout",
491                             MessageEnum.RA_CREATE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName,
492                             heatStack.getStackStatus(), ErrorCode.AvailabilityError.getValue());
493                         break;
494                                                                 } else {
495                                                                         sleep(deletePollInterval * 1000L);
496                                                                         deletePollTimeout -= deletePollInterval;
497                         logger.debug("deletePollTimeout remaining: {}", deletePollTimeout);
498                     }
499                                                         } else if ("DELETE_COMPLETE".equals(heatStack.getStackStatus())){
500                       logger.debug("DELETE_COMPLETE for {}", canonicalName);
501                       deleted = true;
502                                                                 continue;
503                                                         } else if ("DELETE_FAILED".equals(heatStack.getStackStatus())) {
504                                                                 // Warn about this (?) - but still throw the original exception
505                       logger.warn(
506                           "{} Create Stack errored, stack deletion FAILED {} Create Stack error, stack deletion FAILED",
507                           MessageEnum.RA_CREATE_STACK_ERR, ErrorCode.BusinessProcesssError.getValue());
508                       logger.debug("Stack deletion FAILED on a rollback of a create - {}, status={}, reason={}",
509                           canonicalName, heatStack.getStackStatus(), heatStack.getStackStatusReason());
510                       break;
511                                                         } else {
512                                                                 //got a status other than DELETE_IN_PROGRESS or DELETE_COMPLETE - so break and evaluate
513                                                                 break;
514                                                         }
515                                                 } else {
516                                                         // assume if we can't find it - it's deleted
517                     logger.debug("heatStack returned null - assume the stack {} has been deleted", canonicalName);
518                     deleted = true;
519                                                         continue;
520                                                 }
521
522                                         } catch (MsoException me2) {
523                                                 // We got an exception on the delete - don't throw this exception - throw the original - just log.
524                   logger.debug("Exception thrown trying to delete {} on a create->rollback: {} ", canonicalName,
525                       me2.getContextMessage(), me2);
526                   logger.warn("{} Create Stack errored, then stack deletion FAILED - exception thrown {} {}",
527                       MessageEnum.RA_CREATE_STACK_ERR, ErrorCode.BusinessProcesssError.getValue(),
528                       me2.getContextMessage());
529               }
530
531                                 } // end while !deleted
532                                 StringBuilder errorContextMessage;
533                                 if (createTimedOut) {
534                                         errorContextMessage = new StringBuilder("Stack Creation Timeout");
535                                 } else {
536                                         errorContextMessage  = stackErrorStatusReason;
537                                 }
538                                 if (deleted) {
539                                         errorContextMessage.append(" - stack successfully deleted");
540                                 } else {
541                                         errorContextMessage.append(" - encountered an error trying to delete the stack");
542                                 }
543                         } catch (Exception e2) {
544                                 // shouldn't happen - but handle
545               logger.error("{} Create Stack: Nested exception rolling back stack: {} ", MessageEnum.RA_CREATE_STACK_ERR,
546                   ErrorCode.BusinessProcesssError.getValue(), e2);
547           }
548                     }
549                     MsoOpenstackException me = new MsoOpenstackException(0, "", stackErrorStatusReason.toString());
550                     me.addContext(CREATE_STACK);
551                     throw me;
552                 }
553                 return heatStack;
554         }
555
556     /**
557      * Query for a single stack (by Name) in a tenant. This call will always return a
558      * StackInfo object. If the stack does not exist, an "empty" StackInfo will be
559      * returned - containing only the stack name and a status of NOTFOUND.
560      *
561      * @param tenantId The Openstack ID of the tenant in which to query
562      * @param cloudSiteId The cloud identifier (may be a region) in which to query
563      * @param cloudOwner the cloud owner of the cloud site in which to query
564      * @param stackName The name of the stack to query (may be simple or canonical)
565      * @return A StackInfo object
566      * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception.
567      */
568     public StackInfo queryStack (String cloudSiteId, String cloudOwner, String tenantId, String stackName) throws MsoException {
569         logger.debug ("Query HEAT stack: {} in tenant {}", stackName, tenantId);
570
571         // Obtain the cloud site information where we will create the stack
572         CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
573                 () -> new MsoCloudSiteNotFound(cloudSiteId));
574         logger.debug("Found: {}", cloudSite.toString());
575
576         // Get a Heat client. They are cached between calls (keyed by tenantId:cloudId)
577         Heat heatClient = null;
578         try {
579             heatClient = getHeatClient (cloudSite, tenantId);
580             if (heatClient != null) {
581                 logger.debug("Found: {}", heatClient.toString());
582             }
583         } catch (MsoTenantNotFound e) {
584             // Tenant doesn't exist, so stack doesn't either
585             logger.debug ("Tenant with id " + tenantId + "not found.", e);
586             return new StackInfo (stackName, HeatStatus.NOTFOUND);
587         } catch (MsoException me) {
588             // Got an Openstack error. Propagate it
589             logger.error("{} {} Openstack Exception on Token request: ", MessageEnum.RA_CONNECTION_EXCEPTION,
590                 ErrorCode.AvailabilityError.getValue(), me);
591             me.addContext ("QueryStack");
592             throw me;
593         }
594
595         // Query the Stack.
596         // An MsoException will propagate transparently to the caller.
597         Stack heatStack = queryHeatStack (heatClient, stackName);
598
599         if (heatStack == null) {
600             // Stack does not exist. Return a StackInfo with status NOTFOUND
601             return new StackInfo (stackName, HeatStatus.NOTFOUND);
602         }
603
604         return new StackInfoMapper(heatStack).map();
605     }
606
607     /**
608      * Delete a stack (by Name/ID) in a tenant. If the stack is not found, it will be
609      * considered a successful deletion. The return value is a StackInfo object which
610      * contains the current stack status.
611      *
612      * The client may choose to let the adapter poll Openstack for completion of the
613      * stack deletion, or may handle polling itself via separate query calls. In either
614      * case, a StackInfo object will be returned. When polling is enabled, a final
615      * status of NOTFOUND is expected. When not polling, a status of DELETING is expected.
616      *
617      * There is no rollback from a successful stack deletion. A deletion failure will
618      * also result in an undefined stack state - the components may or may not have been
619      * all or partially deleted, so the resulting stack must be considered invalid.
620      *
621      * @param tenantId The Openstack ID of the tenant in which to perform the delete
622      * @param cloudOwner the cloud owner of the cloud site in  which to delete the stack
623      * @param cloudSiteId The cloud identifier (may be a region) from which to delete the stack.
624      * @param stackName The name/id of the stack to delete. May be simple or canonical
625      * @param pollForCompletion Indicator that polling should be handled in Java vs. in the client
626      * @return A StackInfo object
627      * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception.
628      * @throws MsoCloudSiteNotFound
629      */
630     public StackInfo deleteStack (String tenantId,
631                                   String cloudOwner,
632                                   String cloudSiteId,
633                                   String stackName,
634                                   boolean pollForCompletion) throws MsoException {
635         // Obtain the cloud site information where we will create the stack
636         CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
637                 () -> new MsoCloudSiteNotFound(cloudSiteId));
638         logger.debug("Found: {}", cloudSite.toString());
639
640         // Get a Heat client. They are cached between calls (keyed by tenantId:cloudId)
641         Heat heatClient = null;
642         try {
643             heatClient = getHeatClient (cloudSite, tenantId);
644             if (heatClient != null) {
645                 logger.debug("Found: {}", heatClient.toString());
646             }
647         } catch (MsoTenantNotFound e) {
648             // Tenant doesn't exist, so stack doesn't either
649             logger.debug ("Tenant with id " + tenantId + "not found.", e);
650             return new StackInfo (stackName, HeatStatus.NOTFOUND);
651         } catch (MsoException me) {
652             // Got an Openstack error. Propagate it
653             logger.error("{} {} Openstack Exception on Token request: ", MessageEnum.RA_CONNECTION_EXCEPTION,
654                 ErrorCode.AvailabilityError.getValue(), me);
655             me.addContext (DELETE_STACK);
656             throw me;
657         }
658
659         // OK if stack not found, perform a query first
660         Stack heatStack = queryHeatStack (heatClient, stackName);
661         if (heatStack == null || "DELETE_COMPLETE".equals (heatStack.getStackStatus ())) {
662             // Not found. Return a StackInfo with status NOTFOUND
663             return new StackInfo (stackName, HeatStatus.NOTFOUND);
664         }
665
666         // Delete the stack.
667
668         // Use canonical name "<stack name>/<stack-id>" to delete.
669         // Otherwise, deletion by name returns a 302 redirect.
670         // NOTE: This is specific to the v1 Orchestration API.
671         String canonicalName = heatStack.getStackName () + "/" + heatStack.getId ();
672
673         try {
674             OpenStackRequest <Void> request = null;
675             if(null != heatClient) {
676                 request = heatClient.getStacks ().deleteByName (canonicalName);
677             }
678             else {
679                 logger.debug ("Heat Client is NULL" );
680             }
681
682             executeAndRecordOpenstackRequest (request);
683         } catch (OpenStackResponseException e) {
684             if (e.getStatus () == 404) {
685                 // Not found. We are OK with this. Return a StackInfo with status NOTFOUND
686                 return new StackInfo (stackName, HeatStatus.NOTFOUND);
687             } else {
688                 // Convert the OpenStackResponseException to an MsoOpenstackException
689                 throw heatExceptionToMsoException (e, DELETE_STACK);
690             }
691         } catch (OpenStackConnectException e) {
692             // Error connecting to Openstack instance. Convert to an MsoException
693             throw heatExceptionToMsoException (e, DELETE_STACK);
694         } catch (RuntimeException e) {
695             // Catch-all
696             throw runtimeExceptionToMsoException (e, DELETE_STACK);
697         }
698
699         // Requery the stack for current status.
700         // It will probably still exist with "DELETE_IN_PROGRESS" status.
701         heatStack = queryHeatStack (heatClient, canonicalName);
702
703         if (pollForCompletion) {
704             // Set a timeout on polling
705
706             int pollInterval = Integer.parseInt(this.environment.getProperty(deletePollIntervalProp, "" + deletePollIntervalDefault));
707             int pollTimeout = Integer.parseInt(this.environment.getProperty(deletePollTimeoutProp, "" + deletePollIntervalDefault));
708
709             // When querying by canonical name, Openstack returns DELETE_COMPLETE status
710             // instead of "404" (which would result from query by stack name).
711             while (heatStack != null && !"DELETE_COMPLETE".equals (heatStack.getStackStatus ())) {
712                 logger.debug ("Stack status: {}", heatStack.getStackStatus ());
713
714                 if ("DELETE_FAILED".equals (heatStack.getStackStatus ())) {
715                     // Throw a 'special case' of MsoOpenstackException to report the Heat status
716                     String error = "Stack delete error (" + heatStack.getStackStatus ()
717                                    + "): "
718                                    + heatStack.getStackStatusReason ();
719                     MsoOpenstackException me = new MsoOpenstackException (0, "", error);
720                     me.addContext (DELETE_STACK);
721
722                     // Alarm this condition, stack deletion failed
723
724
725                     throw me;
726                 }
727
728                 if (pollTimeout <= 0) {
729                     logger.error("{} Cloud site: {} Tenant: {} Stack: {} Stack status: {} {} Delete Stack Timeout",
730                         MessageEnum.RA_DELETE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName,
731                         heatStack.getStackStatus(), ErrorCode.AvailabilityError.getValue());
732
733                     // Throw a 'special case' of MsoOpenstackException to report the Heat status
734                     MsoOpenstackException me = new MsoOpenstackException (0, "", "Stack Deletion Timeout");
735                     me.addContext (DELETE_STACK);
736
737                     // Alarm this condition, stack deletion failed
738
739
740                     throw me;
741                 }
742
743                 sleep(pollInterval * 1000L);
744
745                 pollTimeout -= pollInterval;
746                 logger.debug("pollTimeout remaining: {}", pollTimeout);
747
748                 heatStack = queryHeatStack (heatClient, canonicalName);
749             }
750
751             // The stack is gone when this point is reached
752             return new StackInfo (stackName, HeatStatus.NOTFOUND);
753         }
754
755         // Return the current status (if not polling, the delete may still be in progress)
756         StackInfo stackInfo = new StackInfoMapper(heatStack).map();
757         stackInfo.setName (stackName);
758
759         return stackInfo;
760     }
761
762     /**
763      * Query for all stacks in a tenant site. This call will return a List of StackInfo
764      * objects, one for each deployed stack.
765      *
766      * Note that this is limited to a single site. To ensure that a tenant is truly
767      * empty would require looping across all tenant endpoints.
768      *
769      * @param tenantId The Openstack ID of the tenant to query
770      * @param cloudSiteId The cloud identifier (may be a region) in which to query.
771      * @return A List of StackInfo objects
772      * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception.
773      * @throws MsoCloudSiteNotFound
774      */
775     public List <StackInfo> queryAllStacks (String tenantId, String cloudSiteId) throws MsoException {
776         // Obtain the cloud site information where we will create the stack
777         CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
778                 () -> new MsoCloudSiteNotFound(cloudSiteId));
779         // Get a Heat client. They are cached between calls (keyed by tenantId:cloudId)
780         Heat heatClient = getHeatClient (cloudSite, tenantId);
781
782         try {
783             OpenStackRequest <Stacks> request = heatClient.getStacks ().list ();
784             Stacks stacks = executeAndRecordOpenstackRequest (request);
785
786             List <StackInfo> stackList = new ArrayList <> ();
787
788             // Not sure if returns an empty list or null if no stacks exist
789             if (stacks != null) {
790                 for (Stack stack : stacks) {
791                     stackList.add (new StackInfoMapper(stack).map());
792                 }
793             }
794
795             return stackList;
796         } catch (OpenStackResponseException e) {
797             if (e.getStatus () == 404) {
798                 // Not sure if this can happen, but return an empty list
799                 logger.debug ("queryAllStacks - stack not found: ");
800                 return new ArrayList <> ();
801             } else {
802                 // Convert the OpenStackResponseException to an MsoOpenstackException
803                 throw heatExceptionToMsoException (e, QUERY_ALL_STACKS);
804             }
805         } catch (OpenStackConnectException e) {
806             // Error connecting to Openstack instance. Convert to an MsoException
807             throw heatExceptionToMsoException (e, QUERY_ALL_STACKS);
808         } catch (RuntimeException e) {
809             // Catch-all
810             throw runtimeExceptionToMsoException (e, QUERY_ALL_STACKS);
811         }
812     }
813
814     /**
815      * Validate parameters to be passed to Heat template. This method performs
816      * three functions:
817      * 1. Apply default values to parameters which have them defined
818      * 2. Report any required parameters that are missing. This will generate an
819      * exception in the caller, since stack create/update operations would fail.
820      * 3. Report and remove any extraneous parameters. This will allow clients to
821      * pass supersets of parameters and not get errors.
822      *
823      * These functions depend on the HeatTemplate definition from the MSO Catalog DB,
824      * along with the input parameter Map. The output is an updated parameter map.
825      * If the parameters are invalid for the template, an IllegalArgumentException
826      * is thrown.
827      */
828     public Map <String, Object> validateStackParams (Map <String, Object> inputParams,
829                                                      HeatTemplate heatTemplate) {
830         // Check that required parameters have been supplied for this template type
831         StringBuilder missingParams = null;
832         List <String> paramList = new ArrayList <> ();
833
834         // TODO: Enhance DB to support defaults for Heat Template parameters
835
836         for (HeatTemplateParam parm : heatTemplate.getParameters ()) {
837             if (parm.isRequired () && !inputParams.containsKey (parm.getParamName ())) {
838                 if (missingParams == null) {
839                     missingParams = new StringBuilder(parm.getParamName());
840                 } else {
841                     missingParams.append("," + parm.getParamName());
842                 }
843             }
844             paramList.add (parm.getParamName ());
845         }
846         if (missingParams != null) {
847             // Problem - missing one or more required parameters
848             String error = "Missing Required inputs for HEAT Template: " + missingParams;
849             logger.error("{} for HEAT Template {} Missing Required inputs for HEAT Template: {}",
850                 MessageEnum.RA_MISSING_PARAM, ErrorCode.SchemaError.getValue(), missingParams);
851             throw new IllegalArgumentException (error);
852         }
853
854         // Remove any extraneous parameters (don't throw an error)
855         Map <String, Object> updatedParams = new HashMap <> ();
856         List <String> extraParams = new ArrayList <> ();
857
858         for (Entry<String, Object> entry : inputParams.entrySet()) {
859                 if (!paramList.contains(entry.getKey())) {
860                         // This is not a valid parameter for this template
861                         extraParams.add(entry.getKey());
862                 } else {
863                         updatedParams.put(entry.getKey(), entry.getValue());
864                 }
865         }
866
867         if (!extraParams.isEmpty ()) {
868             logger.warn("{} Heat Stack ({}) extra input params received: {} {}", MessageEnum.RA_GENERAL_WARNING,
869                 heatTemplate.getTemplateName(), extraParams, ErrorCode.DataError.getValue());
870         }
871
872         return updatedParams;
873     }
874
875     // ---------------------------------------------------------------
876     // PRIVATE FUNCTIONS FOR USE WITHIN THIS CLASS
877
878     /**
879      * Get a Heat client for the Openstack Identity service.
880      * This requires a 'member'-level userId + password, which will be retrieved from
881      * properties based on the specified cloud Id. The tenant in which to operate
882      * must also be provided.
883      * <p>
884      * On successful authentication, the Heat object will be cached for the
885      * tenantID + cloudId so that it can be reused without reauthenticating with
886      * Openstack every time.
887      *
888      * @return an authenticated Heat object
889      */
890     public Heat getHeatClient (CloudSite cloudSite, String tenantId) throws MsoException {
891         String cloudId = cloudSite.getId();
892         // For DCP/LCP, the region should be the cloudId.
893         String region = cloudSite.getRegionId ();
894
895         // Obtain an MSO token for the tenant
896         CloudIdentity cloudIdentity = cloudSite.getIdentityService();
897         logger.debug("Found: {}", cloudIdentity.toString());
898         MsoTenantUtils tenantUtils = tenantUtilsFactory.getTenantUtilsByServerType(cloudIdentity.getIdentityServerType());
899         String keystoneUrl = tenantUtils.getKeystoneUrl(cloudId, cloudIdentity);
900         logger.debug("keystoneUrl={}", keystoneUrl);
901         String heatUrl = null;
902         String tokenId = null;
903         Calendar expiration = null;
904             try {
905                 if (ServerType.KEYSTONE.equals(cloudIdentity.getIdentityServerType())) {
906                         Keystone keystoneTenantClient = new Keystone (keystoneUrl);
907                         Access access = null;
908
909                         Authentication credentials = authenticationMethodFactory.getAuthenticationFor(cloudIdentity);
910
911                         OpenStackRequest <Access> request = keystoneTenantClient.tokens ()
912                                .authenticate (credentials).withTenantId (tenantId);
913
914                     access = executeAndRecordOpenstackRequest (request);
915
916                         try {
917                                 // Isolate trying to printout the region IDs
918                                 try {
919                   logger.debug("access={}", access.toString());
920                   for (Access.Service service : access.getServiceCatalog()) {
921                                                 List<Access.Service.Endpoint> endpoints = service.getEndpoints();
922                                                 for (Access.Service.Endpoint endpoint : endpoints) {
923                       logger.debug("AIC returned region={}", endpoint.getRegion());
924                   }
925                                         }
926                                 } catch (Exception e) {
927                   logger.debug("Encountered an error trying to printout Access object returned from AIC. {}",
928                       e.getMessage(), e);
929               }
930                             heatUrl = KeystoneUtils.findEndpointURL (access.getServiceCatalog (), "orchestration", region, "public");
931                 logger.debug("heatUrl={}, region={}", heatUrl, region);
932             } catch (RuntimeException e) {
933                             // This comes back for not found (probably an incorrect region ID)
934                             String error = "AIC did not match an orchestration service for: region=" + region + ",cloud=" + cloudIdentity.getIdentityUrl();
935                             throw new MsoAdapterException (error, e);
936                         }
937                         tokenId = access.getToken ().getId ();
938                         expiration = access.getToken ().getExpires ();
939                 } else if (ServerType.KEYSTONE_V3.equals(cloudIdentity.getIdentityServerType())) {
940                         try {
941                                 KeystoneAuthHolder holder = keystoneV3Authentication.getToken(cloudSite, tenantId, "orchestration");
942                                 tokenId = holder.getId();
943                                 expiration = holder.getexpiration();
944                                 heatUrl = holder.getServiceUrl();
945                         } catch (ServiceEndpointNotFoundException e) {
946                                 // This comes back for not found (probably an incorrect region ID)
947                             String error = "cloud did not match an orchestration service for: region=" + region + ",cloud=" + cloudIdentity.getIdentityUrl();
948                             throw new MsoAdapterException (error, e);
949                         }
950                 }
951             } catch (OpenStackResponseException e) {
952             if (e.getStatus () == 401) {
953                 // Authentication error.
954                 String error = "Authentication Failure: tenant=" + tenantId + ",cloud=" + cloudIdentity.getId ();
955
956                 throw new MsoAdapterException (error);
957             } else {
958                 throw keystoneErrorToMsoException (e, TOKEN_AUTH);
959             }
960         } catch (OpenStackConnectException e) {
961             // Connection to Openstack failed
962             MsoIOException me = new MsoIOException (e.getMessage (), e);
963             me.addContext (TOKEN_AUTH);
964             throw me;
965         } catch (RuntimeException e) {
966             // Catch-all
967             throw runtimeExceptionToMsoException (e, TOKEN_AUTH);
968         }
969         Heat heatClient = new Heat (heatUrl);
970         heatClient.token (tokenId);
971         return heatClient;
972     }
973
974     /*
975      * Query for a Heat Stack. This function is needed in several places, so
976      * a common method is useful. This method takes an authenticated Heat Client
977      * (which internally identifies the cloud & tenant to search), and returns
978      * a Stack object if found, Null if not found, or an MsoOpenstackException
979      * if the Openstack API call fails.
980      *
981      * The stack name may be a simple name or a canonical name ("{name}/{id}").
982      * When simple name is used, Openstack always returns a 302 redirect which
983      * results in a 2nd request (to the canonical name). Note that query by
984      * canonical name for a deleted stack returns a Stack object with status
985      * "DELETE_COMPLETE" while query by simple name for a deleted stack returns
986      * HTTP 404.
987      *
988      * @param heatClient an authenticated Heat client
989      *
990      * @param stackName the stack name to query
991      *
992      * @return a Stack object that describes the current stack or null if the
993      * requested stack doesn't exist.
994      *
995      * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
996      */
997     protected Stack queryHeatStack (Heat heatClient, String stackName) throws MsoException {
998         if (stackName == null) {
999             return null;
1000         }
1001         try {
1002             OpenStackRequest <Stack> request = heatClient.getStacks ().byName (stackName);
1003             return executeAndRecordOpenstackRequest (request);
1004         } catch (OpenStackResponseException e) {
1005                 logger.error("Error in Query Stack", e);
1006             if (e.getStatus () == 404) {
1007                 logger.debug ("queryHeatStack - stack not found: {}", stackName);
1008                 return null;
1009             } else {
1010                 // Convert the OpenStackResponseException to an MsoOpenstackException
1011                 throw heatExceptionToMsoException (e, "QueryStack");
1012             }
1013         } catch (OpenStackConnectException e) {
1014             // Connection to Openstack failed
1015             throw heatExceptionToMsoException (e, "QueryAllStack");
1016         }
1017     }
1018
1019
1020         public Map<String, Object> queryStackForOutputs(String cloudSiteId, String cloudOwner,
1021                         String tenantId, String stackName) throws MsoException {
1022       logger.debug("MsoHeatUtils.queryStackForOutputs)");
1023       StackInfo heatStack = this.queryStack(cloudSiteId, cloudOwner, tenantId, stackName);
1024                 if (heatStack == null || heatStack.getStatus() == HeatStatus.NOTFOUND) {
1025                         return null;
1026                 }
1027                 return heatStack.getOutputs();
1028         }
1029
1030         public void copyStringOutputsToInputs(Map<String, Object> inputs,
1031                         Map<String, Object> otherStackOutputs, boolean overWrite) {
1032                 if (inputs == null || otherStackOutputs == null)
1033                         return;
1034                 for (String key : otherStackOutputs.keySet()) {
1035                         if (!inputs.containsKey(key)) {
1036                                 Object obj = otherStackOutputs.get(key);
1037                                 if (obj instanceof String) {
1038                                         inputs.put(key, (String) otherStackOutputs.get(key));
1039                                 } else if (obj instanceof JsonNode ){
1040                                         // This is a bit of mess - but I think it's the least impacting
1041                                         // let's convert it BACK to a string - then it will get converted back later
1042                                         try {
1043                                                 String str = this.convertNode((JsonNode) obj);
1044                                                 inputs.put(key, str);
1045                                         } catch (Exception e) {
1046               logger.debug("DANGER WILL ROBINSON: unable to convert value for JsonNode {} ", key, e);
1047               //effect here is this value will not have been copied to the inputs - and therefore will error out downstream
1048                                         }
1049                                 } else if (obj instanceof java.util.LinkedHashMap) {
1050             logger.debug("LinkedHashMap - this is showing up as a LinkedHashMap instead of JsonNode");
1051             try {
1052                                                 String str = JSON_MAPPER.writeValueAsString(obj);
1053                                                 inputs.put(key, str);
1054                                         } catch (Exception e) {
1055               logger.debug("DANGER WILL ROBINSON: unable to convert value for LinkedHashMap {} ", key, e);
1056           }
1057                                 } else if (obj instanceof Integer) {
1058                                         try {
1059                                                 String str = "" + obj;
1060                                                 inputs.put(key, str);
1061                                         } catch (Exception e) {
1062               logger.debug("DANGER WILL ROBINSON: unable to convert value for Integer {} ", key, e);
1063           }
1064                                 } else {
1065                                         try {
1066                                                 String str = obj.toString();
1067                                                 inputs.put(key, str);
1068                                         } catch (Exception e) {
1069               logger.debug("DANGER WILL ROBINSON: unable to convert value for Other {} ({}) ", key, e.getMessage(), e);
1070               //effect here is this value will not have been copied to the inputs - and therefore will error out downstream
1071                                         }
1072                                 }
1073                         }
1074                 }
1075                 return;
1076         }
1077         public StringBuilder requestToStringBuilder(CreateStackParam stack) {
1078                 StringBuilder sb = new StringBuilder();
1079                 sb.append("Stack:\n");
1080                 sb.append("\tStackName: " + stack.getStackName());
1081                 sb.append("\tTemplateUrl: " + stack.getTemplateUrl());
1082                 sb.append("\tTemplate: " + stack.getTemplate());
1083                 sb.append("\tEnvironment: " + stack.getEnvironment());
1084                 sb.append("\tTimeout: " + stack.getTimeoutMinutes());
1085                 sb.append("\tParameters:\n");
1086                 Map<String, Object> params = stack.getParameters();
1087                 if (params == null || params.size() < 1) {
1088                         sb.append("\nNONE");
1089                 } else {
1090                         for (String key : params.keySet()) {
1091                                 if (params.get(key) instanceof String) {
1092                                         sb.append("\n").append(key).append("=").append((String) params.get(key));
1093                                 } else if (params.get(key) instanceof JsonNode) {
1094                                         String jsonStringOut = this.convertNode((JsonNode)params.get(key));
1095                                         sb.append("\n").append(key).append("=").append(jsonStringOut);
1096                                 } else if (params.get(key) instanceof Integer) {
1097                                         String integerOut = "" + params.get(key);
1098                                         sb.append("\n").append(key).append("=").append(integerOut);
1099
1100                                 } else {
1101                                         try {
1102                                                 String str = params.get(key).toString();
1103                                                 sb.append("\n").append(key).append("=").append(str);
1104                                         } catch (Exception e) {
1105               logger.debug("Exception :", e);
1106           }
1107                                 }
1108                         }
1109                 }
1110                 return sb;
1111         }
1112
1113         private String convertNode(final JsonNode node) {
1114                 try {
1115                         final Object obj = JSON_MAPPER.treeToValue(node, Object.class);
1116                         final String json = JSON_MAPPER.writeValueAsString(obj);
1117                         return json;
1118                 } catch (Exception e) {
1119         logger.debug("Error converting json to string {} ", e.getMessage(), e);
1120     }
1121                 return "[Error converting json to string]";
1122         }
1123
1124
1125         protected StringBuilder getOutputsAsStringBuilder(Stack heatStack) {
1126                 // This should only be used as a utility to print out the stack outputs
1127                 // to the log
1128                 StringBuilder sb = new StringBuilder("");
1129                 if (heatStack == null) {
1130                         sb.append("(heatStack is null)");
1131                         return sb;
1132                 }
1133                 List<Output> outputList = heatStack.getOutputs();
1134                 if (outputList == null || outputList.isEmpty()) {
1135                         sb.append("(outputs is empty)");
1136                         return sb;
1137                 }
1138                 Map<String, Object> outputs = new HashMap<>();
1139                 for (Output outputItem : outputList) {
1140                         outputs.put(outputItem.getOutputKey(), outputItem.getOutputValue());
1141                 }
1142                 int counter = 0;
1143                 sb.append("OUTPUTS:\n");
1144                 for (String key : outputs.keySet()) {
1145                         sb.append("outputs[").append(counter++).append("]: ").append(key).append("=");
1146                         Object obj = outputs.get(key);
1147                         if (obj instanceof String) {
1148                                 sb.append((String) obj).append(" (a string)");
1149                         } else if (obj instanceof JsonNode) {
1150                                 sb.append(this.convertNode((JsonNode) obj)).append(" (a JsonNode)");
1151                         } else if (obj instanceof java.util.LinkedHashMap) {
1152                                 try {
1153                                         String str = JSON_MAPPER.writeValueAsString(obj);
1154                                         sb.append(str).append(" (a java.util.LinkedHashMap)");
1155                                 } catch (Exception e) {
1156             logger.debug("Exception :", e);
1157                                         sb.append("(a LinkedHashMap value that would not convert nicely)");
1158                                 }
1159                         } else if (obj instanceof Integer) {
1160                                 String str = "";
1161                                 try {
1162                                         str = obj.toString() + " (an Integer)\n";
1163                                 } catch (Exception e) {
1164             logger.debug("Exception :", e);
1165             str = "(an Integer unable to call .toString() on)";
1166                                 }
1167                                 sb.append(str);
1168                         } else if (obj instanceof ArrayList) {
1169                                 String str = "";
1170                                 try {
1171                                         str = obj.toString() + " (an ArrayList)";
1172                                 } catch (Exception e) {
1173             logger.debug("Exception :", e);
1174             str = "(an ArrayList unable to call .toString() on?)";
1175                                 }
1176                                 sb.append(str);
1177                         } else if (obj instanceof Boolean) {
1178                                 String str = "";
1179                                 try {
1180                                         str = obj.toString() + " (a Boolean)";
1181                                 } catch (Exception e) {
1182             logger.debug("Exception :", e);
1183             str = "(an Boolean unable to call .toString() on?)";
1184                                 }
1185                                 sb.append(str);
1186                         }
1187                         else {
1188                                 String str = "";
1189                                 try {
1190                                         str = obj.toString() + " (unknown Object type)";
1191                                 } catch (Exception e) {
1192             logger.debug("Exception :", e);
1193             str = "(a value unable to call .toString() on?)";
1194                                 }
1195                                 sb.append(str);
1196                         }
1197                         sb.append("\n");
1198                 }
1199                 sb.append("[END]");
1200                 return sb;
1201         }
1202
1203
1204         public void copyBaseOutputsToInputs(Map<String, Object> inputs,
1205                         Map<String, Object> otherStackOutputs, List<String> paramNames, Map<String, String> aliases) {
1206                 if (inputs == null || otherStackOutputs == null)
1207                         return;
1208                 for (String key : otherStackOutputs.keySet()) {
1209                         if (paramNames != null) {
1210                                 if (!paramNames.contains(key) && !aliases.containsKey(key)) {
1211             logger.debug("\tParameter {} is NOT defined to be in the template - do not copy to inputs", key);
1212             continue;
1213                                 }
1214                                 if (aliases.containsKey(key)) {
1215             logger.debug("Found an alias! Will move {} to {}", key, aliases.get(key));
1216             Object obj = otherStackOutputs.get(key);
1217                                         key = aliases.get(key);
1218                                         otherStackOutputs.put(key, obj);
1219                                 }
1220                         }
1221                         if (!inputs.containsKey(key)) {
1222                                 Object obj = otherStackOutputs.get(key);
1223           logger.debug("\t**Adding {} to inputs (.toString()={}", key, obj.toString());
1224           if (obj instanceof String) {
1225               logger.debug("\t\t**A String");
1226               inputs.put(key, obj);
1227                                 } else if (obj instanceof Integer) {
1228               logger.debug("\t\t**An Integer");
1229               inputs.put(key, obj);
1230                                 } else if (obj instanceof JsonNode) {
1231               logger.debug("\t\t**A JsonNode");
1232               inputs.put(key, obj);
1233                                 } else if (obj instanceof Boolean) {
1234               logger.debug("\t\t**A Boolean");
1235               inputs.put(key, obj);
1236                                 } else if (obj instanceof java.util.LinkedHashMap) {
1237               logger.debug("\t\t**A java.util.LinkedHashMap **");
1238               inputs.put(key, obj);
1239                                 } else if (obj instanceof java.util.ArrayList) {
1240               logger.debug("\t\t**An ArrayList");
1241               inputs.put(key, obj);
1242                                 } else {
1243               logger.debug("\t\t**UNKNOWN OBJECT TYPE");
1244               inputs.put(key, obj);
1245                                 }
1246                         } else {
1247           logger.debug("key={} is already in the inputs - will not overwrite", key);
1248       }
1249                 }
1250                 return;
1251         }
1252
1253         public List<String> convertCdlToArrayList(String cdl) {
1254                 String cdl2 = cdl.trim();
1255                 String cdl3;
1256                 if (cdl2.startsWith("[") && cdl2.endsWith("]")) {
1257                         cdl3 = cdl2.substring(1, cdl2.lastIndexOf("]"));
1258                 } else {
1259                         cdl3 = cdl2;
1260                 }
1261                 return new ArrayList<>(Arrays.asList(cdl3.split(",")));
1262         }
1263
1264     /**
1265      * New with 1707 - this method will convert all the String *values* of the inputs
1266      * to their "actual" object type (based on the param type: in the db - which comes from the template):
1267      * (heat variable type) -> java Object type
1268      * string -> String
1269      * number -> Integer
1270      * json -> marshal object to json
1271      * comma_delimited_list -> ArrayList
1272      * boolean -> Boolean
1273      * if any of the conversions should fail, we will default to adding it to the inputs
1274      * as a string - see if Openstack can handle it.
1275      * Also, will remove any params that are extra.
1276      * Any aliases will be converted to their appropriate name (anyone use this feature?)
1277      * @param inputs - the Map<String, String> of the inputs received on the request
1278      * @param template the HeatTemplate object - this is so we can also verify if the param is valid for this template
1279      * @return HashMap<String, Object> of the inputs, cleaned and converted
1280      */
1281         public Map<String, Object> convertInputMap(Map<String, Object> inputs, HeatTemplate template) {
1282                 HashMap<String, Object> newInputs = new HashMap<>();
1283                 HashMap<String, HeatTemplateParam> params = new HashMap<>();
1284                 HashMap<String, HeatTemplateParam> paramAliases = new HashMap<>();
1285
1286                 if (inputs == null) {
1287         logger.debug("convertInputMap - inputs is null - nothing to do here");
1288         return new HashMap<>();
1289                 }
1290
1291       logger.debug("convertInputMap in MsoHeatUtils called, with {} inputs, and template {}", inputs.size(),
1292           template.getArtifactUuid());
1293       try {
1294           logger.debug(template.toString());
1295           Set<HeatTemplateParam> paramSet = template.getParameters();
1296           logger.debug("paramSet has {} entries", paramSet.size());
1297       } catch (Exception e) {
1298           logger.debug("Exception occurred in convertInputMap {} :", e.getMessage(), e);
1299       }
1300
1301                 for (HeatTemplateParam htp : template.getParameters()) {
1302         logger.debug("Adding {}", htp.getParamName());
1303         params.put(htp.getParamName(), htp);
1304                         if (htp.getParamAlias() != null && !"".equals(htp.getParamAlias())) {
1305           logger.debug("\tFound ALIAS {} -> {}", htp.getParamName(), htp.getParamAlias());
1306           paramAliases.put(htp.getParamAlias(), htp);
1307                         }
1308                 }
1309       logger.debug("Now iterate through the inputs...");
1310       for (String key : inputs.keySet()) {
1311         logger.debug("key={}", key);
1312         boolean alias = false;
1313                         String realName = null;
1314                         if (!params.containsKey(key)) {
1315           logger.debug("{} is not a parameter in the template! - check for an alias", key);
1316           // add check here for an alias
1317                                 if (!paramAliases.containsKey(key)) {
1318             logger.debug("The parameter {} is in the inputs, but it's not a parameter for this template - omit", key);
1319             continue;
1320                                 } else {
1321                                         alias = true;
1322                                         realName = paramAliases.get(key).getParamName();
1323             logger.debug("FOUND AN ALIAS! Will use {} in lieu of give key/alias {}", realName, key);
1324         }
1325                         }
1326                         String type = params.get(key).getParamType();
1327                         if (type == null || "".equals(type)) {
1328           logger.debug("**PARAM_TYPE is null/empty for {}, will default to string", key);
1329           type = "string";
1330                         }
1331         logger.debug("Parameter: {} is of type {}", key, type);
1332         if ("string".equalsIgnoreCase(type)) {
1333                                 // Easiest!
1334                                 String str = inputs.get(key) != null ? inputs.get(key).toString() : null;
1335                                 if (alias)
1336                                         newInputs.put(realName, str);
1337                                 else
1338                                         newInputs.put(key, str);
1339                         } else if ("number".equalsIgnoreCase(type)) {
1340                                 String integerString = inputs.get(key) != null ? inputs.get(key).toString() : null;
1341                                 Integer anInteger = null;
1342                                 try {
1343                                         anInteger = Integer.parseInt(integerString);
1344                                 } catch (Exception e) {
1345             logger.debug("Unable to convert {} to an integer!!", integerString, e);
1346             anInteger = null;
1347                                 }
1348                                 if (anInteger != null) {
1349                                         if (alias)
1350                                                 newInputs.put(realName, anInteger);
1351                                         else
1352                                                 newInputs.put(key, anInteger);
1353                                 }
1354                                 else {
1355                                         if (alias)
1356                                                 newInputs.put(realName, integerString);
1357                                         else
1358                                                 newInputs.put(key, integerString);
1359                                 }
1360                         } else if ("json".equalsIgnoreCase(type)) {
1361                                 Object jsonObj = inputs.get(key);
1362                                 Object json;
1363                                 try {
1364                                         if (jsonObj instanceof String) {
1365                                                 json = JSON_MAPPER.readTree(jsonObj.toString());
1366                                         } else {
1367                                                 //will already marshal to json without intervention
1368                                                 json = jsonObj;
1369                                         }
1370                                 } catch (IOException e) {
1371                                         logger.error("failed to map to json, directly converting to string instead", e);
1372                                         json = jsonObj.toString();
1373                                 }
1374                         if (alias)
1375                                 newInputs.put(realName, json);
1376                         else
1377                                 newInputs.put(key, json);
1378                         } else if ("comma_delimited_list".equalsIgnoreCase(type)) {
1379                                 String commaSeparated = inputs.get(key) != null ? inputs.get(key).toString() : null;
1380                                 try {
1381                                         List<String> anArrayList = this.convertCdlToArrayList(commaSeparated);
1382                                         if (alias)
1383                                                 newInputs.put(realName, anArrayList);
1384                                         else
1385                                                 newInputs.put(key, anArrayList);
1386                                 } catch (Exception e) {
1387             logger.debug("Unable to convert {} to an ArrayList!!", commaSeparated, e);
1388             if (alias)
1389                                                 newInputs.put(realName, commaSeparated);
1390                                         else
1391                                                 newInputs.put(key, commaSeparated);
1392                                 }
1393                         } else if ("boolean".equalsIgnoreCase(type)) {
1394                                 String booleanString = inputs.get(key) != null ? inputs.get(key).toString() : null;
1395                                 Boolean aBool = Boolean.valueOf(booleanString);
1396                                 if (alias)
1397                                         newInputs.put(realName, aBool);
1398                                 else
1399                                         newInputs.put(key, aBool);
1400                         } else {
1401                                 // it's null or something undefined - just add it back as a String
1402                                 String str = inputs.get(key).toString();
1403                                 if (alias)
1404                                         newInputs.put(realName, str);
1405                                 else
1406                                         newInputs.put(key, str);
1407                         }
1408                 }
1409                 return newInputs;
1410         }
1411
1412         /*
1413          * This helpful method added for Valet
1414          */
1415         public String getCloudSiteKeystoneUrl(String cloudSiteId) throws MsoCloudSiteNotFound {
1416                 String keystone_url = null;
1417                 try {
1418                         CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(() -> new MsoCloudSiteNotFound(cloudSiteId));
1419                         CloudIdentity cloudIdentity = cloudSite.getIdentityService();
1420                         keystone_url = cloudIdentity.getIdentityUrl();
1421                 } catch (Exception e) {
1422                         throw new MsoCloudSiteNotFound(cloudSiteId);
1423                 }
1424                 if (keystone_url == null || keystone_url.isEmpty()) {
1425                         throw new MsoCloudSiteNotFound(cloudSiteId);
1426                 }
1427                 return keystone_url;
1428         }
1429
1430         /*
1431          * Create a string suitable for being dumped to a debug log that creates a
1432          * pseudo-JSON request dumping what's being sent to Openstack API in the create or update request
1433          */
1434
1435         private String printStackRequest(String tenantId,
1436                         Map<String, Object> heatFiles,
1437                         Map<String, Object> nestedTemplates,
1438                         String environment,
1439                         Map<String, Object> inputs,
1440                         String vfModuleName,
1441                         String template,
1442                         int timeoutMinutes,
1443                         boolean backout,
1444                         String cloudSiteId) {
1445                 StringBuilder sb = new StringBuilder();
1446                 sb.append("CREATE STACK REQUEST (formatted for readability)\n");
1447                 sb.append("tenant=" + tenantId + ", cloud=" + cloudSiteId);
1448                 sb.append("{\n");
1449                 sb.append("  \"stack_name\": \"" + vfModuleName + "\",\n");
1450                 sb.append("  \"disable_rollback\": " + backout + ",\n");
1451                 sb.append("  \"timeout_mins\": " + timeoutMinutes + ",\n");
1452                 sb.append("  \"template\": {\n");
1453                 sb.append(template);
1454                 sb.append("  },\n");
1455                 sb.append("  \"environment\": {\n");
1456                 if (environment == null)
1457                         sb.append("<none>");
1458                 else
1459                         sb.append(environment);
1460                 sb.append("  },\n");
1461                 sb.append("  \"files\": {\n");
1462                 int filesCounter = 0;
1463                 if (heatFiles != null) {
1464                         for (String key : heatFiles.keySet()) {
1465                                 filesCounter++;
1466                                 if (filesCounter > 1) {
1467                                         sb.append(",\n");
1468                                 }
1469                                 sb.append("    \"" + key + "\": {\n");
1470                                 sb.append(heatFiles.get(key).toString() + "\n    }");
1471                         }
1472                 }
1473                 if (nestedTemplates != null) {
1474                         for (String key : nestedTemplates.keySet()) {
1475                                 filesCounter++;
1476                                 if (filesCounter > 1) {
1477                                         sb.append(",\n");
1478                                 }
1479                                 sb.append("    \"" + key + "\": {\n");
1480                                 sb.append(nestedTemplates.get(key).toString() + "\n    }");
1481                         }
1482                 }
1483                 sb.append("\n  },\n");
1484                 sb.append("  \"parameters\": {\n");
1485                 int paramCounter = 0;
1486                 for (String name : inputs.keySet()) {
1487                         paramCounter++;
1488                         if (paramCounter > 1) {
1489                                 sb.append(",\n");
1490                         }
1491                         Object o = inputs.get(name);
1492                         if (o instanceof java.lang.String) {
1493                                 sb.append("    \"" + name + "\": \"" + inputs.get(name).toString() + "\"");
1494                         } else if (o instanceof Integer) {
1495                                 sb.append("    \"" + name + "\": " + inputs.get(name).toString() );
1496                         } else if (o instanceof ArrayList) {
1497                                 sb.append("    \"" + name + "\": " + inputs.get(name).toString() );
1498                         } else if (o instanceof Boolean) {
1499                                 sb.append("    \"" + name + "\": " + inputs.get(name).toString() );
1500                         } else {
1501                                 sb.append("    \"" + name + "\": " + "\"(there was an issue trying to dump this value...)\"" );
1502                         }
1503                 }
1504                 sb.append("\n  }\n}\n");
1505
1506                 return sb.toString();
1507         }
1508
1509         /*******************************************************************************
1510      *
1511      * Methods (and associated utilities) to implement the VduPlugin interface
1512      *
1513      *******************************************************************************/
1514
1515     /**
1516      * VduPlugin interface for instantiate function.
1517      *
1518      * Translate the VduPlugin parameters to the corresponding 'createStack' parameters,
1519      * and then invoke the existing function.
1520      */
1521     @Override
1522         public VduInstance instantiateVdu (
1523                         CloudInfo cloudInfo,
1524                         String instanceName,
1525                         Map<String,Object> inputs,
1526                         VduModelInfo vduModel,
1527                         boolean rollbackOnFailure)
1528         throws VduException
1529     {
1530         String cloudSiteId = cloudInfo.getCloudSiteId();
1531         String cloudOwner = cloudInfo.getCloudOwner();
1532         String tenantId = cloudInfo.getTenantId();
1533
1534         // Translate the VDU ModelInformation structure to that which is needed for
1535         // creating the Heat stack.  Loop through the artifacts, looking specifically
1536         // for MAIN_TEMPLATE and ENVIRONMENT.  Any other artifact will
1537         // be attached as a FILE.
1538         String heatTemplate = null;
1539         Map<String,Object> nestedTemplates = new HashMap<>();
1540         Map<String,Object> files = new HashMap<>();
1541         String heatEnvironment = null;
1542
1543         for (VduArtifact vduArtifact: vduModel.getArtifacts()) {
1544                 if (vduArtifact.getType() == ArtifactType.MAIN_TEMPLATE) {
1545                         heatTemplate = new String(vduArtifact.getContent());
1546                 }
1547                 else if (vduArtifact.getType() == ArtifactType.NESTED_TEMPLATE) {
1548                         nestedTemplates.put(vduArtifact.getName(), new String(vduArtifact.getContent()));
1549                 }
1550                 else if (vduArtifact.getType() == ArtifactType.ENVIRONMENT) {
1551                         heatEnvironment = new String(vduArtifact.getContent());
1552                 }
1553         }
1554
1555         try {
1556             StackInfo stackInfo = createStack (cloudSiteId,
1557                     cloudOwner,
1558                     tenantId,
1559                     instanceName,
1560                     vduModel,
1561                     heatTemplate,
1562                     inputs,
1563                     true,       // poll for completion
1564                     vduModel.getTimeoutMinutes(),
1565                     heatEnvironment,
1566                     nestedTemplates,
1567                     files,
1568                     rollbackOnFailure);
1569
1570             // Populate a vduInstance from the StackInfo
1571                 return stackInfoToVduInstance(stackInfo);
1572         }
1573         catch (Exception e) {
1574                 throw new VduException ("MsoHeatUtils (instantiateVDU): createStack Exception", e);
1575         }
1576     }
1577
1578
1579     /**
1580      * VduPlugin interface for query function.
1581      */
1582     @Override
1583         public VduInstance queryVdu (CloudInfo cloudInfo, String instanceId)
1584         throws VduException
1585     {
1586         String cloudSiteId = cloudInfo.getCloudSiteId();
1587         String cloudOwner = cloudInfo.getCloudOwner();
1588         String tenantId = cloudInfo.getTenantId();
1589
1590         try {
1591                 // Query the Cloudify Deployment object and  populate a VduInstance
1592                 StackInfo stackInfo = queryStack (cloudSiteId, cloudOwner, tenantId, instanceId);
1593
1594                 return stackInfoToVduInstance(stackInfo);
1595         }
1596         catch (Exception e) {
1597                 throw new VduException ("MsoHeatUtile (queryVdu): queryStack Exception ", e);
1598         }
1599     }
1600
1601
1602     /**
1603      * VduPlugin interface for delete function.
1604      */
1605     @Override
1606         public VduInstance deleteVdu (CloudInfo cloudInfo, String instanceId, int timeoutMinutes)
1607         throws VduException
1608     {
1609         String cloudSiteId = cloudInfo.getCloudSiteId();
1610         String cloudOwner = cloudInfo.getCloudOwner();
1611         String tenantId = cloudInfo.getTenantId();
1612
1613         try {
1614                 // Delete the Heat stack
1615                 StackInfo stackInfo = deleteStack (tenantId, cloudOwner, cloudSiteId, instanceId, true);
1616
1617                 // Populate a VduInstance based on the deleted Cloudify Deployment object
1618                 VduInstance vduInstance = stackInfoToVduInstance(stackInfo);
1619
1620                 // Override return state to DELETED (HeatUtils sets to NOTFOUND)
1621                 vduInstance.getStatus().setState(VduStateType.DELETED);
1622
1623                 return vduInstance;
1624         }
1625         catch (Exception e) {
1626                 throw new VduException ("Delete VDU Exception", e);
1627         }
1628     }
1629
1630
1631     /**
1632      * VduPlugin interface for update function.
1633      *
1634      * Update is currently not supported in the MsoHeatUtils implementation of VduPlugin.
1635      * Just return a VduException.
1636      *
1637      */
1638     @Override
1639         public VduInstance updateVdu (
1640                         CloudInfo cloudInfo,
1641                         String instanceId,
1642                         Map<String,Object> inputs,
1643                         VduModelInfo vduModel,
1644                         boolean rollbackOnFailure)
1645         throws VduException
1646     {
1647         throw new VduException ("MsoHeatUtils: updateVdu interface not supported");
1648     }
1649
1650
1651     /*
1652      * Convert the local DeploymentInfo object (Cloudify-specific) to a generic VduInstance object
1653      */
1654     protected VduInstance stackInfoToVduInstance (StackInfo stackInfo)
1655     {
1656         VduInstance vduInstance = new VduInstance();
1657
1658         // The full canonical name as the instance UUID
1659         vduInstance.setVduInstanceId(stackInfo.getCanonicalName());
1660         vduInstance.setVduInstanceName(stackInfo.getName());
1661
1662         // Copy inputs and outputs
1663         vduInstance.setInputs(stackInfo.getParameters());
1664         vduInstance.setOutputs(stackInfo.getOutputs());
1665
1666         // Translate the status elements
1667         vduInstance.setStatus(stackStatusToVduStatus (stackInfo));
1668
1669         return vduInstance;
1670     }
1671
1672     private VduStatus stackStatusToVduStatus (StackInfo stackInfo)
1673     {
1674         VduStatus vduStatus = new VduStatus();
1675
1676         // Map the status fields to more generic VduStatus.
1677         // There are lots of HeatStatus values, so this is a bit long...
1678         HeatStatus heatStatus = stackInfo.getStatus();
1679         String statusMessage = stackInfo.getStatusMessage();
1680
1681         if (heatStatus == HeatStatus.INIT  ||  heatStatus == HeatStatus.BUILDING) {
1682                 vduStatus.setState(VduStateType.INSTANTIATING);
1683                 vduStatus.setLastAction((new PluginAction ("create", "in_progress", statusMessage)));
1684         }
1685         else if (heatStatus == HeatStatus.NOTFOUND) {
1686                 vduStatus.setState(VduStateType.NOTFOUND);
1687         }
1688         else if (heatStatus == HeatStatus.CREATED) {
1689                 vduStatus.setState(VduStateType.INSTANTIATED);
1690                 vduStatus.setLastAction((new PluginAction ("create", "complete", statusMessage)));
1691         }
1692         else if (heatStatus == HeatStatus.UPDATED) {
1693                 vduStatus.setState(VduStateType.INSTANTIATED);
1694                 vduStatus.setLastAction((new PluginAction ("update", "complete", statusMessage)));
1695         }
1696         else if (heatStatus == HeatStatus.UPDATING) {
1697                 vduStatus.setState(VduStateType.UPDATING);
1698                 vduStatus.setLastAction((new PluginAction ("update", "in_progress", statusMessage)));
1699         }
1700         else if (heatStatus == HeatStatus.DELETING) {
1701                 vduStatus.setState(VduStateType.DELETING);
1702                 vduStatus.setLastAction((new PluginAction ("delete", "in_progress", statusMessage)));
1703         }
1704         else if (heatStatus == HeatStatus.FAILED) {
1705                 vduStatus.setState(VduStateType.FAILED);
1706                 vduStatus.setErrorMessage(stackInfo.getStatusMessage());
1707         } else {
1708                 vduStatus.setState(VduStateType.UNKNOWN);
1709         }
1710
1711         return vduStatus;
1712     }
1713     
1714         public Resources queryStackResources(String cloudSiteId, String tenantId, String stackName, int nestedDepth) throws MsoException {
1715                 CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId)
1716                                 .orElseThrow(() -> new MsoCloudSiteNotFound(cloudSiteId));
1717                 Heat heatClient = getHeatClient(cloudSite, tenantId);
1718                 OpenStackRequest<Resources> request = heatClient.getResources().listResources(stackName).queryParam("nested_depth", nestedDepth);
1719                 return executeAndRecordOpenstackRequest(request);
1720         }
1721
1722         public <R> R executeHeatClientRequest(String url, String cloudSiteId, String tenantId, Class<R> returnType) throws MsoException {
1723                 CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId)
1724                                 .orElseThrow(() -> new MsoCloudSiteNotFound(cloudSiteId));
1725                 Heat heatClient = getHeatClient(cloudSite, tenantId);
1726                 OpenStackRequest<R> request = heatClient.get(url, returnType);
1727                 return executeAndRecordOpenstackRequest(request);
1728         }
1729
1730     protected void sleep(long time) {
1731         try {
1732             Thread.sleep(time);
1733         } catch (InterruptedException e) {
1734             logger.debug ("Thread interrupted while sleeping", e);
1735             Thread.currentThread().interrupt();
1736         }
1737     }
1738
1739 }