Merge "Reorder modifiers"
[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  * Copyright (C) 2017 Huawei Technologies Co., Ltd. All rights reserved.
7  * ================================================================================
8  * Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  * 
12  *      http://www.apache.org/licenses/LICENSE-2.0
13  * 
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  * ============LICENSE_END=========================================================
20  */
21
22 package org.openecomp.mso.openstack.utils;
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.openecomp.mso.cloud.CloudConfigFactory;
30 import org.openecomp.mso.cloud.CloudSite;
31 import org.openecomp.mso.logger.MessageEnum;
32 import org.openecomp.mso.logger.MsoLogger;
33 import org.openecomp.mso.openstack.beans.StackInfo;
34 import org.openecomp.mso.openstack.exceptions.MsoCloudSiteNotFound;
35 import org.openecomp.mso.openstack.exceptions.MsoException;
36 import org.openecomp.mso.openstack.exceptions.MsoOpenstackException;
37 import org.openecomp.mso.openstack.exceptions.MsoStackNotFound;
38 import org.openecomp.mso.properties.MsoJavaProperties;
39 import org.openecomp.mso.properties.MsoPropertiesException;
40 import org.openecomp.mso.properties.MsoPropertiesFactory;
41
42 import com.fasterxml.jackson.core.JsonParseException;
43 import com.fasterxml.jackson.databind.JsonNode;
44 import com.fasterxml.jackson.databind.ObjectMapper;
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.Stack.Output;
50 import com.woorea.openstack.heat.model.UpdateStackParam;
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 heatTemplate 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 = getCloudConfigFactory().getCloudConfig().getCloudSite(cloudSiteId).orElseThrow(
197                 () -> new MsoCloudSiteNotFound(cloudSiteId));
198         // Get a Heat client. They are cached between calls (keyed by tenantId:cloudId)
199         // This could throw MsoTenantNotFound or MsoOpenstackException (both propagated)
200         Heat heatClient = getHeatClient (cloudSite, tenantId);
201
202         // Perform a query first to get the current status
203         Stack heatStack = queryHeatStack (heatClient, stackName);
204         if (heatStack == null || "DELETE_COMPLETE".equals (heatStack.getStackStatus ())) {
205             // Not found. Return a StackInfo with status NOTFOUND
206             throw new MsoStackNotFound (stackName, tenantId, cloudSiteId);
207         }
208
209         // Use canonical name "<stack name>/<stack-id>" to update the stack.
210         // Otherwise, update by name returns a 302 redirect.
211         // NOTE: This is specific to the v1 Orchestration API.
212         String canonicalName = heatStack.getStackName () + "/" + heatStack.getId ();
213
214         LOGGER.debug ("Ready to Update Stack (" + canonicalName + ") with input params: " + stackInputs);
215
216         // Build up the stack update parameters
217         // Disable auto-rollback, because error reason is lost. Always rollback in the code.
218         UpdateStackParam stack = new UpdateStackParam ();
219         stack.setTimeoutMinutes (timeoutMinutes);
220         stack.setParameters (stackInputs);
221         stack.setTemplate (heatTemplate);
222         stack.setDisableRollback (true);
223         // TJM add envt to stack
224         if (heatEnvtVariable) {
225             stack.setEnvironment (environment);
226         }
227
228         // Handle nested templates & get_files here. if we have both - must combine
229         // and then add to stack (both are part of "files:" being added to stack)
230         if (haveFiles && haveHeatFiles) {
231             // Let's do this here - not in the bean
232             LOGGER.debug ("Found files AND heatFiles - combine and add!");
233             Map <String, Object> combinedFiles = new HashMap<>();
234             for (String keyString : files.keySet ()) {
235                 combinedFiles.put (keyString, files.get (keyString));
236             }
237             for (String keyString : heatFiles.keySet ()) {
238                 combinedFiles.put (keyString, heatFiles.get (keyString));
239             }
240             stack.setFiles (combinedFiles);
241         } else {
242             // Handle case where we have one or neither
243             if (haveFiles) {
244                 stack.setFiles (files);
245             }
246             if (haveHeatFiles) {
247                 // setFiles method modified to handle adding a map.
248                 stack.setFiles (heatFiles);
249             }
250         }
251
252         try {
253             // Execute the actual Openstack command to update the Heat stack
254             OpenStackRequest <Void> request = heatClient.getStacks ().update (canonicalName, stack);
255             executeAndRecordOpenstackRequest (request, msoProps);
256         } catch (OpenStackBaseException e) {
257             // Since this came on the 'Update Stack' command, nothing was changed
258             // in the cloud. Rethrow the error as an MSO exception.
259             throw heatExceptionToMsoException (e, UPDATE_STACK);
260         } catch (RuntimeException e) {
261             // Catch-all
262             throw runtimeExceptionToMsoException (e, UPDATE_STACK);
263         }
264
265         // If client has requested a final response, poll for stack completion
266         Stack updateStack = null;
267         if (pollForCompletion) {
268             // Set a time limit on overall polling.
269             // Use the resource (template) timeout for Openstack (expressed in minutes)
270             // and add one poll interval to give Openstack a chance to fail on its own.
271             int createPollInterval = msoProps.getIntProperty (createPollIntervalProp, createPollIntervalDefault);
272             int pollTimeout = (timeoutMinutes * 60) + createPollInterval;
273
274             boolean loopAgain = true;
275             while (loopAgain) {
276                 try {
277                     updateStack = queryHeatStack (heatClient, canonicalName);
278                     LOGGER.debug (updateStack.getStackStatus () + " (" + canonicalName + ")");
279                     try {
280                         LOGGER.debug("Current stack " + this.getOutputsAsStringBuilder(heatStack).toString());
281                     } catch (Exception e) {
282                         LOGGER.debug("an error occurred trying to print out the current outputs of the stack", e);
283                     }
284
285
286                     if ("UPDATE_IN_PROGRESS".equals (updateStack.getStackStatus ())) {
287                         // Stack update is still running.
288                         // Sleep and try again unless timeout has been reached
289                         if (pollTimeout <= 0) {
290                             // Note that this should not occur, since there is a timeout specified
291                             // in the Openstack call.
292                                 LOGGER.error (MessageEnum.RA_UPDATE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName, updateStack.getStackStatus(), "", "", MsoLogger.ErrorCode.AvailabilityError, "Update stack timeout");
293                             loopAgain = false;
294                         } else {
295                             try {
296                                 Thread.sleep (createPollInterval * 1000L);
297                             } catch (InterruptedException e) {
298                                 // If we are interrupted, we should stop ASAP.
299                                 loopAgain = false;
300                                 // Set again the interrupted flag
301                                 Thread.currentThread().interrupt();
302                             }
303                         }
304                         pollTimeout -= createPollInterval;
305                         LOGGER.debug("pollTimeout remaining: " + pollTimeout);
306                     } else {
307                         loopAgain = false;
308                     }
309                 } catch (MsoException e) {
310                     // Cannot query the stack. Something is wrong.
311
312                     // TODO: No way to roll back the stack at this point. What to do?
313                     e.addContext (UPDATE_STACK);
314                     throw e;
315                 }
316             }
317
318             if (!"UPDATE_COMPLETE".equals (updateStack.getStackStatus ())) {
319                 LOGGER.error (MessageEnum.RA_UPDATE_STACK_ERR, updateStack.getStackStatus(), updateStack.getStackStatusReason(), "", "", MsoLogger.ErrorCode.DataError, "Update Stack error");
320
321                 // TODO: No way to roll back the stack at this point. What to do?
322                 // Throw a 'special case' of MsoOpenstackException to report the Heat status
323                 MsoOpenstackException me = null;
324                 if ("UPDATE_IN_PROGRESS".equals (updateStack.getStackStatus ())) {
325                     me = new MsoOpenstackException (0, "", "Stack Update Timeout");
326                 } else {
327                     String error = "Stack error (" + updateStack.getStackStatus ()
328                                    + "): "
329                                    + updateStack.getStackStatusReason ();
330                     me = new MsoOpenstackException (0, "", error);
331                 }
332                 me.addContext (UPDATE_STACK);
333                 throw me;
334             }
335
336         } else {
337             // Return the current status.
338             updateStack = queryHeatStack (heatClient, canonicalName);
339             if (updateStack != null) {
340                 LOGGER.debug ("UpdateStack, status = " + updateStack.getStackStatus ());
341             } else {
342                 LOGGER.debug ("UpdateStack, stack not found");
343             }
344         }
345         return new StackInfo (updateStack);
346     }
347     
348         private StringBuilder getOutputsAsStringBuilder(Stack heatStack) {
349                 // This should only be used as a utility to print out the stack outputs
350                 // to the log
351                 StringBuilder sb = new StringBuilder("");
352                 if (heatStack == null) {
353                         sb.append("(heatStack is null)");
354                         return sb;
355                 }
356                 List<Output> outputList = heatStack.getOutputs();
357                 if (outputList == null || outputList.isEmpty()) {
358                         sb.append("(outputs is empty)");
359                         return sb;
360                 }
361                 Map<String, Object> outputs = new HashMap<>();
362                 for (Output outputItem : outputList) {
363                         outputs.put(outputItem.getOutputKey(), outputItem.getOutputValue());
364                 }
365                 int counter = 0;
366                 sb.append("OUTPUTS:\n");
367                 for (String key : outputs.keySet()) {
368                         sb.append("outputs[").append(counter++).append("]: ").append(key).append("=");
369                         Object obj = outputs.get(key);
370                         if (obj instanceof String) {
371                                 sb.append((String) obj).append(" (a string)");
372                         } else if (obj instanceof JsonNode) {
373                                 sb.append(this.convertNode((JsonNode) obj)).append(" (a JsonNode)");
374                         } else if (obj instanceof java.util.LinkedHashMap) {
375                                 try {
376                                         String str = JSON_MAPPER.writeValueAsString(obj);
377                                         sb.append(str).append(" (a java.util.LinkedHashMap)");
378                                 } catch (Exception e) {
379                                         LOGGER.debug("Exception :", e);
380                                         sb.append("(a LinkedHashMap value that would not convert nicely)");
381                                 }                               
382                         } else if (obj instanceof Integer) {
383                                 String str = "";
384                                 try {
385                                         str = obj.toString() + " (an Integer)\n";
386                                 } catch (Exception e) {
387                                         LOGGER.debug("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                                         LOGGER.debug("Exception :", e);
397                                         str = "(an ArrayList unable to call .toString() on?)";
398                                 }
399                                 sb.append(str);
400                         } else if (obj instanceof Boolean) {
401                                 String str = "";
402                                 try {
403                                         str = obj.toString() + " (a Boolean)";
404                                 } catch (Exception e) {
405                                         LOGGER.debug("Exception :", e);
406                                         str = "(an Boolean unable to call .toString() on?)";
407                                 }
408                                 sb.append(str);
409                         }
410                         else {
411                                 String str = "";
412                                 try {
413                                         str = obj.toString() + " (unknown Object type)";
414                                 } catch (Exception e) {
415                                         LOGGER.debug("Exception :", e);
416                                         str = "(a value unable to call .toString() on?)";
417                                 }
418                                 sb.append(str);
419                         }
420                         sb.append("\n");
421                 }
422                 sb.append("[END]");
423                 return sb;
424         }
425         
426         private String convertNode(final JsonNode node) {
427                 try {
428                         final Object obj = JSON_MAPPER.treeToValue(node, Object.class);
429                         final String json = JSON_MAPPER.writeValueAsString(obj);
430                         return json;
431                 } catch (Exception e) {
432                         LOGGER.debug("Error converting json to string " + e.getMessage(), e);
433                 }
434                 return "[Error converting json to string]";
435         }
436         
437 }