Change the header to SO
[so.git] / adapters / mso-adapter-utils / src / main / java / org / openecomp / mso / openstack / utils / MsoHeatUtilsWithUpdate.java
1 /*-
2  * ============LICENSE_START=======================================================
3  * ONAP - SO
4  * ================================================================================
5  * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  * 
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  * 
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END=========================================================
19  */
20
21 package org.openecomp.mso.openstack.utils;
22
23
24 import java.util.ArrayList;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28
29 import org.codehaus.jackson.JsonNode;
30 import org.codehaus.jackson.JsonParseException;
31 import org.codehaus.jackson.map.ObjectMapper;
32
33 import org.openecomp.mso.cloud.CloudConfigFactory;
34 import org.openecomp.mso.cloud.CloudSite;
35 import org.openecomp.mso.logger.MessageEnum;
36 import org.openecomp.mso.logger.MsoLogger;
37 import org.openecomp.mso.openstack.beans.StackInfo;
38 import org.openecomp.mso.openstack.exceptions.MsoCloudSiteNotFound;
39 import org.openecomp.mso.openstack.exceptions.MsoException;
40 import org.openecomp.mso.openstack.exceptions.MsoOpenstackException;
41 import org.openecomp.mso.openstack.exceptions.MsoStackNotFound;
42 import org.openecomp.mso.properties.MsoJavaProperties;
43 import org.openecomp.mso.properties.MsoPropertiesException;
44 import org.openecomp.mso.properties.MsoPropertiesFactory;
45 import com.woorea.openstack.base.client.OpenStackBaseException;
46 import com.woorea.openstack.base.client.OpenStackRequest;
47 import com.woorea.openstack.heat.Heat;
48 import com.woorea.openstack.heat.model.Stack;
49 import com.woorea.openstack.heat.model.UpdateStackParam;
50 import com.woorea.openstack.heat.model.Stack.Output;
51
52 public class MsoHeatUtilsWithUpdate extends MsoHeatUtils {
53
54     private static final String UPDATE_STACK = "UpdateStack";
55     private static MsoLogger LOGGER = MsoLogger.getMsoLogger (MsoLogger.Catalog.RA);
56
57     protected MsoJavaProperties msoProps = null;
58
59     private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
60
61     public MsoHeatUtilsWithUpdate (String msoPropID, MsoPropertiesFactory msoPropertiesFactory, CloudConfigFactory cloudConfFactory) {
62         super (msoPropID,msoPropertiesFactory,cloudConfFactory);
63         
64         try {
65                         msoProps = msoPropertiesFactory.getMsoJavaProperties (msoPropID);
66                 } catch (MsoPropertiesException e) {
67                         LOGGER.error (MessageEnum.LOAD_PROPERTIES_FAIL, "Unknown. Mso Properties ID not found in cache: " + msoPropID, "", "", MsoLogger.ErrorCode.AvailabilityError, "Exception Mso Properties ID not found in cache: " + msoPropID, e);
68                 }
69     }
70     
71     /*
72      * Keep these methods around for backward compatibility
73      */
74
75     public StackInfo updateStack (String cloudSiteId,
76                                   String tenantId,
77                                   String stackName,
78                                   String heatTemplate,
79                                   Map <String, Object> stackInputs,
80                                   boolean pollForCompletion,
81                                   int timeoutMinutes) throws MsoException {
82         // Keeping this method to allow compatibility with no environment or files variable sent. In this case,
83         // simply return the new method with the environment variable set to null.
84         return this.updateStack (cloudSiteId,
85                                  tenantId,
86                                  stackName,
87                                  heatTemplate,
88                                  stackInputs,
89                                  pollForCompletion,
90                                  timeoutMinutes,
91                                  null,
92                                  null,
93                                  null);
94     }
95
96     public StackInfo updateStack (String cloudSiteId,
97                                   String tenantId,
98                                   String stackName,
99                                   String heatTemplate,
100                                   Map <String, Object> stackInputs,
101                                   boolean pollForCompletion,
102                                   int timeoutMinutes,
103                                   String environment) throws MsoException {
104         // Keeping this method to allow compatibility with no environment variable sent. In this case,
105         // simply return the new method with the files variable set to null.
106         return this.updateStack (cloudSiteId,
107                                  tenantId,
108                                  stackName,
109                                  heatTemplate,
110                                  stackInputs,
111                                  pollForCompletion,
112                                  timeoutMinutes,
113                                  environment,
114                                  null,
115                                  null);
116     }
117
118     public StackInfo updateStack (String cloudSiteId,
119                                   String tenantId,
120                                   String stackName,
121                                   String heatTemplate,
122                                   Map <String, Object> stackInputs,
123                                   boolean pollForCompletion,
124                                   int timeoutMinutes,
125                                   String environment,
126                                   Map <String, Object> files) throws MsoException {
127         return this.updateStack (cloudSiteId,
128                                  tenantId,
129                                  stackName,
130                                  heatTemplate,
131                                  stackInputs,
132                                  pollForCompletion,
133                                  timeoutMinutes,
134                                  environment,
135                                  files,
136                                  null);
137     }
138
139     /**
140      * Update a Stack in the specified cloud location and tenant. The Heat template
141      * and parameter map are passed in as arguments, along with the cloud access credentials.
142      * It is expected that parameters have been validated and contain at minimum the required
143      * parameters for the given template with no extra (undefined) parameters..
144      *
145      * The Stack name supplied by the caller must be unique in the scope of this tenant.
146      * However, it should also be globally unique, as it will be the identifier for the
147      * resource going forward in Inventory. This latter is managed by the higher levels
148      * invoking this function.
149      *
150      * The caller may choose to let this function poll Openstack for completion of the
151      * stack creation, or may handle polling itself via separate calls to query the status.
152      * In either case, a StackInfo object will be returned containing the current status.
153      * When polling is enabled, a status of CREATED is expected. When not polling, a
154      * status of BUILDING is expected.
155      *
156      * An error will be thrown if the requested Stack already exists in the specified
157      * Tenant and Cloud.
158      *
159      * @param tenantId The Openstack ID of the tenant in which to create the Stack
160      * @param cloudSiteId The cloud identifier (may be a region) in which to create the tenant.
161      * @param stackName The name of the stack to update
162      * @param stackTemplate The Heat template
163      * @param stackInputs A map of key/value inputs
164      * @param pollForCompletion Indicator that polling should be handled in Java vs. in the client
165      * @param environment An optional yaml-format string to specify environmental parameters
166      * @param files a Map<String, Object> for listing child template IDs
167      * @param heatFiles a Map<String, Object> for listing get_file entries (fileName, fileBody)
168      * @return A StackInfo object
169      * @throws MsoException Thrown if the Openstack API call returns an exception.
170      */
171
172     public StackInfo updateStack (String cloudSiteId,
173                                   String tenantId,
174                                   String stackName,
175                                   String heatTemplate,
176                                   Map <String, Object> stackInputs,
177                                   boolean pollForCompletion,
178                                   int timeoutMinutes,
179                                   String environment,
180                                   Map <String, Object> files,
181                                   Map <String, Object> heatFiles) throws MsoException {
182         boolean heatEnvtVariable = true;
183         if (environment == null || "".equalsIgnoreCase (environment.trim ())) {
184             heatEnvtVariable = false;
185         }
186         boolean haveFiles = true;
187         if (files == null || files.isEmpty ()) {
188             haveFiles = false;
189         }
190         boolean haveHeatFiles = true;
191         if (heatFiles == null || heatFiles.isEmpty ()) {
192             haveHeatFiles = false;
193         }
194
195         // Obtain the cloud site information where we will create the stack
196         CloudSite cloudSite = cloudConfig.getCloudSite (cloudSiteId);
197         if (cloudSite == null) {
198             throw new MsoCloudSiteNotFound (cloudSiteId);
199         }
200         // Get a Heat client. They are cached between calls (keyed by tenantId:cloudId)
201         // This could throw MsoTenantNotFound or MsoOpenstackException (both propagated)
202         Heat heatClient = getHeatClient (cloudSite, tenantId);
203
204         // Perform a query first to get the current status
205         Stack heatStack = queryHeatStack (heatClient, stackName);
206         if (heatStack == null || "DELETE_COMPLETE".equals (heatStack.getStackStatus ())) {
207             // Not found. Return a StackInfo with status NOTFOUND
208             throw new MsoStackNotFound (stackName, tenantId, cloudSiteId);
209         }
210
211         // Use canonical name "<stack name>/<stack-id>" to update the stack.
212         // Otherwise, update by name returns a 302 redirect.
213         // NOTE: This is specific to the v1 Orchestration API.
214         String canonicalName = heatStack.getStackName () + "/" + heatStack.getId ();
215
216         LOGGER.debug ("Ready to Update Stack (" + canonicalName + ") with input params: " + stackInputs);
217
218         // Build up the stack update parameters
219         // Disable auto-rollback, because error reason is lost. Always rollback in the code.
220         UpdateStackParam stack = new UpdateStackParam ();
221         stack.setTimeoutMinutes (timeoutMinutes);
222         stack.setParameters (stackInputs);
223         stack.setTemplate (heatTemplate);
224         stack.setDisableRollback (true);
225         // TJM add envt to stack
226         if (heatEnvtVariable) {
227             stack.setEnvironment (environment);
228         }
229
230         // Handle nested templates & get_files here. if we have both - must combine
231         // and then add to stack (both are part of "files:" being added to stack)
232         if (haveFiles && haveHeatFiles) {
233             // Let's do this here - not in the bean
234             LOGGER.debug ("Found files AND heatFiles - combine and add!");
235             Map <String, Object> combinedFiles = new HashMap <String, Object> ();
236             for (String keyString : files.keySet ()) {
237                 combinedFiles.put (keyString, files.get (keyString));
238             }
239             for (String keyString : heatFiles.keySet ()) {
240                 combinedFiles.put (keyString, heatFiles.get (keyString));
241             }
242             stack.setFiles (combinedFiles);
243         } else {
244             // Handle case where we have one or neither
245             if (haveFiles) {
246                 stack.setFiles (files);
247             }
248             if (haveHeatFiles) {
249                 // setFiles method modified to handle adding a map.
250                 stack.setFiles (heatFiles);
251             }
252         }
253
254         try {
255             // Execute the actual Openstack command to update the Heat stack
256             OpenStackRequest <Void> request = heatClient.getStacks ().update (canonicalName, stack);
257             executeAndRecordOpenstackRequest (request, msoProps);
258         } catch (OpenStackBaseException e) {
259             // Since this came on the 'Update Stack' command, nothing was changed
260             // in the cloud. Rethrow the error as an MSO exception.
261             throw heatExceptionToMsoException (e, UPDATE_STACK);
262         } catch (RuntimeException e) {
263             // Catch-all
264             throw runtimeExceptionToMsoException (e, UPDATE_STACK);
265         }
266
267         // If client has requested a final response, poll for stack completion
268         Stack updateStack = null;
269         if (pollForCompletion) {
270             // Set a time limit on overall polling.
271             // Use the resource (template) timeout for Openstack (expressed in minutes)
272             // and add one poll interval to give Openstack a chance to fail on its own.
273             int createPollInterval = msoProps.getIntProperty (createPollIntervalProp, createPollIntervalDefault);
274             int pollTimeout = (timeoutMinutes * 60) + createPollInterval;
275
276             boolean loopAgain = true;
277             while (loopAgain) {
278                 try {
279                     updateStack = queryHeatStack (heatClient, canonicalName);
280                     LOGGER.debug (updateStack.getStackStatus () + " (" + canonicalName + ")");
281                     try {
282                         LOGGER.debug("Current stack " + this.getOutputsAsStringBuilder(heatStack).toString());
283                     } catch (Exception e) {
284                         LOGGER.debug("an error occurred trying to print out the current outputs of the stack");
285                     }
286
287
288                     if ("UPDATE_IN_PROGRESS".equals (updateStack.getStackStatus ())) {
289                         // Stack update is still running.
290                         // Sleep and try again unless timeout has been reached
291                         if (pollTimeout <= 0) {
292                             // Note that this should not occur, since there is a timeout specified
293                             // in the Openstack call.
294                                 LOGGER.error (MessageEnum.RA_UPDATE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName, updateStack.getStackStatus(), "", "", MsoLogger.ErrorCode.AvailabilityError, "Update stack timeout");
295                             loopAgain = false;
296                         } else {
297                             try {
298                                 Thread.sleep (createPollInterval * 1000L);
299                             } catch (InterruptedException e) {
300                                 // If we are interrupted, we should stop ASAP.
301                                 loopAgain = false;
302                                 // Set again the interrupted flag
303                                 Thread.currentThread().interrupt();
304                             }
305                         }
306                         pollTimeout -= createPollInterval;
307                         LOGGER.debug("pollTimeout remaining: " + pollTimeout);
308                     } else {
309                         loopAgain = false;
310                     }
311                 } catch (MsoException e) {
312                     // Cannot query the stack. Something is wrong.
313
314                     // TODO: No way to roll back the stack at this point. What to do?
315                     e.addContext (UPDATE_STACK);
316                     throw e;
317                 }
318             }
319
320             if (!"UPDATE_COMPLETE".equals (updateStack.getStackStatus ())) {
321                 LOGGER.error (MessageEnum.RA_UPDATE_STACK_ERR, updateStack.getStackStatus(), updateStack.getStackStatusReason(), "", "", MsoLogger.ErrorCode.DataError, "Update Stack error");
322
323                 // TODO: No way to roll back the stack at this point. What to do?
324                 // Throw a 'special case' of MsoOpenstackException to report the Heat status
325                 MsoOpenstackException me = null;
326                 if ("UPDATE_IN_PROGRESS".equals (updateStack.getStackStatus ())) {
327                     me = new MsoOpenstackException (0, "", "Stack Update Timeout");
328                 } else {
329                     String error = "Stack error (" + updateStack.getStackStatus ()
330                                    + "): "
331                                    + updateStack.getStackStatusReason ();
332                     me = new MsoOpenstackException (0, "", error);
333                 }
334                 me.addContext (UPDATE_STACK);
335                 throw me;
336             }
337
338         } else {
339             // Return the current status.
340             updateStack = queryHeatStack (heatClient, canonicalName);
341             if (updateStack != null) {
342                 LOGGER.debug ("UpdateStack, status = " + updateStack.getStackStatus ());
343             } else {
344                 LOGGER.debug ("UpdateStack, stack not found");
345             }
346         }
347         return new StackInfo (updateStack);
348     }
349     
350         private StringBuilder getOutputsAsStringBuilder(Stack heatStack) {
351                 // This should only be used as a utility to print out the stack outputs
352                 // to the log
353                 StringBuilder sb = new StringBuilder("");
354                 if (heatStack == null) {
355                         sb.append("(heatStack is null)");
356                         return sb;
357                 }
358                 List<Output> outputList = heatStack.getOutputs();
359                 if (outputList == null || outputList.isEmpty()) {
360                         sb.append("(outputs is empty)");
361                         return sb;
362                 }
363                 Map<String, Object> outputs = new HashMap<String,Object>();
364                 for (Output outputItem : outputList) {
365                         outputs.put(outputItem.getOutputKey(), outputItem.getOutputValue());
366                 }
367                 int counter = 0;
368                 sb.append("OUTPUTS:\n");
369                 for (String key : outputs.keySet()) {
370                         sb.append("outputs[" + counter++ + "]: " + key + "=");
371                         Object obj = outputs.get(key);
372                         if (obj instanceof String) {
373                                 sb.append((String)obj +" (a string)");
374                         } else if (obj instanceof JsonNode) {
375                                 sb.append(this.convertNode((JsonNode)obj) + " (a JsonNode)");
376                         } else if (obj instanceof java.util.LinkedHashMap) {
377                                 try {
378                                         String str = JSON_MAPPER.writeValueAsString(obj);
379                                         sb.append(str + " (a java.util.LinkedHashMap)");
380                                 } catch (Exception e) {
381                                         sb.append("(a LinkedHashMap value that would not convert nicely)");
382                                 }                               
383                         } else if (obj instanceof Integer) {
384                                 String str = "";
385                                 try {
386                                         str = obj.toString() + " (an Integer)\n";
387                                 } catch (Exception e) {
388                                         str = "(an Integer unable to call .toString() on)";
389                                 }
390                                 sb.append(str);
391                         } else if (obj instanceof ArrayList) {
392                                 String str = "";
393                                 try {
394                                         str = obj.toString() + " (an ArrayList)";
395                                 } catch (Exception e) {
396                                         str = "(an ArrayList unable to call .toString() on?)";
397                                 }
398                                 sb.append(str);
399                         } else if (obj instanceof Boolean) {
400                                 String str = "";
401                                 try {
402                                         str = obj.toString() + " (a Boolean)";
403                                 } catch (Exception e) {
404                                         str = "(an Boolean unable to call .toString() on?)";
405                                 }
406                                 sb.append(str);
407                         }
408                         else {
409                                 String str = "";
410                                 try {
411                                         str = obj.toString() + " (unknown Object type)";
412                                 } catch (Exception e) {
413                                         str = "(a value unable to call .toString() on?)";
414                                 }
415                                 sb.append(str);
416                         }
417                         sb.append("\n");
418                 }
419                 sb.append("[END]");
420                 return sb;
421         }
422         
423         private String convertNode(final JsonNode node) {
424                 try {
425                         final Object obj = JSON_MAPPER.treeToValue(node, Object.class);
426                         final String json = JSON_MAPPER.writeValueAsString(obj);
427                         return json;
428                 } catch (JsonParseException jpe) {
429                         LOGGER.debug("Error converting json to string " + jpe.getMessage());
430                 } catch (Exception e) {
431                         LOGGER.debug("Error converting json to string " + e.getMessage());
432                 }
433                 return "[Error converting json to string]";
434         }
435         
436 }