Removed MsoLogger class
[so.git] / adapters / mso-adapter-utils / src / main / java / org / onap / so / 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  * Modifications Copyright (c) 2019 Samsung
9  * ================================================================================
10  * Licensed under the Apache License, Version 2.0 (the "License");
11  * you may not use this file except in compliance with the License.
12  * You may obtain a copy of the License at
13  * 
14  *      http://www.apache.org/licenses/LICENSE-2.0
15  * 
16  * Unless required by applicable law or agreed to in writing, software
17  * distributed under the License is distributed on an "AS IS" BASIS,
18  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19  * See the License for the specific language governing permissions and
20  * limitations under the License.
21  * ============LICENSE_END=========================================================
22  */
23
24 package org.onap.so.openstack.utils;
25
26
27 import com.fasterxml.jackson.core.type.TypeReference;
28 import com.fasterxml.jackson.databind.JsonNode;
29 import com.fasterxml.jackson.databind.ObjectMapper;
30 import com.woorea.openstack.base.client.OpenStackBaseException;
31 import com.woorea.openstack.base.client.OpenStackRequest;
32 import com.woorea.openstack.heat.Heat;
33 import com.woorea.openstack.heat.model.Stack;
34 import com.woorea.openstack.heat.model.Stack.Output;
35 import com.woorea.openstack.heat.model.UpdateStackParam;
36 import java.io.IOException;
37 import java.util.ArrayList;
38 import java.util.HashMap;
39 import java.util.List;
40 import java.util.Map;
41 import org.onap.so.db.catalog.beans.CloudSite;
42 import org.onap.so.logger.ErrorCode;
43 import org.onap.so.logger.MessageEnum;
44 import org.onap.so.openstack.beans.StackInfo;
45 import org.onap.so.openstack.exceptions.MsoCloudSiteNotFound;
46 import org.onap.so.openstack.exceptions.MsoException;
47 import org.onap.so.openstack.exceptions.MsoOpenstackException;
48 import org.onap.so.openstack.exceptions.MsoStackNotFound;
49 import org.onap.so.openstack.mappers.StackInfoMapper;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52 import org.springframework.beans.factory.annotation.Autowired;
53 import org.springframework.core.env.Environment;
54 import org.springframework.stereotype.Component;
55
56 @Component
57 public class MsoHeatUtilsWithUpdate extends MsoHeatUtils {
58
59     private static final String UPDATE_STACK = "UpdateStack";
60     private static final Logger logger = LoggerFactory.getLogger(MsoHeatUtilsWithUpdate.class);
61
62     private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
63
64     @Autowired
65     private Environment environment;
66     /*
67      * Keep these methods around for backward compatibility
68      */
69
70     public StackInfo updateStack (String cloudSiteId,
71                                   String tenantId,
72                                   String stackName,
73                                   String heatTemplate,
74                                   Map <String, Object> stackInputs,
75                                   boolean pollForCompletion,
76                                   int timeoutMinutes) throws MsoException {
77         // Keeping this method to allow compatibility with no environment or files variable sent. In this case,
78         // simply return the new method with the environment variable set to null.
79         return this.updateStack (cloudSiteId,
80                                  tenantId,
81                                  stackName,
82                                  heatTemplate,
83                                  stackInputs,
84                                  pollForCompletion,
85                                  timeoutMinutes,
86                                  null,
87                                  null,
88                                  null);
89     }
90
91     public StackInfo updateStack (String cloudSiteId,
92                                   String tenantId,
93                                   String stackName,
94                                   String heatTemplate,
95                                   Map <String, Object> stackInputs,
96                                   boolean pollForCompletion,
97                                   int timeoutMinutes,
98                                   String environment) throws MsoException {
99         // Keeping this method to allow compatibility with no environment variable sent. In this case,
100         // simply return the new method with the files variable set to null.
101         return this.updateStack (cloudSiteId,
102                                  tenantId,
103                                  stackName,
104                                  heatTemplate,
105                                  stackInputs,
106                                  pollForCompletion,
107                                  timeoutMinutes,
108                                  environment,
109                                  null,
110                                  null);
111     }
112
113     public StackInfo updateStack (String cloudSiteId,
114                                   String tenantId,
115                                   String stackName,
116                                   String heatTemplate,
117                                   Map <String, Object> stackInputs,
118                                   boolean pollForCompletion,
119                                   int timeoutMinutes,
120                                   String environment,
121                                   Map <String, Object> files) throws MsoException {
122         return this.updateStack (cloudSiteId,
123                                  tenantId,
124                                  stackName,
125                                  heatTemplate,
126                                  stackInputs,
127                                  pollForCompletion,
128                                  timeoutMinutes,
129                                  environment,
130                                  files,
131                                  null);
132     }
133
134     /**
135      * Update a Stack in the specified cloud location and tenant. The Heat template
136      * and parameter map are passed in as arguments, along with the cloud access credentials.
137      * It is expected that parameters have been validated and contain at minimum the required
138      * parameters for the given template with no extra (undefined) parameters..
139      *
140      * The Stack name supplied by the caller must be unique in the scope of this tenant.
141      * However, it should also be globally unique, as it will be the identifier for the
142      * resource going forward in Inventory. This latter is managed by the higher levels
143      * invoking this function.
144      *
145      * The caller may choose to let this function poll Openstack for completion of the
146      * stack creation, or may handle polling itself via separate calls to query the status.
147      * In either case, a StackInfo object will be returned containing the current status.
148      * When polling is enabled, a status of CREATED is expected. When not polling, a
149      * status of BUILDING is expected.
150      *
151      * An error will be thrown if the requested Stack already exists in the specified
152      * Tenant and Cloud.
153      *
154      * @param tenantId The Openstack ID of the tenant in which to create the Stack
155      * @param cloudSiteId The cloud identifier (may be a region) in which to create the tenant.
156      * @param stackName The name of the stack to update
157      * @param heatTemplate The Heat template
158      * @param stackInputs A map of key/value inputs
159      * @param pollForCompletion Indicator that polling should be handled in Java vs. in the client
160      * @param environment An optional yaml-format string to specify environmental parameters
161      * @param files a Map<String, Object> for listing child template IDs
162      * @param heatFiles a Map<String, Object> for listing get_file entries (fileName, fileBody)
163      * @return A StackInfo object
164      * @throws MsoException Thrown if the Openstack API call returns an exception.
165      */
166
167     public StackInfo updateStack (String cloudSiteId,
168                                   String tenantId,
169                                   String stackName,
170                                   String heatTemplate,
171                                   Map <String, Object> stackInputs,
172                                   boolean pollForCompletion,
173                                   int timeoutMinutes,
174                                   String environment,
175                                   Map <String, Object> files,
176                                   Map <String, Object> heatFiles) throws MsoException {
177         boolean heatEnvtVariable = true;
178         if (environment == null || "".equalsIgnoreCase (environment.trim ())) {
179             heatEnvtVariable = false;
180         }
181         boolean haveFiles = true;
182         if (files == null || files.isEmpty ()) {
183             haveFiles = false;
184         }
185         boolean haveHeatFiles = true;
186         if (heatFiles == null || heatFiles.isEmpty ()) {
187             haveHeatFiles = false;
188         }
189
190         // Obtain the cloud site information where we will create the stack
191         CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(
192                 () -> new MsoCloudSiteNotFound(cloudSiteId));
193         // Get a Heat client. They are cached between calls (keyed by tenantId:cloudId)
194         // This could throw MsoTenantNotFound or MsoOpenstackException (both propagated)
195         Heat heatClient = getHeatClient (cloudSite, tenantId);
196
197         // Perform a query first to get the current status
198         Stack heatStack = queryHeatStack (heatClient, stackName);
199         if (heatStack == null || "DELETE_COMPLETE".equals (heatStack.getStackStatus ())) {
200             // Not found. Return a StackInfo with status NOTFOUND
201             throw new MsoStackNotFound (stackName, tenantId, cloudSiteId);
202         }
203
204         // Use canonical name "<stack name>/<stack-id>" to update the stack.
205         // Otherwise, update by name returns a 302 redirect.
206         // NOTE: This is specific to the v1 Orchestration API.
207         String canonicalName = heatStack.getStackName () + "/" + heatStack.getId ();
208
209         logger.debug ("Ready to Update Stack ({}) with input params: {}", canonicalName, stackInputs);
210         //force entire stackInput object to generic Map<String, Object> for openstack compatibility
211                 ObjectMapper mapper = new ObjectMapper();
212                 Map<String, Object> normalized = new HashMap<>();
213                 try {
214                         normalized = mapper.readValue(mapper.writeValueAsString(stackInputs), new TypeReference<HashMap<String,Object>>() {});
215                 } catch (IOException e1) {
216         logger.debug("could not map json", e1);
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 (normalized);
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<>();
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);
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 = Integer.parseInt(this.environment.getProperty(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
283                             .debug("Current stack {}" + this.getOutputsAsStringBuilderWithUpdate(heatStack).toString());
284                     } catch (Exception e) {
285                         logger.debug("an error occurred trying to print out the current outputs of the stack", e);
286                     }
287
288
289                     if ("UPDATE_IN_PROGRESS".equals (updateStack.getStackStatus ())) {
290                         // Stack update is still running.
291                         // Sleep and try again unless timeout has been reached
292                         if (pollTimeout <= 0) {
293                             // Note that this should not occur, since there is a timeout specified
294                             // in the Openstack call.
295                             logger.error(
296                                 "{} Cloud site: {} Tenant: {} Stack: {} Stack status: {} {} Update stack timeout",
297                                 MessageEnum.RA_UPDATE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName,
298                                 updateStack.getStackStatus(), ErrorCode.AvailabilityError.getValue());
299                             loopAgain = false;
300                         } else {
301                             try {
302                                 Thread.sleep (createPollInterval * 1000L);
303                             } catch (InterruptedException e) {
304                                 // If we are interrupted, we should stop ASAP.
305                                 loopAgain = false;
306                                 // Set again the interrupted flag
307                                 Thread.currentThread().interrupt();
308                             }
309                         }
310                         pollTimeout -= createPollInterval;
311                         logger.debug("pollTimeout remaining: {}", pollTimeout);
312                     } else {
313                         loopAgain = false;
314                     }
315                 } catch (MsoException e) {
316                     // Cannot query the stack. Something is wrong.
317
318                     // TODO: No way to roll back the stack at this point. What to do?
319                     e.addContext (UPDATE_STACK);
320                     throw e;
321                 }
322             }
323
324             if (!"UPDATE_COMPLETE".equals (updateStack.getStackStatus ())) {
325                 logger.error("{} Stack status: {} Stack status reason: {} {} Update Stack error",
326                     MessageEnum.RA_UPDATE_STACK_ERR, updateStack.getStackStatus(), updateStack.getStackStatusReason(),
327                     ErrorCode.DataError.getValue());
328
329                 // TODO: No way to roll back the stack at this point. What to do?
330                 // Throw a 'special case' of MsoOpenstackException to report the Heat status
331                 MsoOpenstackException me = null;
332                 if ("UPDATE_IN_PROGRESS".equals (updateStack.getStackStatus ())) {
333                     me = new MsoOpenstackException (0, "", "Stack Update Timeout");
334                 } else {
335                     String error = "Stack error (" + updateStack.getStackStatus ()
336                                    + "): "
337                                    + updateStack.getStackStatusReason ();
338                     me = new MsoOpenstackException (0, "", error);
339                 }
340                 me.addContext (UPDATE_STACK);
341                 throw me;
342             }
343
344         } else {
345             // Return the current status.
346             updateStack = queryHeatStack (heatClient, canonicalName);
347             if (updateStack != null) {
348                 logger.debug("UpdateStack, status = {}", updateStack.getStackStatus());
349             } else {
350                 logger.debug("UpdateStack, stack not found");
351             }
352         }
353         return new StackInfoMapper(updateStack).map();
354     }
355
356         private StringBuilder getOutputsAsStringBuilderWithUpdate(Stack heatStack) {
357                 // This should only be used as a utility to print out the stack outputs
358                 // to the log
359                 StringBuilder sb = new StringBuilder("");
360                 if (heatStack == null) {
361                         sb.append("(heatStack is null)");
362                         return sb;
363                 }
364                 List<Output> outputList = heatStack.getOutputs();
365                 if (outputList == null || outputList.isEmpty()) {
366                         sb.append("(outputs is empty)");
367                         return sb;
368                 }
369                 Map<String, Object> outputs = new HashMap<>();
370                 for (Output outputItem : outputList) {
371                         outputs.put(outputItem.getOutputKey(), outputItem.getOutputValue());
372                 }
373                 int counter = 0;
374                 sb.append("OUTPUTS:\n");
375                 for (String key : outputs.keySet()) {
376                         sb.append("outputs[").append(counter++).append("]: ").append(key).append("=");
377                         Object obj = outputs.get(key);
378                         if (obj instanceof String) {
379                                 sb.append((String) obj).append(" (a string)");
380                         } else if (obj instanceof JsonNode) {
381                                 sb.append(this.convertNodeWithUpdate((JsonNode) obj)).append(" (a JsonNode)");
382                         } else if (obj instanceof java.util.LinkedHashMap) {
383                                 try {
384                                         String str = JSON_MAPPER.writeValueAsString(obj);
385                                         sb.append(str).append(" (a java.util.LinkedHashMap)");
386                                 } catch (Exception e) {
387             logger.debug("Exception :", e);
388             sb.append("(a LinkedHashMap value that would not convert nicely)");
389                                 }
390                         } else if (obj instanceof Integer) {
391                                 String str = "";
392                                 try {
393                                         str = obj.toString() + " (an Integer)\n";
394                                 } catch (Exception e) {
395             logger.debug("Exception :", e);
396             str = "(an Integer unable to call .toString() on)";
397                                 }
398                                 sb.append(str);
399                         } else if (obj instanceof ArrayList) {
400                                 String str = "";
401                                 try {
402                                         str = obj.toString() + " (an ArrayList)";
403                                 } catch (Exception e) {
404             logger.debug("Exception :", e);
405             str = "(an ArrayList unable to call .toString() on?)";
406                                 }
407                                 sb.append(str);
408                         } else if (obj instanceof Boolean) {
409                                 String str = "";
410                                 try {
411                                         str = obj.toString() + " (a Boolean)";
412                                 } catch (Exception e) {
413             logger.debug("Exception :", e);
414             str = "(an Boolean unable to call .toString() on?)";
415                                 }
416                                 sb.append(str);
417                         }
418                         else {
419                                 String str = "";
420                                 try {
421                                         str = obj.toString() + " (unknown Object type)";
422                                 } catch (Exception e) {
423             logger.debug("Exception :", e);
424             str = "(a value unable to call .toString() on?)";
425                                 }
426                                 sb.append(str);
427                         }
428                         sb.append("\n");
429                 }
430                 sb.append("[END]");
431                 return sb;
432         }
433
434         private String convertNodeWithUpdate(final JsonNode node) {
435                 try {
436                         final Object obj = JSON_MAPPER.treeToValue(node, Object.class);
437                         final String json = JSON_MAPPER.writeValueAsString(obj);
438                         return json;
439                 } catch (Exception e) {
440         logger.debug("Error converting json to string {} ", e.getMessage(), e);
441     }
442                 return "[Error converting json to string]";
443         }
444
445 }