Initial OpenECOMP MSO commit
[so.git] / adapters / mso-adapter-utils / src / main / java / org / openecomp / mso / openstack / utils / MsoHeatUtils.java
1 /*-
2  * ============LICENSE_START=======================================================
3  * OPENECOMP - MSO
4  * ================================================================================
5  * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  * 
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  * 
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END=========================================================
19  */
20
21 package org.openecomp.mso.openstack.utils;
22
23 import java.io.Serializable;
24 import java.util.ArrayList;
25 import java.util.Calendar;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29
30 import org.codehaus.jackson.map.ObjectMapper;
31 import org.codehaus.jackson.JsonNode;
32 import org.codehaus.jackson.JsonParseException;
33
34 import org.openecomp.mso.cloud.CloudConfig;
35 import org.openecomp.mso.cloud.CloudConfigFactory;
36 import org.openecomp.mso.cloud.CloudIdentity;
37 import org.openecomp.mso.cloud.CloudSite;
38 import org.openecomp.mso.db.catalog.beans.HeatTemplate;
39 import org.openecomp.mso.db.catalog.beans.HeatTemplateParam;
40 import org.openecomp.mso.logger.MessageEnum;
41 import org.openecomp.mso.logger.MsoAlarmLogger;
42 import org.openecomp.mso.logger.MsoLogger;
43 import org.openecomp.mso.openstack.beans.HeatStatus;
44 import org.openecomp.mso.openstack.beans.StackInfo;
45 import org.openecomp.mso.openstack.exceptions.MsoAdapterException;
46 import org.openecomp.mso.openstack.exceptions.MsoCloudSiteNotFound;
47 import org.openecomp.mso.openstack.exceptions.MsoException;
48 import org.openecomp.mso.openstack.exceptions.MsoIOException;
49 import org.openecomp.mso.openstack.exceptions.MsoOpenstackException;
50 import org.openecomp.mso.openstack.exceptions.MsoStackAlreadyExists;
51 import org.openecomp.mso.openstack.exceptions.MsoTenantNotFound;
52 import org.openecomp.mso.properties.MsoJavaProperties;
53 import org.openecomp.mso.properties.MsoPropertiesException;
54 import org.openecomp.mso.properties.MsoPropertiesFactory;
55 import com.woorea.openstack.base.client.OpenStackConnectException;
56 import com.woorea.openstack.base.client.OpenStackRequest;
57 import com.woorea.openstack.base.client.OpenStackResponseException;
58 import com.woorea.openstack.heat.Heat;
59 import com.woorea.openstack.heat.model.CreateStackParam;
60 import com.woorea.openstack.heat.model.Stack;
61 import com.woorea.openstack.heat.model.Stacks;
62 import com.woorea.openstack.keystone.Keystone;
63 import com.woorea.openstack.keystone.model.Access;
64 import com.woorea.openstack.keystone.model.Authentication;
65 import com.woorea.openstack.keystone.utils.KeystoneUtils;
66
67 public class MsoHeatUtils extends MsoCommonUtils {
68
69         private MsoPropertiesFactory msoPropertiesFactory;
70
71         private CloudConfigFactory cloudConfigFactory;
72         
73     private static final String TOKEN_AUTH = "TokenAuth";
74
75     private static final String QUERY_ALL_STACKS = "QueryAllStacks";
76
77     private static final String DELETE_STACK = "DeleteStack";
78
79     private static final String HEAT_ERROR = "HeatError";
80
81     private static final String CREATE_STACK = "CreateStack";
82
83     // Cache Heat Clients statically. Since there is just one MSO user, there is no
84     // benefit to re-authentication on every request (or across different flows). The
85     // token will be used until it expires.
86     //
87     // The cache key is "tenantId:cloudId"
88     private static Map <String, HeatCacheEntry> heatClientCache = new HashMap <String, HeatCacheEntry> ();
89
90     // Fetch cloud configuration each time (may be cached in CloudConfig class)
91     protected CloudConfig cloudConfig;
92
93     private static MsoLogger LOGGER = MsoLogger.getMsoLogger (MsoLogger.Catalog.RA);
94
95     protected MsoJavaProperties msoProps = null;
96
97     // Properties names and variables (with default values)
98     protected String createPollIntervalProp = "ecomp.mso.adapters.heat.create.pollInterval";
99     private String deletePollIntervalProp = "ecomp.mso.adapters.heat.delete.pollInterval";
100     private String deletePollTimeoutProp = "ecomp.mso.adapters.heat.delete.pollTimeout";
101
102     protected int createPollIntervalDefault = 15;
103     private int deletePollIntervalDefault = 15;
104     private int deletePollTimeoutDefault = 300;
105     private String msoPropID;
106     
107     private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
108
109     /**
110      * This constructor MUST be used ONLY in the JUNIT tests, not for real code.
111      * The MsoPropertiesFactory will be added by EJB injection.
112      *
113      * @param msoPropID ID of the mso pro config as defined in web.xml
114      * @param msoPropFactory The mso properties factory instanciated by EJB injection
115      * @param cloudConfFactory the Cloud Config instantiated by EJB injection
116      */
117     public MsoHeatUtils (String msoPropID, MsoPropertiesFactory msoPropFactory, CloudConfigFactory cloudConfFactory) {
118         msoPropertiesFactory = msoPropFactory;
119         cloudConfigFactory = cloudConfFactory;
120         this.msoPropID = msoPropID;
121         // Dynamically get properties each time (in case reloaded).
122
123         try {
124                         msoProps = msoPropertiesFactory.getMsoJavaProperties (msoPropID);
125                 } catch (MsoPropertiesException e) {
126                         LOGGER.error (MessageEnum.LOAD_PROPERTIES_FAIL, "Unknown. Mso Properties ID not found in cache: " + msoPropID, "", "", MsoLogger.ErrorCode.DataError, "Exception - Mso Properties ID not found in cache", e);
127                 }
128         cloudConfig = cloudConfigFactory.getCloudConfig ();
129         LOGGER.debug("MsoHeatUtils:" + msoPropID);
130         
131     }
132
133
134     /**
135      * keep this old method signature here to maintain backwards compatibility. keep others as well.
136      * this method does not include environment, files, or heatFiles
137      */
138     public StackInfo createStack (String cloudSiteId,
139                                   String tenantId,
140                                   String stackName,
141                                   String heatTemplate,
142                                   Map <String, ? extends Object> 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                                  tenantId,
148                                  stackName,
149                                  heatTemplate,
150                                  stackInputs,
151                                  pollForCompletion,
152                                  timeoutMinutes,
153                                  null,
154                                  null,
155                                  null,
156                                  true);
157     }
158
159     // This method has environment, but not files or heatFiles
160     public StackInfo createStack (String cloudSiteId,
161                                   String tenantId,
162                                   String stackName,
163                                   String heatTemplate,
164                                   Map <String, ? extends Object> stackInputs,
165                                   boolean pollForCompletion,
166                                   int timeoutMinutes,
167                                   String environment) throws MsoException {
168         // Just call the new method with the files/heatFiles variables set to null
169         return this.createStack (cloudSiteId,
170                                  tenantId,
171                                  stackName,
172                                  heatTemplate,
173                                  stackInputs,
174                                  pollForCompletion,
175                                  timeoutMinutes,
176                                  environment,
177                                  null,
178                                  null,
179                                  true);
180     }
181
182     // This method has environment and files, but not heatFiles.
183     public StackInfo createStack (String cloudSiteId,
184                                   String tenantId,
185                                   String stackName,
186                                   String heatTemplate,
187                                   Map <String, ? extends Object> stackInputs,
188                                   boolean pollForCompletion,
189                                   int timeoutMinutes,
190                                   String environment,
191                                   Map <String, Object> files) throws MsoException {
192         return this.createStack (cloudSiteId,
193                                  tenantId,
194                                  stackName,
195                                  heatTemplate,
196                                  stackInputs,
197                                  pollForCompletion,
198                                  timeoutMinutes,
199                                  environment,
200                                  files,
201                                  null,
202                                  true);
203     }
204     
205     // This method has environment, files, heatfiles
206     public StackInfo createStack (String cloudSiteId,
207                                   String tenantId,
208                                   String stackName,
209                                   String heatTemplate,
210                                   Map <String, ? extends Object> stackInputs,
211                                   boolean pollForCompletion,
212                                   int timeoutMinutes,
213                                   String environment,
214                                   Map <String, Object> files,
215                                   Map <String, Object> heatFiles) throws MsoException {
216         return this.createStack (cloudSiteId,
217                                  tenantId,
218                                  stackName,
219                                  heatTemplate,
220                                  stackInputs,
221                                  pollForCompletion,
222                                  timeoutMinutes,
223                                  environment,
224                                  files,
225                                  heatFiles,
226                                  true);
227     }
228
229     /**
230      * Create a new Stack in the specified cloud location and tenant. The Heat template
231      * and parameter map are passed in as arguments, along with the cloud access credentials.
232      * It is expected that parameters have been validated and contain at minimum the required
233      * parameters for the given template with no extra (undefined) parameters..
234      *
235      * The Stack name supplied by the caller must be unique in the scope of this tenant.
236      * However, it should also be globally unique, as it will be the identifier for the
237      * resource going forward in Inventory. This latter is managed by the higher levels
238      * invoking this function.
239      *
240      * The caller may choose to let this function poll Openstack for completion of the
241      * stack creation, or may handle polling itself via separate calls to query the status.
242      * In either case, a StackInfo object will be returned containing the current status.
243      * When polling is enabled, a status of CREATED is expected. When not polling, a
244      * status of BUILDING is expected.
245      *
246      * An error will be thrown if the requested Stack already exists in the specified
247      * Tenant and Cloud.
248      *
249      * For 1510 - add "environment", "files" (nested templates), and "heatFiles" (get_files) as
250      * parameters for createStack. If environment is non-null, it will be added to the stack.
251      * The nested templates and get_file entries both end up being added to the "files" on the
252      * stack. We must combine them before we add them to the stack if they're both non-null.
253      *
254      * @param cloudSiteId The cloud (may be a region) in which to create the stack.
255      * @param tenantId The Openstack ID of the tenant in which to create the Stack
256      * @param stackName The name of the stack to create
257      * @param stackTemplate The Heat template
258      * @param stackInputs A map of key/value inputs
259      * @param pollForCompletion Indicator that polling should be handled in Java vs. in the client
260      * @param environment An optional yaml-format string to specify environmental parameters
261      * @param files a Map<String, Object> that lists the child template IDs (file is the string, object is an int of
262      *        Template id)
263      * @param heatFiles a Map<String, Object> that lists the get_file entries (fileName, fileBody)
264      * @param backout Donot delete stack on create Failure - defaulted to True
265      * @return A StackInfo object
266      * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception.
267      */
268
269     @SuppressWarnings("unchecked")
270     public StackInfo createStack (String cloudSiteId,
271                                   String tenantId,
272                                   String stackName,
273                                   String heatTemplate,
274                                   Map <String, ? extends Object> stackInputs,
275                                   boolean pollForCompletion,
276                                   int timeoutMinutes,
277                                   String environment,
278                                   Map <String, Object> files,
279                                   Map <String, Object> heatFiles,
280                                   boolean backout) throws MsoException {
281         // Create local variables checking to see if we have an environment, nested, get_files
282         // Could later add some checks to see if it's valid.
283         boolean haveEnvtVariable = true;
284         if (environment == null || "".equalsIgnoreCase (environment.trim ())) {
285             haveEnvtVariable = false;
286             LOGGER.debug ("createStack called with no environment variable");
287         } else {
288             LOGGER.debug ("createStack called with an environment variable: " + environment);
289         }
290
291         boolean haveFiles = true;
292         if (files == null || files.isEmpty ()) {
293             haveFiles = false;
294             LOGGER.debug ("createStack called with no files / child template ids");
295         } else {
296             LOGGER.debug ("createStack called with " + files.size () + " files / child template ids");
297         }
298
299         boolean haveHeatFiles = true;
300         if (heatFiles == null || heatFiles.isEmpty ()) {
301             haveHeatFiles = false;
302             LOGGER.debug ("createStack called with no heatFiles");
303         } else {
304             LOGGER.debug ("createStack called with " + heatFiles.size () + " heatFiles");
305         }
306
307         // Obtain the cloud site information where we will create the stack
308         CloudSite cloudSite = cloudConfig.getCloudSite (cloudSiteId);
309         if (cloudSite == null) {
310             throw new MsoCloudSiteNotFound (cloudSiteId);
311         }
312         // Get a Heat client. They are cached between calls (keyed by tenantId:cloudId)
313         // This could throw MsoTenantNotFound or MsoOpenstackException (both propagated)
314         Heat heatClient = getHeatClient (cloudSite, tenantId);
315
316         LOGGER.debug ("Ready to Create Stack (" + heatTemplate + ") with input params: " + stackInputs);
317
318         // Build up the stack to create
319         // Disable auto-rollback, because error reason is lost. Always rollback in the code.
320         CreateStackParam stack = new CreateStackParam ();
321         stack.setStackName (stackName);
322         stack.setTimeoutMinutes (timeoutMinutes);
323         stack.setParameters ((Map <String, Object>) stackInputs);
324         stack.setTemplate (heatTemplate);
325         stack.setDisableRollback (true);
326         // TJM New for PO Adapter - add envt variable
327         if (haveEnvtVariable) {
328             LOGGER.debug ("Found an environment variable - value: " + environment);
329             stack.setEnvironment (environment);
330         }
331         // Now handle nested templates or get_files - have to combine if we have both
332         // as they're both treated as "files:" on the stack.
333         if (haveFiles && haveHeatFiles) {
334             // Let's do this here - not in the bean
335             LOGGER.debug ("Found files AND heatFiles - combine and add!");
336             Map <String, Object> combinedFiles = new HashMap <String, Object> ();
337             for (String keyString : files.keySet ()) {
338                 combinedFiles.put (keyString, files.get (keyString));
339             }
340             for (String keyString : heatFiles.keySet ()) {
341                 combinedFiles.put (keyString, heatFiles.get (keyString));
342             }
343             stack.setFiles (combinedFiles);
344         } else {
345             // Handle if we only have one or neither:
346             if (haveFiles) {
347                 LOGGER.debug ("Found files - adding to stack");
348                 stack.setFiles (files);
349             }
350             if (haveHeatFiles) {
351                 LOGGER.debug ("Found heatFiles - adding to stack");
352                 // the setFiles was modified to handle adding the entries
353                 stack.setFiles (heatFiles);
354             }
355         }
356
357         Stack heatStack = null;
358         try {
359             // Execute the actual Openstack command to create the Heat stack
360             OpenStackRequest <Stack> request = heatClient.getStacks ().create (stack);
361             // Begin X-Auth-User
362             // Obtain an MSO token for the tenant
363             CloudIdentity cloudIdentity = cloudSite.getIdentityService ();
364             // cloudIdentity.getMsoId(), cloudIdentity.getMsoPass()
365             //req
366             request.header ("X-Auth-User", cloudIdentity.getMsoId ());
367             request.header ("X-Auth-Key", cloudIdentity.getMsoPass ());
368             LOGGER.debug ("headers added, about to executeAndRecordOpenstackRequest");
369             LOGGER.debug(this.requestToStringBuilder(stack).toString());
370             // END - try to fix X-Auth-User
371             heatStack = executeAndRecordOpenstackRequest (request, msoProps);
372         } catch (OpenStackResponseException e) {
373             // Since this came on the 'Create Stack' command, nothing was changed
374             // in the cloud. Return the error as an exception.
375             if (e.getStatus () == 409) {
376                 // Stack already exists. Return a specific error for this case
377                 MsoStackAlreadyExists me = new MsoStackAlreadyExists (stackName, tenantId, cloudSiteId);
378                 me.addContext (CREATE_STACK);
379                 throw me;
380             } else {
381                 // Convert the OpenStackResponseException to an MsoOpenstackException
382                 LOGGER.debug("ERROR STATUS = " + e.getStatus() + ",\n" + e.getMessage() + "\n" + e.getLocalizedMessage());
383                 throw heatExceptionToMsoException (e, CREATE_STACK);
384             }
385         } catch (OpenStackConnectException e) {
386             // Error connecting to Openstack instance. Convert to an MsoException
387             throw heatExceptionToMsoException (e, CREATE_STACK);
388         } catch (RuntimeException e) {
389             // Catch-all
390             throw runtimeExceptionToMsoException (e, CREATE_STACK);
391         }
392
393         // Subsequent access by the canonical name "<stack name>/<stack-id>".
394         // Otherwise, simple query by name returns a 302 redirect.
395         // NOTE: This is specific to the v1 Orchestration API.
396         String canonicalName = stackName + "/" + heatStack.getId ();
397
398         // If client has requested a final response, poll for stack completion
399         if (pollForCompletion) {
400             // Set a time limit on overall polling.
401             // Use the resource (template) timeout for Openstack (expressed in minutes)
402             // and add one poll interval to give Openstack a chance to fail on its own.
403             int createPollInterval = msoProps.getIntProperty (createPollIntervalProp, createPollIntervalDefault);
404             int pollTimeout = (timeoutMinutes * 60) + createPollInterval;
405             // New 1610 - poll on delete if we rollback - use same values for now
406             int deletePollInterval = createPollInterval;
407             int deletePollTimeout = pollTimeout;
408             boolean createTimedOut = false;
409             StringBuilder stackErrorStatusReason = new StringBuilder("");
410             LOGGER.debug("createPollInterval=" + createPollInterval + ", pollTimeout=" + pollTimeout);
411
412             while (true) {
413                 try {
414                     heatStack = queryHeatStack (heatClient, canonicalName);
415                     LOGGER.debug (heatStack.getStackStatus () + " (" + canonicalName + ")");
416
417                     if ("CREATE_IN_PROGRESS".equals (heatStack.getStackStatus ())) {
418                         // Stack creation is still running.
419                         // Sleep and try again unless timeout has been reached
420                         if (pollTimeout <= 0) {
421                             // Note that this should not occur, since there is a timeout specified
422                             // in the Openstack call.
423                             LOGGER.error (MessageEnum.RA_CREATE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName, heatStack.getStackStatus (), "", "", MsoLogger.ErrorCode.AvailabilityError, "Create stack timeout");
424                             createTimedOut = true;
425                             break;
426                         }
427                         try {
428                             Thread.sleep (createPollInterval * 1000L);
429                         } catch (InterruptedException e) {
430                             LOGGER.debug ("Thread interrupted while sleeping", e);
431                         }
432
433                         pollTimeout -= createPollInterval;
434                                 LOGGER.debug("pollTimeout remaining: " + pollTimeout);
435                     } else {
436                         //save off the status & reason msg before we attempt delete
437                         stackErrorStatusReason.append("Stack error (" + heatStack.getStackStatus() + "): " + heatStack.getStackStatusReason());
438                         break;
439                     }
440                 } catch (MsoException me) {
441                         // Cannot query the stack status. Something is wrong.
442                         // Try to roll back the stack
443                         if (!backout)
444                         {
445                                 LOGGER.warn(MessageEnum.RA_CREATE_STACK_ERR, "Create Stack errored, stack deletion suppressed", "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Exception in Create Stack, stack deletion suppressed");
446                         }       
447                         else
448                         {
449                                 try {
450                                         LOGGER.debug("Create Stack error - unable to query for stack status - attempting to delete stack: " + canonicalName + " - This will likely fail and/or we won't be able to query to see if delete worked");
451                                         OpenStackRequest <Void> request = heatClient.getStacks ().deleteByName (canonicalName);
452                                         executeAndRecordOpenstackRequest (request, msoProps);
453                                         // this may be a waste of time - if we just got an exception trying to query the stack - we'll just
454                                         // get another one, n'est-ce pas? 
455                                         boolean deleted = false;
456                                         while (!deleted) {
457                                                 try {
458                                                         heatStack = queryHeatStack(heatClient, canonicalName);
459                                                         if (heatStack != null) {
460                                                         LOGGER.debug(heatStack.getStackStatus());
461                                                         if ("DELETE_IN_PROGRESS".equals(heatStack.getStackStatus())) {
462                                                                 if (deletePollTimeout <= 0) {
463                                                                         LOGGER.error (MessageEnum.RA_CREATE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName, 
464                                                                                         heatStack.getStackStatus (), "", "", MsoLogger.ErrorCode.AvailabilityError, 
465                                                                                         "Rollback: DELETE stack timeout");
466                                                                         break;
467                                                                 } else {
468                                                                         try {
469                                                                                 Thread.sleep(deletePollInterval * 1000L);
470                                                                         } catch (InterruptedException ie) {
471                                                                                 LOGGER.debug("Thread interrupted while sleeping", ie);
472                                                                         }
473                                                                         deletePollTimeout -= deletePollInterval;
474                                                                 }
475                                                         } else if ("DELETE_COMPLETE".equals(heatStack.getStackStatus())){
476                                                                 LOGGER.debug("DELETE_COMPLETE for " + canonicalName);
477                                                                 deleted = true;
478                                                                 continue;
479                                                         } else {
480                                                                 //got a status other than DELETE_IN_PROGRESS or DELETE_COMPLETE - so break and evaluate
481                                                                 break;
482                                                         }
483                                                 } else {
484                                                         // assume if we can't find it - it's deleted
485                                                         LOGGER.debug("heatStack returned null - assume the stack " + canonicalName + " has been deleted");
486                                                         deleted = true;
487                                                         continue;
488                                                         }
489                                                         
490                                                 } catch (Exception e3) {
491                                                         // Just log this one. We will report the original exception.
492                                                         LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "Create Stack: Nested exception rolling back stack: " + e3, "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Create Stack: Nested exception rolling back stack on error on query");
493                                                         
494                                                 }
495                                         }
496                                 } catch (Exception e2) {
497                                         // Just log this one. We will report the original exception.
498                                         LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "Create Stack: Nested exception rolling back stack: " + e2, "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Create Stack: Nested exception rolling back stack");
499                                 }
500                         }
501
502                     // Propagate the original exception from Stack Query.
503                     me.addContext (CREATE_STACK);
504                     throw me;
505                 }
506             }
507
508             if (!"CREATE_COMPLETE".equals (heatStack.getStackStatus ())) {
509                 LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "Create Stack error:  Polling complete with non-success status: "
510                               + heatStack.getStackStatus () + ", " + heatStack.getStackStatusReason (), "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Create Stack error");
511
512                 // Rollback the stack creation, since it is in an indeterminate state.
513                 if (!backout)
514                 {
515                         LOGGER.warn(MessageEnum.RA_CREATE_STACK_ERR, "Create Stack errored, stack deletion suppressed", "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Create Stack error, stack deletion suppressed");
516                 }
517                 else
518                 {
519                         try {
520                                 LOGGER.debug("Create Stack errored - attempting to DELETE stack: " + canonicalName);
521                                 LOGGER.debug("deletePollInterval=" + deletePollInterval + ", deletePollTimeout=" + deletePollTimeout);
522                                 OpenStackRequest <Void> request = heatClient.getStacks ().deleteByName (canonicalName);
523                                 executeAndRecordOpenstackRequest (request, msoProps);
524                                 boolean deleted = false;
525                                 while (!deleted) {
526                                         try {
527                                                 heatStack = queryHeatStack(heatClient, canonicalName);
528                                                 if (heatStack != null) {
529                                                         LOGGER.debug(heatStack.getStackStatus() + " (" + canonicalName + ")");
530                                                         if ("DELETE_IN_PROGRESS".equals(heatStack.getStackStatus())) {
531                                                                 if (deletePollTimeout <= 0) {
532                                                                         LOGGER.error (MessageEnum.RA_CREATE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName, 
533                                                                                         heatStack.getStackStatus (), "", "", MsoLogger.ErrorCode.AvailabilityError, 
534                                                                                         "Rollback: DELETE stack timeout");
535                                                                         break;
536                                                                 } else {
537                                                                         try {
538                                                                                 Thread.sleep(deletePollInterval * 1000L);
539                                                                         } catch (InterruptedException ie) {
540                                                                                 LOGGER.debug("Thread interrupted while sleeping", ie);
541                                                                         }
542                                                                         deletePollTimeout -= deletePollInterval;
543                                                                         LOGGER.debug("deletePollTimeout remaining: " + deletePollTimeout);
544                                                                 }
545                                                         } else if ("DELETE_COMPLETE".equals(heatStack.getStackStatus())){
546                                                                 LOGGER.debug("DELETE_COMPLETE for " + canonicalName);
547                                                                 deleted = true;
548                                                                 continue;
549                                                         } else if ("DELETE_FAILED".equals(heatStack.getStackStatus())) {
550                                                                 // Warn about this (?) - but still throw the original exception
551                                                                 LOGGER.warn(MessageEnum.RA_CREATE_STACK_ERR, "Create Stack errored, stack deletion FAILED", "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Create Stack error, stack deletion FAILED");
552                                                                 LOGGER.debug("Stack deletion FAILED on a rollback of a create - " + canonicalName + ", status=" + heatStack.getStackStatus() + ", reason=" + heatStack.getStackStatusReason());
553                                                                 break;
554                                                         } else {
555                                                                 //got a status other than DELETE_IN_PROGRESS or DELETE_COMPLETE - so break and evaluate
556                                                                 break;
557                                                         }
558                                                 } else {
559                                                         // assume if we can't find it - it's deleted
560                                                         LOGGER.debug("heatStack returned null - assume the stack " + canonicalName + " has been deleted");
561                                                         deleted = true;
562                                                         continue;
563                                                 }
564                                                 
565                                         } catch (MsoException me2) {
566                                                 // We got an exception on the delete - don't throw this exception - throw the original - just log.
567                                                 LOGGER.debug("Exception thrown trying to delete " + canonicalName + " on a create->rollback: " + me2.getContextMessage());
568                                                 LOGGER.warn(MessageEnum.RA_CREATE_STACK_ERR, "Create Stack errored, then stack deletion FAILED - exception thrown", "", "", MsoLogger.ErrorCode.BusinessProcesssError, me2.getContextMessage());
569                                         }
570                                         
571                                 } // end while !deleted
572                                 StringBuilder errorContextMessage = null;
573                                 if (createTimedOut) {
574                                         errorContextMessage = new StringBuilder("Stack Creation Timeout");
575                                 } else {
576                                         errorContextMessage  = stackErrorStatusReason;
577                                 }
578                                 if (deleted) {
579                                         errorContextMessage.append(" - stack successfully deleted");
580                                 } else {
581                                         errorContextMessage.append(" - encountered an error trying to delete the stack");
582                                 }
583 //                              MsoOpenstackException me = new MsoOpenstackException(0, "", stackErrorStatusReason.toString());
584  //                             me.addContext(CREATE_STACK);
585   //                            alarmLogger.sendAlarm(HEAT_ERROR, MsoAlarmLogger.CRITICAL, me.getContextMessage());
586    //                           throw me;
587                         } catch (Exception e2) {
588                                 // shouldn't happen - but handle
589                                 LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "Create Stack: Nested exception rolling back stack: " + e2, "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Exception in Create Stack: rolling back stack");
590                         }
591                 } 
592                 MsoOpenstackException me = new MsoOpenstackException(0, "", stackErrorStatusReason.toString());
593                 me.addContext(CREATE_STACK);
594                 alarmLogger.sendAlarm(HEAT_ERROR, MsoAlarmLogger.CRITICAL, me.getContextMessage());
595                 throw me;
596             }
597
598         } else {
599             // Get initial status, since it will have been null after the create.
600             heatStack = queryHeatStack (heatClient, canonicalName);
601             LOGGER.debug (heatStack.getStackStatus ());
602         }
603
604         return new StackInfo (heatStack);
605     }
606
607     /**
608      * Query for a single stack (by Name) in a tenant. This call will always return a
609      * StackInfo object. If the stack does not exist, an "empty" StackInfo will be
610      * returned - containing only the stack name and a status of NOTFOUND.
611      *
612      * @param tenantId The Openstack ID of the tenant in which to query
613      * @param cloudSiteId The cloud identifier (may be a region) in which to query
614      * @param stackName The name of the stack to query (may be simple or canonical)
615      * @return A StackInfo object
616      * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception.
617      */
618     public StackInfo queryStack (String cloudSiteId, String tenantId, String stackName) throws MsoException {
619         LOGGER.debug ("Query HEAT stack: " + stackName + " in tenant " + tenantId);
620
621         // Obtain the cloud site information where we will create the stack
622         CloudSite cloudSite = cloudConfig.getCloudSite (cloudSiteId);
623         if (cloudSite == null) {
624             throw new MsoCloudSiteNotFound (cloudSiteId);
625         }
626
627         // Get a Heat client. They are cached between calls (keyed by tenantId:cloudId)
628         Heat heatClient = null;
629         try {
630             heatClient = getHeatClient (cloudSite, tenantId);
631         } catch (MsoTenantNotFound e) {
632             // Tenant doesn't exist, so stack doesn't either
633             LOGGER.debug ("Tenant with id " + tenantId + "not found.", e);
634             return new StackInfo (stackName, HeatStatus.NOTFOUND, null, null);
635         } catch (MsoException me) {
636             // Got an Openstack error. Propagate it
637             LOGGER.error (MessageEnum.RA_CONNECTION_EXCEPTION, "OpenStack", "Openstack Exception on Token request: " + me, "Openstack", "", MsoLogger.ErrorCode.AvailabilityError, "Connection Exception");
638             me.addContext ("QueryStack");
639             throw me;
640         }
641
642         // Query the Stack.
643         // An MsoException will propagate transparently to the caller.
644         Stack heatStack = queryHeatStack (heatClient, stackName);
645
646         if (heatStack == null) {
647             // Stack does not exist. Return a StackInfo with status NOTFOUND
648             StackInfo stackInfo = new StackInfo (stackName, HeatStatus.NOTFOUND, null, null);
649             return stackInfo;
650         }
651
652         return new StackInfo (heatStack);
653     }
654
655     /**
656      * Delete a stack (by Name/ID) in a tenant. If the stack is not found, it will be
657      * considered a successful deletion. The return value is a StackInfo object which
658      * contains the current stack status.
659      *
660      * The client may choose to let the adapter poll Openstack for completion of the
661      * stack deletion, or may handle polling itself via separate query calls. In either
662      * case, a StackInfo object will be returned. When polling is enabled, a final
663      * status of NOTFOUND is expected. When not polling, a status of DELETING is expected.
664      *
665      * There is no rollback from a successful stack deletion. A deletion failure will
666      * also result in an undefined stack state - the components may or may not have been
667      * all or partially deleted, so the resulting stack must be considered invalid.
668      *
669      * @param tenantId The Openstack ID of the tenant in which to perform the delete
670      * @param cloudSiteId The cloud identifier (may be a region) from which to delete the stack.
671      * @param stackName The name/id of the stack to delete. May be simple or canonical
672      * @param pollForCompletion Indicator that polling should be handled in Java vs. in the client
673      * @return A StackInfo object
674      * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception.
675      * @throws MsoCloudSiteNotFound
676      */
677     public StackInfo deleteStack (String tenantId,
678                                   String cloudSiteId,
679                                   String stackName,
680                                   boolean pollForCompletion) throws MsoException {
681         // Obtain the cloud site information where we will create the stack
682         CloudSite cloudSite = cloudConfig.getCloudSite (cloudSiteId);
683         if (cloudSite == null) {
684             throw new MsoCloudSiteNotFound (cloudSiteId);
685         }
686
687         // Get a Heat client. They are cached between calls (keyed by tenantId:cloudId)
688         Heat heatClient = null;
689         try {
690             heatClient = getHeatClient (cloudSite, tenantId);
691         } catch (MsoTenantNotFound e) {
692             // Tenant doesn't exist, so stack doesn't either
693             LOGGER.debug ("Tenant with id " + tenantId + "not found.", e);
694             return new StackInfo (stackName, HeatStatus.NOTFOUND, null, null);
695         } catch (MsoException me) {
696             // Got an Openstack error. Propagate it
697             LOGGER.error (MessageEnum.RA_CONNECTION_EXCEPTION, "Openstack", "Openstack Exception on Token request: " + me, "Openstack", "", MsoLogger.ErrorCode.AvailabilityError, "Connection Exception");
698             me.addContext (DELETE_STACK);
699             throw me;
700         }
701
702         // OK if stack not found, perform a query first
703         Stack heatStack = queryHeatStack (heatClient, stackName);
704         if (heatStack == null || "DELETE_COMPLETE".equals (heatStack.getStackStatus ())) {
705             // Not found. Return a StackInfo with status NOTFOUND
706             return new StackInfo (stackName, HeatStatus.NOTFOUND, null, null);
707         }
708
709         // Delete the stack.
710
711         // Use canonical name "<stack name>/<stack-id>" to delete.
712         // Otherwise, deletion by name returns a 302 redirect.
713         // NOTE: This is specific to the v1 Orchestration API.
714         String canonicalName = heatStack.getStackName () + "/" + heatStack.getId ();
715
716         try {
717             OpenStackRequest <Void> request = heatClient.getStacks ().deleteByName (canonicalName);
718             executeAndRecordOpenstackRequest (request, msoProps);
719         } catch (OpenStackResponseException e) {
720             if (e.getStatus () == 404) {
721                 // Not found. We are OK with this. Return a StackInfo with status NOTFOUND
722                 return new StackInfo (stackName, HeatStatus.NOTFOUND, null, null);
723             } else {
724                 // Convert the OpenStackResponseException to an MsoOpenstackException
725                 throw heatExceptionToMsoException (e, DELETE_STACK);
726             }
727         } catch (OpenStackConnectException e) {
728             // Error connecting to Openstack instance. Convert to an MsoException
729             throw heatExceptionToMsoException (e, DELETE_STACK);
730         } catch (RuntimeException e) {
731             // Catch-all
732             throw runtimeExceptionToMsoException (e, DELETE_STACK);
733         }
734
735         // Requery the stack for current status.
736         // It will probably still exist with "DELETE_IN_PROGRESS" status.
737         heatStack = queryHeatStack (heatClient, canonicalName);
738
739         if (pollForCompletion) {
740             // Set a timeout on polling
741             int pollInterval = msoProps.getIntProperty (deletePollIntervalProp, deletePollIntervalDefault);
742             int pollTimeout = msoProps.getIntProperty (deletePollTimeoutProp, deletePollTimeoutDefault);
743
744             // When querying by canonical name, Openstack returns DELETE_COMPLETE status
745             // instead of "404" (which would result from query by stack name).
746             while (heatStack != null && !"DELETE_COMPLETE".equals (heatStack.getStackStatus ())) {
747                 LOGGER.debug ("Stack status: " + heatStack.getStackStatus ());
748
749                 if ("DELETE_FAILED".equals (heatStack.getStackStatus ())) {
750                     // Throw a 'special case' of MsoOpenstackException to report the Heat status
751                     String error = "Stack delete error (" + heatStack.getStackStatus ()
752                                    + "): "
753                                    + heatStack.getStackStatusReason ();
754                     MsoOpenstackException me = new MsoOpenstackException (0, "", error);
755                     me.addContext (DELETE_STACK);
756
757                     // Alarm this condition, stack deletion failed
758                     alarmLogger.sendAlarm (HEAT_ERROR, MsoAlarmLogger.CRITICAL, me.getContextMessage ());
759
760                     throw me;
761                 }
762
763                 if (pollTimeout <= 0) {
764                     LOGGER.error (MessageEnum.RA_DELETE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName, heatStack.getStackStatus (), "", "", MsoLogger.ErrorCode.AvailabilityError, "Delete Stack Timeout");
765
766                     // Throw a 'special case' of MsoOpenstackException to report the Heat status
767                     MsoOpenstackException me = new MsoOpenstackException (0, "", "Stack Deletion Timeout");
768                     me.addContext (DELETE_STACK);
769
770                     // Alarm this condition, stack deletion failed
771                     alarmLogger.sendAlarm (HEAT_ERROR, MsoAlarmLogger.CRITICAL, me.getContextMessage ());
772
773                     throw me;
774                 }
775
776                 try {
777                     Thread.sleep (pollInterval * 1000L);
778                 } catch (InterruptedException e) {
779                     LOGGER.debug ("Thread interrupted while sleeping", e);
780                 }
781
782                 pollTimeout -= pollInterval;
783
784                 heatStack = queryHeatStack (heatClient, canonicalName);
785             }
786
787             // The stack is gone when this point is reached
788             return new StackInfo (stackName, HeatStatus.NOTFOUND, null, null);
789         }
790
791         // Return the current status (if not polling, the delete may still be in progress)
792         StackInfo stackInfo = new StackInfo (heatStack);
793         stackInfo.setName (stackName);
794
795         return stackInfo;
796     }
797
798     /**
799      * Query for all stacks in a tenant site. This call will return a List of StackInfo
800      * objects, one for each deployed stack.
801      *
802      * Note that this is limited to a single site. To ensure that a tenant is truly
803      * empty would require looping across all tenant endpoints.
804      *
805      * @param tenantId The Openstack ID of the tenant to query
806      * @param cloudSiteId The cloud identifier (may be a region) in which to query.
807      * @return A List of StackInfo objects
808      * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception.
809      * @throws MsoCloudSiteNotFound
810      */
811     public List <StackInfo> queryAllStacks (String tenantId, String cloudSiteId) throws MsoException {
812         // Obtain the cloud site information where we will create the stack
813         CloudSite cloudSite = cloudConfig.getCloudSite (cloudSiteId);
814         if (cloudSite == null) {
815             throw new MsoCloudSiteNotFound (cloudSiteId);
816         }
817
818         // Get a Heat client. They are cached between calls (keyed by tenantId:cloudId)
819         Heat heatClient = getHeatClient (cloudSite, tenantId);
820
821         try {
822             OpenStackRequest <Stacks> request = heatClient.getStacks ().list ();
823             Stacks stacks = executeAndRecordOpenstackRequest (request, msoProps);
824
825             List <StackInfo> stackList = new ArrayList <StackInfo> ();
826
827             // Not sure if returns an empty list or null if no stacks exist
828             if (stacks != null) {
829                 for (Stack stack : stacks) {
830                     stackList.add (new StackInfo (stack));
831                 }
832             }
833
834             return stackList;
835         } catch (OpenStackResponseException e) {
836             if (e.getStatus () == 404) {
837                 // Not sure if this can happen, but return an empty list
838                 LOGGER.debug ("queryAllStacks - stack not found: ");
839                 return new ArrayList <StackInfo> ();
840             } else {
841                 // Convert the OpenStackResponseException to an MsoOpenstackException
842                 throw heatExceptionToMsoException (e, QUERY_ALL_STACKS);
843             }
844         } catch (OpenStackConnectException e) {
845             // Error connecting to Openstack instance. Convert to an MsoException
846             throw heatExceptionToMsoException (e, QUERY_ALL_STACKS);
847         } catch (RuntimeException e) {
848             // Catch-all
849             throw runtimeExceptionToMsoException (e, QUERY_ALL_STACKS);
850         }
851     }
852
853     /**
854      * Validate parameters to be passed to Heat template. This method performs
855      * three functions:
856      * 1. Apply default values to parameters which have them defined
857      * 2. Report any required parameters that are missing. This will generate an
858      * exception in the caller, since stack create/update operations would fail.
859      * 3. Report and remove any extraneous parameters. This will allow clients to
860      * pass supersets of parameters and not get errors.
861      *
862      * These functions depend on the HeatTemplate definition from the MSO Catalog DB,
863      * along with the input parameter Map. The output is an updated parameter map.
864      * If the parameters are invalid for the template, an IllegalArgumentException
865      * is thrown.
866      */
867     public Map <String, Object> validateStackParams (Map <String, Object> inputParams,
868                                                      HeatTemplate heatTemplate) throws IllegalArgumentException {
869         // Check that required parameters have been supplied for this template type
870         String missingParams = null;
871         List <String> paramList = new ArrayList <String> ();
872
873         // TODO: Enhance DB to support defaults for Heat Template parameters
874
875         for (HeatTemplateParam parm : heatTemplate.getParameters ()) {
876             if (parm.isRequired () && !inputParams.containsKey (parm.getParamName ())) {
877                 if (missingParams == null) {
878                     missingParams = parm.getParamName ();
879                 } else {
880                     missingParams += "," + parm.getParamName ();
881                 }
882             }
883             paramList.add (parm.getParamName ());
884         }
885         if (missingParams != null) {
886             // Problem - missing one or more required parameters
887             String error = "Missing Required inputs for HEAT Template: " + missingParams;
888             LOGGER.error (MessageEnum.RA_MISSING_PARAM, missingParams + " for HEAT Template", "", "", MsoLogger.ErrorCode.SchemaError, "Missing Required inputs for HEAT Template: " + missingParams);
889             throw new IllegalArgumentException (error);
890         }
891
892         // Remove any extraneous parameters (don't throw an error)
893         Map <String, Object> updatedParams = new HashMap <String, Object> ();
894         List <String> extraParams = new ArrayList <String> ();
895         for (String key : inputParams.keySet ()) {
896             if (!paramList.contains (key)) {
897                 // This is not a valid parameter for this template
898                 extraParams.add (key);
899             } else {
900                 updatedParams.put (key, inputParams.get (key));
901             }
902         }
903         if (!extraParams.isEmpty ()) {
904             LOGGER.warn (MessageEnum.RA_GENERAL_WARNING, "Heat Stack (" + heatTemplate.getTemplateName ()
905                          + ") extra input params received: "
906                          + extraParams, "", "", MsoLogger.ErrorCode.DataError, "Heat Stack (" + heatTemplate.getTemplateName () + ") extra input params received: "+ extraParams);
907         }
908
909         return updatedParams;
910     }
911
912     // ---------------------------------------------------------------
913     // PRIVATE FUNCTIONS FOR USE WITHIN THIS CLASS
914
915     /**
916      * Get a Heat client for the Openstack Identity service.
917      * This requires a 'member'-level userId + password, which will be retrieved from
918      * properties based on the specified cloud Id. The tenant in which to operate
919      * must also be provided.
920      * <p>
921      * On successful authentication, the Heat object will be cached for the
922      * tenantID + cloudId so that it can be reused without reauthenticating with
923      * Openstack every time.
924      *
925      * @param tenantName
926      * @param cloudId
927      * @return an authenticated Heat object
928      */
929     public Heat getHeatClient (CloudSite cloudSite, String tenantId) throws MsoException {
930         String cloudId = cloudSite.getId ();
931
932         // Check first in the cache of previously authorized clients
933         String cacheKey = cloudId + ":" + tenantId;
934         if (heatClientCache.containsKey (cacheKey)) {
935             if (!heatClientCache.get (cacheKey).isExpired ()) {
936                 LOGGER.debug ("Using Cached HEAT Client for " + cacheKey);
937                 return heatClientCache.get (cacheKey).getHeatClient ();
938             } else {
939                 // Token is expired. Remove it from cache.
940                 heatClientCache.remove (cacheKey);
941                 LOGGER.debug ("Expired Cached HEAT Client for " + cacheKey);
942             }
943         }
944
945         // Obtain an MSO token for the tenant
946         CloudIdentity cloudIdentity = cloudSite.getIdentityService ();
947         String keystoneUrl = cloudIdentity.getKeystoneUrl (cloudId, msoPropID);
948         Keystone keystoneTenantClient = new Keystone (keystoneUrl);
949         Access access = null;
950         try {
951                 Authentication credentials = cloudIdentity.getAuthentication ();
952                         
953                 OpenStackRequest <Access> request = keystoneTenantClient.tokens ()
954                        .authenticate (credentials).withTenantId (tenantId);
955                 
956             access = executeAndRecordOpenstackRequest (request, msoProps);
957         } catch (OpenStackResponseException e) {
958             if (e.getStatus () == 401) {
959                 // Authentication error.
960                 String error = "Authentication Failure: tenant=" + tenantId + ",cloud=" + cloudIdentity.getId ();
961                 alarmLogger.sendAlarm ("MsoAuthenticationError", MsoAlarmLogger.CRITICAL, error);
962                 throw new MsoAdapterException (error);
963             } else {
964                 throw keystoneErrorToMsoException (e, TOKEN_AUTH);
965             }
966         } catch (OpenStackConnectException e) {
967             // Connection to Openstack failed
968             MsoIOException me = new MsoIOException (e.getMessage (), e);
969             me.addContext (TOKEN_AUTH);
970             throw me;
971         } catch (RuntimeException e) {
972             // Catch-all
973             throw runtimeExceptionToMsoException (e, TOKEN_AUTH);
974         }
975
976         // For DCP/LCP, the region should be the cloudId.
977         String region = cloudSite.getRegionId ();
978         String heatUrl = null;
979         try {
980             heatUrl = KeystoneUtils.findEndpointURL (access.getServiceCatalog (), "orchestration", region, "public");
981         } catch (RuntimeException e) {
982             // This comes back for not found (probably an incorrect region ID)
983             String error = "Orchestration service not found: region=" + region + ",cloud=" + cloudIdentity.getId ();
984             alarmLogger.sendAlarm ("MsoConfigurationError", MsoAlarmLogger.CRITICAL, error);
985             throw new MsoAdapterException (error, e);
986         }
987         // This is needed for testing in the MT cloud
988         if ("MT".equals (cloudId)) {
989             heatUrl = heatUrl.replace ("controller", "mtdnj02bh01wt.bvoip.labs.att.com");
990         }
991
992         Heat heatClient = new Heat (heatUrl);
993         heatClient.token (access.getToken ().getId ());
994
995         heatClientCache.put (cacheKey,
996                              new HeatCacheEntry (heatUrl,
997                                                  access.getToken ().getId (),
998                                                  access.getToken ().getExpires ()));
999         LOGGER.debug ("Caching HEAT Client for " + cacheKey);
1000
1001         return heatClient;
1002     }
1003
1004     /**
1005      * Forcibly expire a HEAT client from the cache. This call is for use by
1006      * the KeystoneClient in case where a tenant is deleted. In that case,
1007      * all cached credentials must be purged so that fresh authentication is
1008      * done if a similarly named tenant is re-created.
1009      * <p>
1010      * Note: This is probably only applicable to dev/test environments where
1011      * the same Tenant Name is repeatedly used for creation/deletion.
1012      * <p>
1013      *
1014      * @param tenantName
1015      * @param cloudId
1016      */
1017     public static void expireHeatClient (String tenantId, String cloudId) {
1018         String cacheKey = cloudId + ":" + tenantId;
1019         if (heatClientCache.containsKey (cacheKey)) {
1020             heatClientCache.remove (cacheKey);
1021             LOGGER.debug ("Deleted Cached HEAT Client for " + cacheKey);
1022         }
1023     }
1024
1025     /*
1026      * Query for a Heat Stack. This function is needed in several places, so
1027      * a common method is useful. This method takes an authenticated Heat Client
1028      * (which internally identifies the cloud & tenant to search), and returns
1029      * a Stack object if found, Null if not found, or an MsoOpenstackException
1030      * if the Openstack API call fails.
1031      *
1032      * The stack name may be a simple name or a canonical name ("{name}/{id}").
1033      * When simple name is used, Openstack always returns a 302 redirect which
1034      * results in a 2nd request (to the canonical name). Note that query by
1035      * canonical name for a deleted stack returns a Stack object with status
1036      * "DELETE_COMPLETE" while query by simple name for a deleted stack returns
1037      * HTTP 404.
1038      *
1039      * @param heatClient an authenticated Heat client
1040      *
1041      * @param stackName the stack name to query
1042      *
1043      * @return a Stack object that describes the current stack or null if the
1044      * requested stack doesn't exist.
1045      *
1046      * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
1047      */
1048     protected Stack queryHeatStack (Heat heatClient, String stackName) throws MsoException {
1049         if (stackName == null) {
1050             return null;
1051         }
1052         try {
1053             OpenStackRequest <Stack> request = heatClient.getStacks ().byName (stackName);
1054             return executeAndRecordOpenstackRequest (request, msoProps);
1055         } catch (OpenStackResponseException e) {
1056             if (e.getStatus () == 404) {
1057                 LOGGER.debug ("queryHeatStack - stack not found: " + stackName);
1058                 return null;
1059             } else {
1060                 // Convert the OpenStackResponseException to an MsoOpenstackException
1061                 throw heatExceptionToMsoException (e, "QueryStack");
1062             }
1063         } catch (OpenStackConnectException e) {
1064             // Connection to Openstack failed
1065             throw heatExceptionToMsoException (e, "QueryAllStack");
1066         }
1067     }
1068
1069     /*
1070      * An entry in the Heat Client Cache. It saves the Heat client object
1071      * along with the token expiration. After this interval, this cache
1072      * item will no longer be used.
1073      */
1074     private static class HeatCacheEntry implements Serializable {
1075
1076         private static final long serialVersionUID = 1L;
1077
1078         private String heatUrl;
1079         private String token;
1080         private Calendar expires;
1081
1082         public HeatCacheEntry (String heatUrl, String token, Calendar expires) {
1083             this.heatUrl = heatUrl;
1084             this.token = token;
1085             this.expires = expires;
1086         }
1087
1088         public Heat getHeatClient () {
1089             Heat heatClient = new Heat (heatUrl);
1090             heatClient.token (token);
1091             return heatClient;
1092         }
1093
1094         public boolean isExpired () {
1095             if (expires == null) {
1096                 return true;
1097             }
1098
1099             Calendar now = Calendar.getInstance ();
1100             if (now.after (expires)) {
1101                 return true;
1102             }
1103
1104             return false;
1105         }
1106     }
1107
1108     /**
1109      * Clean up the Heat client cache to remove expired entries.
1110      */
1111     public static void heatCacheCleanup () {
1112         for (String cacheKey : heatClientCache.keySet ()) {
1113             if (heatClientCache.get (cacheKey).isExpired ()) {
1114                 heatClientCache.remove (cacheKey);
1115                 LOGGER.debug ("Cleaned Up Cached Heat Client for " + cacheKey);
1116             }
1117         }
1118     }
1119
1120     /**
1121      * Reset the Heat client cache.
1122      * This may be useful if cached credentials get out of sync.
1123      */
1124     public static void heatCacheReset () {
1125         heatClientCache = new HashMap <String, HeatCacheEntry> ();
1126     }
1127     
1128         public Map<String, Object> queryStackForOutputs(String cloudSiteId,
1129                         String tenantId, String stackName) throws MsoException {
1130                 LOGGER.debug("MsoHeatUtils.queryStackForOutputs)");
1131                 StackInfo heatStack = this.queryStack(cloudSiteId, tenantId, stackName);
1132                 if (heatStack == null || heatStack.getStatus() == HeatStatus.NOTFOUND) {
1133                         return null;
1134                 }
1135                 Map<String, Object> outputs = heatStack.getOutputs();
1136                 return outputs;
1137         }
1138
1139         public void queryAndCopyOutputsToInputs(String cloudSiteId,
1140                         String tenantId, String stackName, Map<String, String> inputs,
1141                         boolean overWrite) throws MsoException {
1142                 LOGGER.debug("MsoHeatUtils.queryAndCopyOutputsToInputs");
1143                 Map<String, Object> outputs = this.queryStackForOutputs(cloudSiteId,
1144                                 tenantId, stackName);
1145                 this.copyStringOutputsToInputs(inputs, outputs, overWrite);
1146                 return;
1147         }
1148
1149         public void copyStringOutputsToInputs(Map<String, String> inputs,
1150                         Map<String, Object> otherStackOutputs, boolean overWrite) {
1151                 if (inputs == null || otherStackOutputs == null)
1152                         return;
1153                 for (String key : otherStackOutputs.keySet()) {
1154                         if (!inputs.containsKey(key)) {
1155                                 Object obj = otherStackOutputs.get(key);
1156                                 if (obj instanceof String) {
1157                                         inputs.put(key, (String) otherStackOutputs.get(key));
1158                                 } else if (obj instanceof JsonNode ){
1159                                         // This is a bit of mess - but I think it's the least impacting
1160                                         // let's convert it BACK to a string - then it will get converted back later
1161                                         try {
1162                                                 String str = this.convertNode((JsonNode) obj);
1163                                                 inputs.put(key, str);
1164                                         } catch (Exception e) {
1165                                                 LOGGER.debug("DANGER WILL ROBINSON: unable to convert value for "+ key);
1166                                                 //effect here is this value will not have been copied to the inputs - and therefore will error out downstream
1167                                         }
1168                                 } else if (obj instanceof java.util.LinkedHashMap) {
1169                                         LOGGER.debug("LinkedHashMap - this is showing up as a LinkedHashMap instead of JsonNode");
1170                                         try {
1171                                                 String str = JSON_MAPPER.writeValueAsString(obj);
1172                                                 inputs.put(key, str);
1173                                         } catch (Exception e) {
1174                                                 LOGGER.debug("DANGER WILL ROBINSON: unable to convert value for "+ key);
1175                                         }
1176                                 } else {
1177                                         // just try to cast it - could be an integer or some such
1178                                         try {
1179                                                 String str = (String) obj;
1180                                                 inputs.put(key, str);
1181                                         } catch (Exception e) {
1182                                                 LOGGER.debug("DANGER WILL ROBINSON: unable to convert value for "+ key);
1183                                                 //effect here is this value will not have been copied to the inputs - and therefore will error out downstream
1184                                         }
1185                                 }
1186                         }
1187                 }
1188                 return;
1189         }
1190         public StringBuilder requestToStringBuilder(CreateStackParam stack) {
1191                 StringBuilder sb = new StringBuilder();
1192                 sb.append("Stack:\n");
1193                 sb.append("\tStackName: " + stack.getStackName());
1194                 sb.append("\tTemplateUrl: " + stack.getTemplateUrl());
1195                 sb.append("\tTemplate: " + stack.getTemplate());
1196                 sb.append("\tEnvironment: " + stack.getEnvironment());
1197                 sb.append("\tTimeout: " + stack.getTimeoutMinutes());
1198                 sb.append("\tParameters:\n");
1199                 Map<String, Object> params = stack.getParameters();
1200                 if (params == null || params.size() < 1) {
1201                         sb.append("\tNONE");
1202                 } else {
1203                         for (String key : params.keySet()) {
1204                                 if (params.get(key) instanceof String) {
1205                                         sb.append("\t" + key + "=" + (String) params.get(key));
1206                                 } else if (params.get(key) instanceof JsonNode) {
1207                                         String jsonStringOut = this.convertNode((JsonNode)params.get(key));
1208                                         sb.append("\t" + key + "=" + jsonStringOut);
1209                                 } else {
1210                                         sb.append("\t" + key + "= [some non-string/non-json]");
1211                                 }
1212                         }
1213                 }
1214                 return sb;
1215         }
1216         
1217         private String convertNode(final JsonNode node) {
1218                 try {
1219                         final Object obj = JSON_MAPPER.treeToValue(node, Object.class);
1220                         final String json = JSON_MAPPER.writeValueAsString(obj);
1221                         return json;
1222                 } catch (JsonParseException jpe) {
1223                         LOGGER.debug("Error converting json to string " + jpe.getMessage());
1224                 } catch (Exception e) {
1225                         LOGGER.debug("Error converting json to string " + e.getMessage());
1226                 }
1227                 return "[Error converting json to string]";
1228         }
1229 }