Merge remote-tracking branch 'origin/dublin' into 'origin/master'
[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 java.io.IOException;
28 import java.util.ArrayList;
29 import java.util.HashMap;
30 import java.util.List;
31 import java.util.Map;
32 import org.onap.so.logger.ErrorCode;
33 import org.onap.so.logger.MessageEnum;
34 import org.onap.so.openstack.beans.StackInfo;
35 import org.onap.so.openstack.exceptions.MsoException;
36 import org.onap.so.openstack.exceptions.MsoOpenstackException;
37 import org.onap.so.openstack.exceptions.MsoStackNotFound;
38 import org.onap.so.openstack.mappers.StackInfoMapper;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41 import org.springframework.beans.factory.annotation.Autowired;
42 import org.springframework.core.env.Environment;
43 import org.springframework.stereotype.Component;
44 import com.fasterxml.jackson.core.type.TypeReference;
45 import com.fasterxml.jackson.databind.JsonNode;
46 import com.fasterxml.jackson.databind.ObjectMapper;
47 import com.woorea.openstack.base.client.OpenStackBaseException;
48 import com.woorea.openstack.base.client.OpenStackRequest;
49 import com.woorea.openstack.heat.Heat;
50 import com.woorea.openstack.heat.model.Stack;
51 import com.woorea.openstack.heat.model.Stack.Output;
52 import com.woorea.openstack.heat.model.UpdateStackParam;
53
54 @Component
55 public class MsoHeatUtilsWithUpdate extends MsoHeatUtils {
56
57     private static final String UPDATE_STACK = "UpdateStack";
58     private static final Logger logger = LoggerFactory.getLogger(MsoHeatUtilsWithUpdate.class);
59
60     private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
61     public static final String EXCEPTION = "Exception :";
62
63     @Autowired
64     private Environment environment;
65     /*
66      * Keep these methods around for backward compatibility
67      */
68
69     public StackInfo updateStack(String cloudSiteId, String cloudOwner, String tenantId, String stackName,
70             String heatTemplate, Map<String, Object> stackInputs, boolean pollForCompletion, int timeoutMinutes)
71             throws MsoException {
72         // Keeping this method to allow compatibility with no environment or files variable sent. In this case,
73         // simply return the new method with the environment variable set to null.
74         return this.updateStack(cloudSiteId, cloudOwner, tenantId, stackName, heatTemplate, stackInputs,
75                 pollForCompletion, timeoutMinutes, null, null, null);
76     }
77
78     public StackInfo updateStack(String cloudSiteId, String cloudOwner, String tenantId, String stackName,
79             String heatTemplate, Map<String, Object> stackInputs, boolean pollForCompletion, int timeoutMinutes,
80             String environment) throws MsoException {
81         // Keeping this method to allow compatibility with no environment variable sent. In this case,
82         // simply return the new method with the files variable set to null.
83         return this.updateStack(cloudSiteId, cloudOwner, tenantId, stackName, heatTemplate, stackInputs,
84                 pollForCompletion, timeoutMinutes, environment, null, null);
85     }
86
87     public StackInfo updateStack(String cloudSiteId, String cloudOwner, String tenantId, String stackName,
88             String heatTemplate, Map<String, Object> stackInputs, boolean pollForCompletion, int timeoutMinutes,
89             String environment, Map<String, Object> files) throws MsoException {
90         return this.updateStack(cloudSiteId, cloudOwner, tenantId, stackName, heatTemplate, stackInputs,
91                 pollForCompletion, timeoutMinutes, environment, files, null);
92     }
93
94     /**
95      * Update a Stack in the specified cloud location and tenant. The Heat template and parameter map are passed in as
96      * arguments, along with the cloud access credentials. It is expected that parameters have been validated and
97      * contain at minimum the required parameters for the given template with no extra (undefined) parameters..
98      *
99      * The Stack name supplied by the caller must be unique in the scope of this tenant. However, it should also be
100      * globally unique, as it will be the identifier for the resource going forward in Inventory. This latter is managed
101      * by the higher levels invoking this function.
102      *
103      * The caller may choose to let this function poll Openstack for completion of the stack creation, or may handle
104      * polling itself via separate calls to query the status. In either case, a StackInfo object will be returned
105      * containing the current status. When polling is enabled, a status of CREATED is expected. When not polling, a
106      * status of BUILDING is expected.
107      *
108      * An error will be thrown if the requested Stack already exists in the specified Tenant and Cloud.
109      *
110      * @param tenantId The Openstack ID of the tenant in which to create the Stack
111      * @param cloudSiteId The cloud identifier (may be a region) in which to create the tenant.
112      * @param stackName The name of the stack to update
113      * @param heatTemplate The Heat template
114      * @param stackInputs A map of key/value inputs
115      * @param pollForCompletion Indicator that polling should be handled in Java vs. in the client
116      * @param environment An optional yaml-format string to specify environmental parameters
117      * @param files a Map<String, Object> for listing child template IDs
118      * @param heatFiles a Map<String, Object> for listing get_file entries (fileName, fileBody)
119      * @return A StackInfo object
120      * @throws MsoException Thrown if the Openstack API call returns an exception.
121      */
122
123     public StackInfo updateStack(String cloudSiteId, String cloudOwner, String tenantId, String stackName,
124             String heatTemplate, Map<String, Object> stackInputs, boolean pollForCompletion, int timeoutMinutes,
125             String environment, Map<String, Object> files, Map<String, Object> heatFiles) throws MsoException {
126         boolean heatEnvtVariable = true;
127         if (environment == null || "".equalsIgnoreCase(environment.trim())) {
128             heatEnvtVariable = false;
129         }
130         boolean haveFiles = true;
131         if (files == null || files.isEmpty()) {
132             haveFiles = false;
133         }
134         boolean haveHeatFiles = true;
135         if (heatFiles == null || heatFiles.isEmpty()) {
136             haveHeatFiles = false;
137         }
138
139         Heat heatClient = getHeatClient(cloudSiteId, tenantId);
140
141         // Perform a query first to get the current status
142         Stack heatStack = queryHeatStack(heatClient, stackName);
143         if (heatStack == null || "DELETE_COMPLETE".equals(heatStack.getStackStatus())) {
144             // Not found. Return a StackInfo with status NOTFOUND
145             throw new MsoStackNotFound(stackName, tenantId, cloudSiteId);
146         }
147
148         // Use canonical name "<stack name>/<stack-id>" to update the stack.
149         // Otherwise, update by name returns a 302 redirect.
150         // NOTE: This is specific to the v1 Orchestration API.
151         String canonicalName = heatStack.getStackName() + "/" + heatStack.getId();
152
153         logger.debug("Ready to Update Stack ({}) with input params: {}", canonicalName, stackInputs);
154         // force entire stackInput object to generic Map<String, Object> for openstack compatibility
155         ObjectMapper mapper = new ObjectMapper();
156         Map<String, Object> normalized = new HashMap<>();
157         try {
158             normalized = mapper.readValue(mapper.writeValueAsString(stackInputs),
159                     new TypeReference<HashMap<String, Object>>() {});
160         } catch (IOException e1) {
161             logger.debug("could not map json", e1);
162         }
163         // Build up the stack update parameters
164         // Disable auto-rollback, because error reason is lost. Always rollback in the code.
165         UpdateStackParam stack = new UpdateStackParam();
166         stack.setTimeoutMinutes(timeoutMinutes);
167         stack.setParameters(normalized);
168         stack.setTemplate(heatTemplate);
169         stack.setDisableRollback(true);
170         // TJM add envt to stack
171         if (heatEnvtVariable) {
172             stack.setEnvironment(environment);
173         }
174
175         // Handle nested templates & get_files here. if we have both - must combine
176         // and then add to stack (both are part of "files:" being added to stack)
177         if (haveFiles && haveHeatFiles) {
178             // Let's do this here - not in the bean
179             logger.debug("Found files AND heatFiles - combine and add!");
180             Map<String, Object> combinedFiles = new HashMap<>();
181             for (String keyString : files.keySet()) {
182                 combinedFiles.put(keyString, files.get(keyString));
183             }
184             for (String keyString : heatFiles.keySet()) {
185                 combinedFiles.put(keyString, heatFiles.get(keyString));
186             }
187             stack.setFiles(combinedFiles);
188         } else {
189             // Handle case where we have one or neither
190             if (haveFiles) {
191                 stack.setFiles(files);
192             }
193             if (haveHeatFiles) {
194                 // setFiles method modified to handle adding a map.
195                 stack.setFiles(heatFiles);
196             }
197         }
198
199         try {
200             // Execute the actual Openstack command to update the Heat stack
201             OpenStackRequest<Void> request = heatClient.getStacks().update(canonicalName, stack);
202             executeAndRecordOpenstackRequest(request);
203         } catch (OpenStackBaseException e) {
204             // Since this came on the 'Update Stack' command, nothing was changed
205             // in the cloud. Rethrow the error as an MSO exception.
206             throw heatExceptionToMsoException(e, UPDATE_STACK);
207         } catch (RuntimeException e) {
208             // Catch-all
209             throw runtimeExceptionToMsoException(e, UPDATE_STACK);
210         }
211
212         // If client has requested a final response, poll for stack completion
213         Stack updateStack = null;
214         if (pollForCompletion) {
215             // Set a time limit on overall polling.
216             // Use the resource (template) timeout for Openstack (expressed in minutes)
217             // and add one poll interval to give Openstack a chance to fail on its own.
218             int createPollInterval = Integer
219                     .parseInt(this.environment.getProperty(createPollIntervalProp, CREATE_POLL_INTERVAL_DEFAULT));
220             int pollTimeout = (timeoutMinutes * 60) + createPollInterval;
221
222             boolean loopAgain = true;
223             while (loopAgain) {
224                 try {
225                     updateStack = queryHeatStack(heatClient, canonicalName);
226                     logger.debug("{} ({}) ", updateStack.getStackStatus(), canonicalName);
227                     try {
228                         logger.debug(
229                                 "Current stack {}" + this.getOutputsAsStringBuilderWithUpdate(heatStack).toString());
230                     } catch (Exception e) {
231                         logger.debug("an error occurred trying to print out the current outputs of the stack", e);
232                     }
233
234
235                     if ("UPDATE_IN_PROGRESS".equals(updateStack.getStackStatus())) {
236                         // Stack update is still running.
237                         // Sleep and try again unless timeout has been reached
238                         if (pollTimeout <= 0) {
239                             // Note that this should not occur, since there is a timeout specified
240                             // in the Openstack call.
241                             logger.error(
242                                     "{} Cloud site: {} Tenant: {} Stack: {} Stack status: {} {} Update stack timeout",
243                                     MessageEnum.RA_UPDATE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName,
244                                     updateStack.getStackStatus(), ErrorCode.AvailabilityError.getValue());
245                             loopAgain = false;
246                         } else {
247                             try {
248                                 Thread.sleep(createPollInterval * 1000L);
249                             } catch (InterruptedException e) {
250                                 // If we are interrupted, we should stop ASAP.
251                                 loopAgain = false;
252                                 // Set again the interrupted flag
253                                 Thread.currentThread().interrupt();
254                             }
255                         }
256                         pollTimeout -= createPollInterval;
257                         logger.debug("pollTimeout remaining: {}", pollTimeout);
258                     } else {
259                         loopAgain = false;
260                     }
261                 } catch (MsoException e) {
262                     // Cannot query the stack. Something is wrong.
263
264                     // TODO: No way to roll back the stack at this point. What to do?
265                     e.addContext(UPDATE_STACK);
266                     throw e;
267                 }
268             }
269
270             if (!"UPDATE_COMPLETE".equals(updateStack.getStackStatus())) {
271                 logger.error("{} Stack status: {} Stack status reason: {} {} Update Stack error",
272                         MessageEnum.RA_UPDATE_STACK_ERR, updateStack.getStackStatus(),
273                         updateStack.getStackStatusReason(), ErrorCode.DataError.getValue());
274
275                 // TODO: No way to roll back the stack at this point. What to do?
276                 // Throw a 'special case' of MsoOpenstackException to report the Heat status
277                 MsoOpenstackException me = null;
278                 if ("UPDATE_IN_PROGRESS".equals(updateStack.getStackStatus())) {
279                     me = new MsoOpenstackException(0, "", "Stack Update Timeout");
280                 } else {
281                     String error =
282                             "Stack error (" + updateStack.getStackStatus() + "): " + updateStack.getStackStatusReason();
283                     me = new MsoOpenstackException(0, "", error);
284                 }
285                 me.addContext(UPDATE_STACK);
286                 throw me;
287             }
288
289         } else {
290             // Return the current status.
291             updateStack = queryHeatStack(heatClient, canonicalName);
292             if (updateStack != null) {
293                 logger.debug("UpdateStack, status = {}", updateStack.getStackStatus());
294             } else {
295                 logger.debug("UpdateStack, stack not found");
296             }
297         }
298         return new StackInfoMapper(updateStack).map();
299     }
300
301     private StringBuilder getOutputsAsStringBuilderWithUpdate(Stack heatStack) {
302         // This should only be used as a utility to print out the stack outputs
303         // to the log
304         StringBuilder sb = new StringBuilder("");
305         if (heatStack == null) {
306             sb.append("(heatStack is null)");
307             return sb;
308         }
309         List<Output> outputList = heatStack.getOutputs();
310         if (outputList == null || outputList.isEmpty()) {
311             sb.append("(outputs is empty)");
312             return sb;
313         }
314         Map<String, Object> outputs = new HashMap<>();
315         for (Output outputItem : outputList) {
316             outputs.put(outputItem.getOutputKey(), outputItem.getOutputValue());
317         }
318         int counter = 0;
319         sb.append("OUTPUTS:\n");
320         for (String key : outputs.keySet()) {
321             sb.append("outputs[").append(counter++).append("]: ").append(key).append("=");
322             Object obj = outputs.get(key);
323             if (obj instanceof String) {
324                 sb.append((String) obj).append(" (a string)");
325             } else if (obj instanceof JsonNode) {
326                 sb.append(this.convertNodeWithUpdate((JsonNode) obj)).append(" (a JsonNode)");
327             } else if (obj instanceof java.util.LinkedHashMap) {
328                 try {
329                     String str = JSON_MAPPER.writeValueAsString(obj);
330                     sb.append(str).append(" (a java.util.LinkedHashMap)");
331                 } catch (Exception e) {
332                     logger.debug(EXCEPTION, e);
333                     sb.append("(a LinkedHashMap value that would not convert nicely)");
334                 }
335             } else if (obj instanceof Integer) {
336                 String str = "";
337                 try {
338                     str = obj.toString() + " (an Integer)\n";
339                 } catch (Exception e) {
340                     logger.debug(EXCEPTION, e);
341                     str = "(an Integer unable to call .toString() on)";
342                 }
343                 sb.append(str);
344             } else if (obj instanceof ArrayList) {
345                 String str = "";
346                 try {
347                     str = obj.toString() + " (an ArrayList)";
348                 } catch (Exception e) {
349                     logger.debug(EXCEPTION, e);
350                     str = "(an ArrayList unable to call .toString() on?)";
351                 }
352                 sb.append(str);
353             } else if (obj instanceof Boolean) {
354                 String str = "";
355                 try {
356                     str = obj.toString() + " (a Boolean)";
357                 } catch (Exception e) {
358                     logger.debug(EXCEPTION, e);
359                     str = "(an Boolean unable to call .toString() on?)";
360                 }
361                 sb.append(str);
362             } else {
363                 String str = "";
364                 try {
365                     str = obj.toString() + " (unknown Object type)";
366                 } catch (Exception e) {
367                     logger.debug(EXCEPTION, e);
368                     str = "(a value unable to call .toString() on?)";
369                 }
370                 sb.append(str);
371             }
372             sb.append("\n");
373         }
374         sb.append("[END]");
375         return sb;
376     }
377
378     private String convertNodeWithUpdate(final JsonNode node) {
379         try {
380             final Object obj = JSON_MAPPER.treeToValue(node, Object.class);
381             return JSON_MAPPER.writeValueAsString(obj);
382         } catch (Exception e) {
383             logger.debug("Error converting json to string {} ", e.getMessage(), e);
384         }
385         return "[Error converting json to string]";
386     }
387
388 }