dba52d4306d86793de1efa84ef9a9c862339260b
[so.git] /
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.openecomp.mso.openstack.utils;
23
24
25 import java.util.ArrayList;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29
30 import org.codehaus.jackson.JsonNode;
31 import org.codehaus.jackson.JsonParseException;
32 import org.codehaus.jackson.map.ObjectMapper;
33
34 import org.openecomp.mso.cloud.CloudConfigFactory;
35 import org.openecomp.mso.cloud.CloudSite;
36 import org.openecomp.mso.logger.MessageEnum;
37 import org.openecomp.mso.logger.MsoLogger;
38 import org.openecomp.mso.openstack.beans.StackInfo;
39 import org.openecomp.mso.openstack.exceptions.MsoCloudSiteNotFound;
40 import org.openecomp.mso.openstack.exceptions.MsoException;
41 import org.openecomp.mso.openstack.exceptions.MsoOpenstackException;
42 import org.openecomp.mso.openstack.exceptions.MsoStackNotFound;
43 import org.openecomp.mso.properties.MsoJavaProperties;
44 import org.openecomp.mso.properties.MsoPropertiesException;
45 import org.openecomp.mso.properties.MsoPropertiesFactory;
46 import com.woorea.openstack.base.client.OpenStackBaseException;
47 import com.woorea.openstack.base.client.OpenStackRequest;
48 import com.woorea.openstack.heat.Heat;
49 import com.woorea.openstack.heat.model.Stack;
50 import com.woorea.openstack.heat.model.UpdateStackParam;
51 import com.woorea.openstack.heat.model.Stack.Output;
52
53 public class MsoHeatUtilsWithUpdate extends MsoHeatUtils {
54
55     private static final String UPDATE_STACK = "UpdateStack";
56     private static MsoLogger LOGGER = MsoLogger.getMsoLogger (MsoLogger.Catalog.RA);
57
58     protected MsoJavaProperties msoProps = null;
59
60     private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
61
62     public MsoHeatUtilsWithUpdate (String msoPropID, MsoPropertiesFactory msoPropertiesFactory, CloudConfigFactory cloudConfFactory) {
63         super (msoPropID,msoPropertiesFactory,cloudConfFactory);
64         
65         try {
66                         msoProps = msoPropertiesFactory.getMsoJavaProperties (msoPropID);
67                 } catch (MsoPropertiesException e) {
68                         LOGGER.error (MessageEnum.LOAD_PROPERTIES_FAIL, "Unknown. Mso Properties ID not found in cache: " + msoPropID, "", "", MsoLogger.ErrorCode.AvailabilityError, "Exception Mso Properties ID not found in cache: " + msoPropID, e);
69                 }
70     }
71     
72     /*
73      * Keep these methods around for backward compatibility
74      */
75
76     public StackInfo updateStack (String cloudSiteId,
77                                   String tenantId,
78                                   String stackName,
79                                   String heatTemplate,
80                                   Map <String, Object> stackInputs,
81                                   boolean pollForCompletion,
82                                   int timeoutMinutes) throws MsoException {
83         // Keeping this method to allow compatibility with no environment or files variable sent. In this case,
84         // simply return the new method with the environment variable set to null.
85         return this.updateStack (cloudSiteId,
86                                  tenantId,
87                                  stackName,
88                                  heatTemplate,
89                                  stackInputs,
90                                  pollForCompletion,
91                                  timeoutMinutes,
92                                  null,
93                                  null,
94                                  null);
95     }
96
97     public StackInfo updateStack (String cloudSiteId,
98                                   String tenantId,
99                                   String stackName,
100                                   String heatTemplate,
101                                   Map <String, Object> stackInputs,
102                                   boolean pollForCompletion,
103                                   int timeoutMinutes,
104                                   String environment) throws MsoException {
105         // Keeping this method to allow compatibility with no environment variable sent. In this case,
106         // simply return the new method with the files variable set to null.
107         return this.updateStack (cloudSiteId,
108                                  tenantId,
109                                  stackName,
110                                  heatTemplate,
111                                  stackInputs,
112                                  pollForCompletion,
113                                  timeoutMinutes,
114                                  environment,
115                                  null,
116                                  null);
117     }
118
119     public StackInfo updateStack (String cloudSiteId,
120                                   String tenantId,
121                                   String stackName,
122                                   String heatTemplate,
123                                   Map <String, Object> stackInputs,
124                                   boolean pollForCompletion,
125                                   int timeoutMinutes,
126                                   String environment,
127                                   Map <String, Object> files) throws MsoException {
128         return this.updateStack (cloudSiteId,
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 stackTemplate 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 tenantId,
175                                   String stackName,
176                                   String heatTemplate,
177                                   Map <String, Object> stackInputs,
178                                   boolean pollForCompletion,
179                                   int timeoutMinutes,
180                                   String environment,
181                                   Map <String, Object> files,
182                                   Map <String, Object> heatFiles) throws MsoException {
183         boolean heatEnvtVariable = true;
184         if (environment == null || "".equalsIgnoreCase (environment.trim ())) {
185             heatEnvtVariable = false;
186         }
187         boolean haveFiles = true;
188         if (files == null || files.isEmpty ()) {
189             haveFiles = false;
190         }
191         boolean haveHeatFiles = true;
192         if (heatFiles == null || heatFiles.isEmpty ()) {
193             haveHeatFiles = false;
194         }
195
196         // Obtain the cloud site information where we will create the stack
197         CloudSite cloudSite = cloudConfig.getCloudSite (cloudSiteId);
198         if (cloudSite == null) {
199             throw new MsoCloudSiteNotFound (cloudSiteId);
200         }
201         // Get a Heat client. They are cached between calls (keyed by tenantId:cloudId)
202         // This could throw MsoTenantNotFound or MsoOpenstackException (both propagated)
203         Heat heatClient = getHeatClient (cloudSite, tenantId);
204
205         // Perform a query first to get the current status
206         Stack heatStack = queryHeatStack (heatClient, stackName);
207         if (heatStack == null || "DELETE_COMPLETE".equals (heatStack.getStackStatus ())) {
208             // Not found. Return a StackInfo with status NOTFOUND
209             throw new MsoStackNotFound (stackName, tenantId, cloudSiteId);
210         }
211
212         // Use canonical name "<stack name>/<stack-id>" to update the stack.
213         // Otherwise, update by name returns a 302 redirect.
214         // NOTE: This is specific to the v1 Orchestration API.
215         String canonicalName = heatStack.getStackName () + "/" + heatStack.getId ();
216
217         LOGGER.debug ("Ready to Update Stack (" + canonicalName + ") with input params: " + stackInputs);
218
219         // Build up the stack update parameters
220         // Disable auto-rollback, because error reason is lost. Always rollback in the code.
221         UpdateStackParam stack = new UpdateStackParam ();
222         stack.setTimeoutMinutes (timeoutMinutes);
223         stack.setParameters (stackInputs);
224         stack.setTemplate (heatTemplate);
225         stack.setDisableRollback (true);
226         // TJM add envt to stack
227         if (heatEnvtVariable) {
228             stack.setEnvironment (environment);
229         }
230
231         // Handle nested templates & get_files here. if we have both - must combine
232         // and then add to stack (both are part of "files:" being added to stack)
233         if (haveFiles && haveHeatFiles) {
234             // Let's do this here - not in the bean
235             LOGGER.debug ("Found files AND heatFiles - combine and add!");
236             Map <String, Object> combinedFiles = new HashMap <String, Object> ();
237             for (String keyString : files.keySet ()) {
238                 combinedFiles.put (keyString, files.get (keyString));
239             }
240             for (String keyString : heatFiles.keySet ()) {
241                 combinedFiles.put (keyString, heatFiles.get (keyString));
242             }
243             stack.setFiles (combinedFiles);
244         } else {
245             // Handle case where we have one or neither
246             if (haveFiles) {
247                 stack.setFiles (files);
248             }
249             if (haveHeatFiles) {
250                 // setFiles method modified to handle adding a map.
251                 stack.setFiles (heatFiles);
252             }
253         }
254
255         try {
256             // Execute the actual Openstack command to update the Heat stack
257             OpenStackRequest <Void> request = heatClient.getStacks ().update (canonicalName, stack);
258             executeAndRecordOpenstackRequest (request, msoProps);
259         } catch (OpenStackBaseException e) {
260             // Since this came on the 'Update Stack' command, nothing was changed
261             // in the cloud. Rethrow the error as an MSO exception.
262             throw heatExceptionToMsoException (e, UPDATE_STACK);
263         } catch (RuntimeException e) {
264             // Catch-all
265             throw runtimeExceptionToMsoException (e, UPDATE_STACK);
266         }
267
268         // If client has requested a final response, poll for stack completion
269         Stack updateStack = null;
270         if (pollForCompletion) {
271             // Set a time limit on overall polling.
272             // Use the resource (template) timeout for Openstack (expressed in minutes)
273             // and add one poll interval to give Openstack a chance to fail on its own.
274             int createPollInterval = msoProps.getIntProperty (createPollIntervalProp, createPollIntervalDefault);
275             int pollTimeout = (timeoutMinutes * 60) + createPollInterval;
276
277             boolean loopAgain = true;
278             while (loopAgain) {
279                 try {
280                     updateStack = queryHeatStack (heatClient, canonicalName);
281                     LOGGER.debug (updateStack.getStackStatus () + " (" + canonicalName + ")");
282                     try {
283                         LOGGER.debug("Current stack " + this.getOutputsAsStringBuilder(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 (MessageEnum.RA_UPDATE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName, updateStack.getStackStatus(), "", "", MsoLogger.ErrorCode.AvailabilityError, "Update stack timeout");
296                             loopAgain = false;
297                         } else {
298                             try {
299                                 Thread.sleep (createPollInterval * 1000L);
300                             } catch (InterruptedException e) {
301                                 // If we are interrupted, we should stop ASAP.
302                                 loopAgain = false;
303                                 // Set again the interrupted flag
304                                 Thread.currentThread().interrupt();
305                             }
306                         }
307                         pollTimeout -= createPollInterval;
308                         LOGGER.debug("pollTimeout remaining: " + pollTimeout);
309                     } else {
310                         loopAgain = false;
311                     }
312                 } catch (MsoException e) {
313                     // Cannot query the stack. Something is wrong.
314
315                     // TODO: No way to roll back the stack at this point. What to do?
316                     e.addContext (UPDATE_STACK);
317                     throw e;
318                 }
319             }
320
321             if (!"UPDATE_COMPLETE".equals (updateStack.getStackStatus ())) {
322                 LOGGER.error (MessageEnum.RA_UPDATE_STACK_ERR, updateStack.getStackStatus(), updateStack.getStackStatusReason(), "", "", MsoLogger.ErrorCode.DataError, "Update Stack error");
323
324                 // TODO: No way to roll back the stack at this point. What to do?
325                 // Throw a 'special case' of MsoOpenstackException to report the Heat status
326                 MsoOpenstackException me = null;
327                 if ("UPDATE_IN_PROGRESS".equals (updateStack.getStackStatus ())) {
328                     me = new MsoOpenstackException (0, "", "Stack Update Timeout");
329                 } else {
330                     String error = "Stack error (" + updateStack.getStackStatus ()
331                                    + "): "
332                                    + updateStack.getStackStatusReason ();
333                     me = new MsoOpenstackException (0, "", error);
334                 }
335                 me.addContext (UPDATE_STACK);
336                 throw me;
337             }
338
339         } else {
340             // Return the current status.
341             updateStack = queryHeatStack (heatClient, canonicalName);
342             if (updateStack != null) {
343                 LOGGER.debug ("UpdateStack, status = " + updateStack.getStackStatus ());
344             } else {
345                 LOGGER.debug ("UpdateStack, stack not found");
346             }
347         }
348         return new StackInfo (updateStack);
349     }
350     
351         private StringBuilder getOutputsAsStringBuilder(Stack heatStack) {
352                 // This should only be used as a utility to print out the stack outputs
353                 // to the log
354                 StringBuilder sb = new StringBuilder("");
355                 if (heatStack == null) {
356                         sb.append("(heatStack is null)");
357                         return sb;
358                 }
359                 List<Output> outputList = heatStack.getOutputs();
360                 if (outputList == null || outputList.isEmpty()) {
361                         sb.append("(outputs is empty)");
362                         return sb;
363                 }
364                 Map<String, Object> outputs = new HashMap<String,Object>();
365                 for (Output outputItem : outputList) {
366                         outputs.put(outputItem.getOutputKey(), outputItem.getOutputValue());
367                 }
368                 int counter = 0;
369                 sb.append("OUTPUTS:\n");
370                 for (String key : outputs.keySet()) {
371                         sb.append("outputs[" + counter++ + "]: " + key + "=");
372                         Object obj = outputs.get(key);
373                         if (obj instanceof String) {
374                                 sb.append((String)obj +" (a string)");
375                         } else if (obj instanceof JsonNode) {
376                                 sb.append(this.convertNode((JsonNode)obj) + " (a JsonNode)");
377                         } else if (obj instanceof java.util.LinkedHashMap) {
378                                 try {
379                                         String str = JSON_MAPPER.writeValueAsString(obj);
380                                         sb.append(str + " (a java.util.LinkedHashMap)");
381                                 } catch (Exception e) {
382                                         LOGGER.debug("Exception :", e);
383                                         sb.append("(a LinkedHashMap value that would not convert nicely)");
384                                 }                               
385                         } else if (obj instanceof Integer) {
386                                 String str = "";
387                                 try {
388                                         str = obj.toString() + " (an Integer)\n";
389                                 } catch (Exception e) {
390                                         LOGGER.debug("Exception :", e);
391                                         str = "(an Integer unable to call .toString() on)";
392                                 }
393                                 sb.append(str);
394                         } else if (obj instanceof ArrayList) {
395                                 String str = "";
396                                 try {
397                                         str = obj.toString() + " (an ArrayList)";
398                                 } catch (Exception e) {
399                                         LOGGER.debug("Exception :", e);
400                                         str = "(an ArrayList unable to call .toString() on?)";
401                                 }
402                                 sb.append(str);
403                         } else if (obj instanceof Boolean) {
404                                 String str = "";
405                                 try {
406                                         str = obj.toString() + " (a Boolean)";
407                                 } catch (Exception e) {
408                                         LOGGER.debug("Exception :", e);
409                                         str = "(an Boolean unable to call .toString() on?)";
410                                 }
411                                 sb.append(str);
412                         }
413                         else {
414                                 String str = "";
415                                 try {
416                                         str = obj.toString() + " (unknown Object type)";
417                                 } catch (Exception e) {
418                                         LOGGER.debug("Exception :", e);
419                                         str = "(a value unable to call .toString() on?)";
420                                 }
421                                 sb.append(str);
422                         }
423                         sb.append("\n");
424                 }
425                 sb.append("[END]");
426                 return sb;
427         }
428         
429         private String convertNode(final JsonNode node) {
430                 try {
431                         final Object obj = JSON_MAPPER.treeToValue(node, Object.class);
432                         final String json = JSON_MAPPER.writeValueAsString(obj);
433                         return json;
434                 } catch (JsonParseException jpe) {
435                         LOGGER.debug("Error converting json to string " + jpe.getMessage(), jpe);
436                 } catch (Exception e) {
437                         LOGGER.debug("Error converting json to string " + e.getMessage(), e);
438                 }
439                 return "[Error converting json to string]";
440         }
441         
442 }