2  * ============LICENSE_START=======================================================
 
   4  * ================================================================================
 
   5  * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
 
   6  * ================================================================================
 
   7  * Licensed under the Apache License, Version 2.0 (the "License");
 
   8  * you may not use this file except in compliance with the License.
 
   9  * You may obtain a copy of the License at
 
  11  *      http://www.apache.org/licenses/LICENSE-2.0
 
  13  * Unless required by applicable law or agreed to in writing, software
 
  14  * distributed under the License is distributed on an "AS IS" BASIS,
 
  15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 
  16  * See the License for the specific language governing permissions and
 
  17  * limitations under the License.
 
  18  * ============LICENSE_END=========================================================
 
  21 package org.openecomp.mso.openstack.utils;
 
  24 import java.util.ArrayList;
 
  25 import java.util.HashMap;
 
  26 import java.util.List;
 
  29 import org.codehaus.jackson.JsonNode;
 
  30 import org.codehaus.jackson.JsonParseException;
 
  31 import org.codehaus.jackson.map.ObjectMapper;
 
  33 import org.openecomp.mso.cloud.CloudConfigFactory;
 
  34 import org.openecomp.mso.cloud.CloudSite;
 
  35 import org.openecomp.mso.logger.MessageEnum;
 
  36 import org.openecomp.mso.logger.MsoLogger;
 
  37 import org.openecomp.mso.openstack.beans.StackInfo;
 
  38 import org.openecomp.mso.openstack.exceptions.MsoCloudSiteNotFound;
 
  39 import org.openecomp.mso.openstack.exceptions.MsoException;
 
  40 import org.openecomp.mso.openstack.exceptions.MsoOpenstackException;
 
  41 import org.openecomp.mso.openstack.exceptions.MsoStackNotFound;
 
  42 import org.openecomp.mso.properties.MsoJavaProperties;
 
  43 import org.openecomp.mso.properties.MsoPropertiesException;
 
  44 import org.openecomp.mso.properties.MsoPropertiesFactory;
 
  45 import com.woorea.openstack.base.client.OpenStackBaseException;
 
  46 import com.woorea.openstack.base.client.OpenStackRequest;
 
  47 import com.woorea.openstack.heat.Heat;
 
  48 import com.woorea.openstack.heat.model.Stack;
 
  49 import com.woorea.openstack.heat.model.UpdateStackParam;
 
  50 import com.woorea.openstack.heat.model.Stack.Output;
 
  52 public class MsoHeatUtilsWithUpdate extends MsoHeatUtils {
 
  54     private static final String UPDATE_STACK = "UpdateStack";
 
  55     private static MsoLogger LOGGER = MsoLogger.getMsoLogger (MsoLogger.Catalog.RA);
 
  57     protected MsoJavaProperties msoProps = null;
 
  59     private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
 
  61     public MsoHeatUtilsWithUpdate (String msoPropID, MsoPropertiesFactory msoPropertiesFactory, CloudConfigFactory cloudConfFactory) {
 
  62         super (msoPropID,msoPropertiesFactory,cloudConfFactory);
 
  65                         msoProps = msoPropertiesFactory.getMsoJavaProperties (msoPropID);
 
  66                 } catch (MsoPropertiesException e) {
 
  67                         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);
 
  72      * Keep these methods around for backward compatibility
 
  75     public StackInfo updateStack (String cloudSiteId,
 
  79                                   Map <String, Object> stackInputs,
 
  80                                   boolean pollForCompletion,
 
  81                                   int timeoutMinutes) throws MsoException {
 
  82         // Keeping this method to allow compatibility with no environment or files variable sent. In this case,
 
  83         // simply return the new method with the environment variable set to null.
 
  84         return this.updateStack (cloudSiteId,
 
  96     public StackInfo updateStack (String cloudSiteId,
 
 100                                   Map <String, Object> stackInputs,
 
 101                                   boolean pollForCompletion,
 
 103                                   String environment) throws MsoException {
 
 104         // Keeping this method to allow compatibility with no environment variable sent. In this case,
 
 105         // simply return the new method with the files variable set to null.
 
 106         return this.updateStack (cloudSiteId,
 
 118     public StackInfo updateStack (String cloudSiteId,
 
 122                                   Map <String, Object> stackInputs,
 
 123                                   boolean pollForCompletion,
 
 126                                   Map <String, Object> files) throws MsoException {
 
 127         return this.updateStack (cloudSiteId,
 
 140      * Update a Stack in the specified cloud location and tenant. The Heat template
 
 141      * and parameter map are passed in as arguments, along with the cloud access credentials.
 
 142      * It is expected that parameters have been validated and contain at minimum the required
 
 143      * parameters for the given template with no extra (undefined) parameters..
 
 145      * The Stack name supplied by the caller must be unique in the scope of this tenant.
 
 146      * However, it should also be globally unique, as it will be the identifier for the
 
 147      * resource going forward in Inventory. This latter is managed by the higher levels
 
 148      * invoking this function.
 
 150      * The caller may choose to let this function poll Openstack for completion of the
 
 151      * stack creation, or may handle polling itself via separate calls to query the status.
 
 152      * In either case, a StackInfo object will be returned containing the current status.
 
 153      * When polling is enabled, a status of CREATED is expected. When not polling, a
 
 154      * status of BUILDING is expected.
 
 156      * An error will be thrown if the requested Stack already exists in the specified
 
 159      * @param tenantId The Openstack ID of the tenant in which to create the Stack
 
 160      * @param cloudSiteId The cloud identifier (may be a region) in which to create the tenant.
 
 161      * @param stackName The name of the stack to update
 
 162      * @param stackTemplate The Heat template
 
 163      * @param stackInputs A map of key/value inputs
 
 164      * @param pollForCompletion Indicator that polling should be handled in Java vs. in the client
 
 165      * @param environment An optional yaml-format string to specify environmental parameters
 
 166      * @param files a Map<String, Object> for listing child template IDs
 
 167      * @param heatFiles a Map<String, Object> for listing get_file entries (fileName, fileBody)
 
 168      * @return A StackInfo object
 
 169      * @throws MsoException Thrown if the Openstack API call returns an exception.
 
 172     public StackInfo updateStack (String cloudSiteId,
 
 176                                   Map <String, Object> stackInputs,
 
 177                                   boolean pollForCompletion,
 
 180                                   Map <String, Object> files,
 
 181                                   Map <String, Object> heatFiles) throws MsoException {
 
 182         boolean heatEnvtVariable = true;
 
 183         if (environment == null || "".equalsIgnoreCase (environment.trim ())) {
 
 184             heatEnvtVariable = false;
 
 186         boolean haveFiles = true;
 
 187         if (files == null || files.isEmpty ()) {
 
 190         boolean haveHeatFiles = true;
 
 191         if (heatFiles == null || heatFiles.isEmpty ()) {
 
 192             haveHeatFiles = false;
 
 195         // Obtain the cloud site information where we will create the stack
 
 196         CloudSite cloudSite = cloudConfig.getCloudSite (cloudSiteId);
 
 197         if (cloudSite == null) {
 
 198             throw 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);
 
 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);
 
 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 ();
 
 216         LOGGER.debug ("Ready to Update Stack (" + canonicalName + ") with input params: " + stackInputs);
 
 218         // Build up the stack update parameters
 
 219         // Disable auto-rollback, because error reason is lost. Always rollback in the code.
 
 220         UpdateStackParam stack = new UpdateStackParam ();
 
 221         stack.setTimeoutMinutes (timeoutMinutes);
 
 222         stack.setParameters (stackInputs);
 
 223         stack.setTemplate (heatTemplate);
 
 224         stack.setDisableRollback (true);
 
 225         // TJM add envt to stack
 
 226         if (heatEnvtVariable) {
 
 227             stack.setEnvironment (environment);
 
 230         // Handle nested templates & get_files here. if we have both - must combine
 
 231         // and then add to stack (both are part of "files:" being added to stack)
 
 232         if (haveFiles && haveHeatFiles) {
 
 233             // Let's do this here - not in the bean
 
 234             LOGGER.debug ("Found files AND heatFiles - combine and add!");
 
 235             Map <String, Object> combinedFiles = new HashMap <String, Object> ();
 
 236             for (String keyString : files.keySet ()) {
 
 237                 combinedFiles.put (keyString, files.get (keyString));
 
 239             for (String keyString : heatFiles.keySet ()) {
 
 240                 combinedFiles.put (keyString, heatFiles.get (keyString));
 
 242             stack.setFiles (combinedFiles);
 
 244             // Handle case where we have one or neither
 
 246                 stack.setFiles (files);
 
 249                 // setFiles method modified to handle adding a map.
 
 250                 stack.setFiles (heatFiles);
 
 255             // Execute the actual Openstack command to update the Heat stack
 
 256             OpenStackRequest <Void> request = heatClient.getStacks ().update (canonicalName, stack);
 
 257             executeAndRecordOpenstackRequest (request, msoProps);
 
 258         } catch (OpenStackBaseException e) {
 
 259             // Since this came on the 'Update Stack' command, nothing was changed
 
 260             // in the cloud. Rethrow the error as an MSO exception.
 
 261             throw heatExceptionToMsoException (e, UPDATE_STACK);
 
 262         } catch (RuntimeException e) {
 
 264             throw runtimeExceptionToMsoException (e, UPDATE_STACK);
 
 267         // If client has requested a final response, poll for stack completion
 
 268         Stack updateStack = null;
 
 269         if (pollForCompletion) {
 
 270             // Set a time limit on overall polling.
 
 271             // Use the resource (template) timeout for Openstack (expressed in minutes)
 
 272             // and add one poll interval to give Openstack a chance to fail on its own.
 
 273             int createPollInterval = msoProps.getIntProperty (createPollIntervalProp, createPollIntervalDefault);
 
 274             int pollTimeout = (timeoutMinutes * 60) + createPollInterval;
 
 276             boolean loopAgain = true;
 
 279                     updateStack = queryHeatStack (heatClient, canonicalName);
 
 280                     LOGGER.debug (updateStack.getStackStatus () + " (" + canonicalName + ")");
 
 282                         LOGGER.debug("Current stack " + this.getOutputsAsStringBuilder(heatStack).toString());
 
 283                     } catch (Exception e) {
 
 284                         LOGGER.debug("an error occurred trying to print out the current outputs of the stack");
 
 288                     if ("UPDATE_IN_PROGRESS".equals (updateStack.getStackStatus ())) {
 
 289                         // Stack update is still running.
 
 290                         // Sleep and try again unless timeout has been reached
 
 291                         if (pollTimeout <= 0) {
 
 292                             // Note that this should not occur, since there is a timeout specified
 
 293                             // in the Openstack call.
 
 294                                 LOGGER.error (MessageEnum.RA_UPDATE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName, updateStack.getStackStatus(), "", "", MsoLogger.ErrorCode.AvailabilityError, "Update stack timeout");
 
 298                                 Thread.sleep (createPollInterval * 1000L);
 
 299                             } catch (InterruptedException e) {
 
 300                                 // If we are interrupted, we should stop ASAP.
 
 302                                 // Set again the interrupted flag
 
 303                                 Thread.currentThread().interrupt();
 
 306                         pollTimeout -= createPollInterval;
 
 307                         LOGGER.debug("pollTimeout remaining: " + pollTimeout);
 
 311                 } catch (MsoException e) {
 
 312                     // Cannot query the stack. Something is wrong.
 
 314                     // TODO: No way to roll back the stack at this point. What to do?
 
 315                     e.addContext (UPDATE_STACK);
 
 320             if (!"UPDATE_COMPLETE".equals (updateStack.getStackStatus ())) {
 
 321                 LOGGER.error (MessageEnum.RA_UPDATE_STACK_ERR, updateStack.getStackStatus(), updateStack.getStackStatusReason(), "", "", MsoLogger.ErrorCode.DataError, "Update Stack error");
 
 323                 // TODO: No way to roll back the stack at this point. What to do?
 
 324                 // Throw a 'special case' of MsoOpenstackException to report the Heat status
 
 325                 MsoOpenstackException me = null;
 
 326                 if ("UPDATE_IN_PROGRESS".equals (updateStack.getStackStatus ())) {
 
 327                     me = new MsoOpenstackException (0, "", "Stack Update Timeout");
 
 329                     String error = "Stack error (" + updateStack.getStackStatus ()
 
 331                                    + updateStack.getStackStatusReason ();
 
 332                     me = new MsoOpenstackException (0, "", error);
 
 334                 me.addContext (UPDATE_STACK);
 
 339             // Return the current status.
 
 340             updateStack = queryHeatStack (heatClient, canonicalName);
 
 341             if (updateStack != null) {
 
 342                 LOGGER.debug ("UpdateStack, status = " + updateStack.getStackStatus ());
 
 344                 LOGGER.debug ("UpdateStack, stack not found");
 
 347         return new StackInfo (updateStack);
 
 350         private StringBuilder getOutputsAsStringBuilder(Stack heatStack) {
 
 351                 // This should only be used as a utility to print out the stack outputs
 
 353                 StringBuilder sb = new StringBuilder("");
 
 354                 if (heatStack == null) {
 
 355                         sb.append("(heatStack is null)");
 
 358                 List<Output> outputList = heatStack.getOutputs();
 
 359                 if (outputList == null || outputList.isEmpty()) {
 
 360                         sb.append("(outputs is empty)");
 
 363                 Map<String, Object> outputs = new HashMap<String,Object>();
 
 364                 for (Output outputItem : outputList) {
 
 365                         outputs.put(outputItem.getOutputKey(), outputItem.getOutputValue());
 
 368                 sb.append("OUTPUTS:\n");
 
 369                 for (String key : outputs.keySet()) {
 
 370                         sb.append("outputs[" + counter++ + "]: " + key + "=");
 
 371                         Object obj = outputs.get(key);
 
 372                         if (obj instanceof String) {
 
 373                                 sb.append((String)obj +" (a string)");
 
 374                         } else if (obj instanceof JsonNode) {
 
 375                                 sb.append(this.convertNode((JsonNode)obj) + " (a JsonNode)");
 
 376                         } else if (obj instanceof java.util.LinkedHashMap) {
 
 378                                         String str = JSON_MAPPER.writeValueAsString(obj);
 
 379                                         sb.append(str + " (a java.util.LinkedHashMap)");
 
 380                                 } catch (Exception e) {
 
 381                                         sb.append("(a LinkedHashMap value that would not convert nicely)");
 
 383                         } else if (obj instanceof Integer) {
 
 386                                         str = obj.toString() + " (an Integer)\n";
 
 387                                 } catch (Exception e) {
 
 388                                         str = "(an Integer unable to call .toString() on)";
 
 391                         } else if (obj instanceof ArrayList) {
 
 394                                         str = obj.toString() + " (an ArrayList)";
 
 395                                 } catch (Exception e) {
 
 396                                         str = "(an ArrayList unable to call .toString() on?)";
 
 399                         } else if (obj instanceof Boolean) {
 
 402                                         str = obj.toString() + " (a Boolean)";
 
 403                                 } catch (Exception e) {
 
 404                                         str = "(an Boolean unable to call .toString() on?)";
 
 411                                         str = obj.toString() + " (unknown Object type)";
 
 412                                 } catch (Exception e) {
 
 413                                         str = "(a value unable to call .toString() on?)";
 
 423         private String convertNode(final JsonNode node) {
 
 425                         final Object obj = JSON_MAPPER.treeToValue(node, Object.class);
 
 426                         final String json = JSON_MAPPER.writeValueAsString(obj);
 
 428                 } catch (JsonParseException jpe) {
 
 429                         LOGGER.debug("Error converting json to string " + jpe.getMessage());
 
 430                 } catch (Exception e) {
 
 431                         LOGGER.debug("Error converting json to string " + e.getMessage());
 
 433                 return "[Error converting json to string]";