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