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