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