2 * ============LICENSE_START=======================================================
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
14 * http://www.apache.org/licenses/LICENSE-2.0
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=========================================================
24 package org.onap.so.openstack.utils;
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;
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;
57 public class MsoHeatUtilsWithUpdate extends MsoHeatUtils {
59 private static final String UPDATE_STACK = "UpdateStack";
60 private static final Logger logger = LoggerFactory.getLogger(MsoHeatUtilsWithUpdate.class);
62 private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
65 private Environment environment;
67 * Keep these methods around for backward compatibility
70 public StackInfo updateStack (String cloudSiteId,
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,
93 public StackInfo updateStack (String cloudSiteId,
98 Map <String, Object> stackInputs,
99 boolean pollForCompletion,
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,
117 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,
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..
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.
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.
157 * An error will be thrown if the requested Stack already exists in the specified
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.
173 public StackInfo updateStack (String cloudSiteId,
178 Map <String, Object> stackInputs,
179 boolean pollForCompletion,
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;
188 boolean haveFiles = true;
189 if (files == null || files.isEmpty ()) {
192 boolean haveHeatFiles = true;
193 if (heatFiles == null || heatFiles.isEmpty ()) {
194 haveHeatFiles = false;
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);
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 ({}) 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<>();
221 normalized = mapper.readValue(mapper.writeValueAsString(stackInputs), new TypeReference<HashMap<String,Object>>() {});
222 } catch (IOException e1) {
223 logger.debug("could not map json", e1);
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);
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));
246 for (String keyString : heatFiles.keySet ()) {
247 combinedFiles.put (keyString, heatFiles.get (keyString));
249 stack.setFiles (combinedFiles);
251 // Handle case where we have one or neither
253 stack.setFiles (files);
256 // setFiles method modified to handle adding a map.
257 stack.setFiles (heatFiles);
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) {
271 throw runtimeExceptionToMsoException (e, UPDATE_STACK);
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;
283 boolean loopAgain = true;
286 updateStack = queryHeatStack (heatClient, canonicalName);
287 logger.debug("{} ({}) ", updateStack.getStackStatus(), canonicalName);
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);
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.
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());
309 Thread.sleep (createPollInterval * 1000L);
310 } catch (InterruptedException e) {
311 // If we are interrupted, we should stop ASAP.
313 // Set again the interrupted flag
314 Thread.currentThread().interrupt();
317 pollTimeout -= createPollInterval;
318 logger.debug("pollTimeout remaining: {}", pollTimeout);
322 } catch (MsoException e) {
323 // Cannot query the stack. Something is wrong.
325 // TODO: No way to roll back the stack at this point. What to do?
326 e.addContext (UPDATE_STACK);
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());
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");
342 String error = "Stack error (" + updateStack.getStackStatus ()
344 + updateStack.getStackStatusReason ();
345 me = new MsoOpenstackException (0, "", error);
347 me.addContext (UPDATE_STACK);
352 // Return the current status.
353 updateStack = queryHeatStack (heatClient, canonicalName);
354 if (updateStack != null) {
355 logger.debug("UpdateStack, status = {}", updateStack.getStackStatus());
357 logger.debug("UpdateStack, stack not found");
360 return new StackInfoMapper(updateStack).map();
363 private StringBuilder getOutputsAsStringBuilderWithUpdate(Stack heatStack) {
364 // This should only be used as a utility to print out the stack outputs
366 StringBuilder sb = new StringBuilder("");
367 if (heatStack == null) {
368 sb.append("(heatStack is null)");
371 List<Output> outputList = heatStack.getOutputs();
372 if (outputList == null || outputList.isEmpty()) {
373 sb.append("(outputs is empty)");
376 Map<String, Object> outputs = new HashMap<>();
377 for (Output outputItem : outputList) {
378 outputs.put(outputItem.getOutputKey(), outputItem.getOutputValue());
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) {
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)");
397 } else if (obj instanceof Integer) {
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)";
406 } else if (obj instanceof ArrayList) {
409 str = obj.toString() + " (an ArrayList)";
410 } catch (Exception e) {
411 logger.debug("Exception :", e);
412 str = "(an ArrayList unable to call .toString() on?)";
415 } else if (obj instanceof Boolean) {
418 str = obj.toString() + " (a Boolean)";
419 } catch (Exception e) {
420 logger.debug("Exception :", e);
421 str = "(an Boolean unable to call .toString() on?)";
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?)";
441 private String convertNodeWithUpdate(final JsonNode node) {
443 final Object obj = JSON_MAPPER.treeToValue(node, Object.class);
444 final String json = JSON_MAPPER.writeValueAsString(obj);
446 } catch (Exception e) {
447 logger.debug("Error converting json to string {} ", e.getMessage(), e);
449 return "[Error converting json to string]";