Merge changes I8e443077,I51a9106a,I54076323
[so.git] / adapters / mso-adapter-utils / src / main / java / org / onap / so / openstack / utils / MsoHeatUtils.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 import java.io.IOException;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.HashMap;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Map.Entry;
33 import java.util.Set;
34 import java.util.regex.Matcher;
35 import java.util.regex.Pattern;
36 import org.onap.logging.ref.slf4j.ONAPLogConstants;
37 import org.onap.so.adapters.vdu.CloudInfo;
38 import org.onap.so.adapters.vdu.PluginAction;
39 import org.onap.so.adapters.vdu.VduArtifact;
40 import org.onap.so.adapters.vdu.VduArtifact.ArtifactType;
41 import org.onap.so.adapters.vdu.VduException;
42 import org.onap.so.adapters.vdu.VduInstance;
43 import org.onap.so.adapters.vdu.VduModelInfo;
44 import org.onap.so.adapters.vdu.VduPlugin;
45 import org.onap.so.adapters.vdu.VduStateType;
46 import org.onap.so.adapters.vdu.VduStatus;
47 import org.onap.so.cloud.authentication.KeystoneAuthHolder;
48 import org.onap.so.db.catalog.beans.CloudIdentity;
49 import org.onap.so.db.catalog.beans.CloudSite;
50 import org.onap.so.db.catalog.beans.HeatTemplate;
51 import org.onap.so.db.catalog.beans.HeatTemplateParam;
52 import org.onap.so.db.request.beans.CloudApiRequests;
53 import org.onap.so.db.request.beans.InfraActiveRequests;
54 import org.onap.so.db.request.client.RequestsDbClient;
55 import org.onap.so.logger.ErrorCode;
56 import org.onap.so.logger.MessageEnum;
57 import org.onap.so.openstack.beans.CreateStackRequest;
58 import org.onap.so.openstack.beans.HeatStatus;
59 import org.onap.so.openstack.beans.StackInfo;
60 import org.onap.so.openstack.exceptions.MsoCloudSiteNotFound;
61 import org.onap.so.openstack.exceptions.MsoException;
62 import org.onap.so.openstack.exceptions.MsoOpenstackException;
63 import org.onap.so.openstack.exceptions.MsoStackAlreadyExists;
64 import org.onap.so.openstack.exceptions.MsoTenantNotFound;
65 import org.onap.so.openstack.mappers.StackInfoMapper;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
68 import org.slf4j.MDC;
69 import org.springframework.beans.factory.annotation.Autowired;
70 import org.springframework.context.annotation.Primary;
71 import org.springframework.core.env.Environment;
72 import org.springframework.stereotype.Component;
73 import com.fasterxml.jackson.databind.JsonNode;
74 import com.fasterxml.jackson.databind.ObjectMapper;
75 import com.google.common.base.Strings;
76 import com.woorea.openstack.base.client.OpenStackConnectException;
77 import com.woorea.openstack.base.client.OpenStackRequest;
78 import com.woorea.openstack.base.client.OpenStackResponseException;
79 import com.woorea.openstack.heat.Heat;
80 import com.woorea.openstack.heat.model.CreateStackParam;
81 import com.woorea.openstack.heat.model.Events;
82 import com.woorea.openstack.heat.model.Resources;
83 import com.woorea.openstack.heat.model.Stack;
84 import com.woorea.openstack.heat.model.Stack.Output;
85 import com.woorea.openstack.heat.model.Stacks;
86
87
88 @Primary
89 @Component
90 public class MsoHeatUtils extends MsoCommonUtils implements VduPlugin {
91
92     private static final String CREATE_COMPLETE = "CREATE_COMPLETE";
93
94     private static final String DELETE_COMPLETE = "DELETE_COMPLETE";
95
96     private static final String DELETE_IN_PROGRESS = "DELETE_IN_PROGRESS";
97
98     private static final String CREATE_IN_PROGRESS = "CREATE_IN_PROGRESS";
99
100     private static final String DELETE_STACK = "DeleteStack";
101
102     protected static final String HEAT_ERROR = "HeatError";
103
104     protected static final String CREATE_STACK = "CreateStack";
105     public static final String FOUND = "Found: {}";
106     public static final String EXCEPTION_ROLLING_BACK_STACK =
107             "{} Create Stack: Nested exception rolling back stack: {} ";
108     public static final String IN_PROGRESS = "in_progress";
109
110     @Autowired
111     private Environment environment;
112
113     @Autowired
114     RequestsDbClient requestDBClient;
115
116     @Autowired
117     StackStatusHandler statusHandler;
118
119     @Autowired
120     NovaClientImpl novaClient;
121
122     private static final Logger logger = LoggerFactory.getLogger(MsoHeatUtils.class);
123
124     // Properties names and variables (with default values)
125     protected String createPollIntervalProp = "org.onap.so.adapters.po.pollInterval";
126     private String deletePollIntervalProp = "org.onap.so.adapters.po.pollInterval";
127     private String deletePollTimeoutProp = "org.onap.so.adapters.po.pollTimeout";
128     private String pollingMultiplierProp = "org.onap.so.adapters.po.pollMultiplier";
129
130     protected static final String CREATE_POLL_INTERVAL_DEFAULT = "15";
131     private static final String DELETE_POLL_INTERVAL_DEFAULT = "15";
132     private static final String POLLING_MULTIPLIER_DEFAULT = "60";
133
134     private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
135
136     /**
137      * keep this old method signature here to maintain backwards compatibility. keep others as well. this method does
138      * not include environment, files, or heatFiles
139      */
140     public StackInfo createStack(String cloudSiteId, String cloudOwner, String tenantId, String stackName,
141             String heatTemplate, Map<String, ?> stackInputs, boolean pollForCompletion, int timeoutMinutes)
142             throws MsoException {
143         // Just call the new method with the environment & files variable set to null
144         return this.createStack(cloudSiteId, cloudOwner, tenantId, stackName, null, heatTemplate, stackInputs,
145                 pollForCompletion, timeoutMinutes, null, null, null, true);
146     }
147
148     // This method has environment, but not files or heatFiles
149     public StackInfo createStack(String cloudSiteId, String cloudOwner, String tenantId, String stackName,
150             String heatTemplate, Map<String, ?> stackInputs, boolean pollForCompletion, int timeoutMinutes,
151             String environment) throws MsoException {
152         // Just call the new method with the files/heatFiles variables set to null
153         return this.createStack(cloudSiteId, cloudOwner, tenantId, stackName, null, heatTemplate, stackInputs,
154                 pollForCompletion, timeoutMinutes, environment, null, null, true);
155     }
156
157     // This method has environment and files, but not heatFiles.
158     public StackInfo createStack(String cloudSiteId, String cloudOwner, String tenantId, String stackName,
159             String heatTemplate, Map<String, ?> stackInputs, boolean pollForCompletion, int timeoutMinutes,
160             String environment, Map<String, Object> files) throws MsoException {
161         return this.createStack(cloudSiteId, cloudOwner, tenantId, stackName, null, heatTemplate, stackInputs,
162                 pollForCompletion, timeoutMinutes, environment, files, null, true);
163     }
164
165     // This method has environment, files, heatfiles
166     public StackInfo createStack(String cloudSiteId, String cloudOwner, String tenantId, String stackName,
167             String heatTemplate, Map<String, ?> stackInputs, boolean pollForCompletion, int timeoutMinutes,
168             String environment, Map<String, Object> files, Map<String, Object> heatFiles) throws MsoException {
169         return this.createStack(cloudSiteId, cloudOwner, tenantId, stackName, null, heatTemplate, stackInputs,
170                 pollForCompletion, timeoutMinutes, environment, files, heatFiles, true);
171     }
172
173     /**
174      * Create a new Stack in the specified cloud location and tenant. The Heat template and parameter map are passed in
175      * as arguments, along with the cloud access credentials. It is expected that parameters have been validated and
176      * contain at minimum the required parameters for the given template with no extra (undefined) parameters..
177      *
178      * The Stack name supplied by the caller must be unique in the scope of this tenant. However, it should also be
179      * globally unique, as it will be the identifier for the resource going forward in Inventory. This latter is managed
180      * by the higher levels invoking this function.
181      *
182      * The caller may choose to let this function poll Openstack for completion of the stack creation, or may handle
183      * polling itself via separate calls to query the status. In either case, a StackInfo object will be returned
184      * containing the current status. When polling is enabled, a status of CREATED is expected. When not polling, a
185      * status of BUILDING is expected.
186      *
187      * An error will be thrown if the requested Stack already exists in the specified Tenant and Cloud.
188      *
189      * For 1510 - add "environment", "files" (nested templates), and "heatFiles" (get_files) as parameters for
190      * createStack. If environment is non-null, it will be added to the stack. The nested templates and get_file entries
191      * both end up being added to the "files" on the stack. We must combine them before we add them to the stack if
192      * they're both non-null.
193      *
194      * @param cloudSiteId The cloud (may be a region) in which to create the stack.
195      * @param cloudOwner the cloud owner of the cloud site in which to create the stack
196      * @param tenantId The Openstack ID of the tenant in which to create the Stack
197      * @param stackName The name of the stack to create
198      * @param vduModel contains information about the vdu model (added for plugin adapter)
199      * @param heatTemplate The Heat template
200      * @param stackInputs A map of key/value inputs
201      * @param pollForCompletion Indicator that polling should be handled in Java vs. in the client
202      * @param environment An optional yaml-format string to specify environmental parameters
203      * @param files a Map<String, Object> that lists the child template IDs (file is the string, object is an int of
204      *        Template id)
205      * @param heatFiles a Map<String, Object> that lists the get_file entries (fileName, fileBody)
206      * @param backout Donot delete stack on create Failure - defaulted to True
207      * @return A StackInfo object
208      * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception.
209      */
210
211     @SuppressWarnings("unchecked")
212     public StackInfo createStack(String cloudSiteId, String cloudOwner, String tenantId, String stackName,
213             VduModelInfo vduModel, String heatTemplate, Map<String, ?> stackInputs, boolean pollForCompletion,
214             int timeoutMinutes, String environment, Map<String, Object> files, Map<String, Object> heatFiles,
215             boolean backout) throws MsoException {
216
217         stripMultiCloudInputs(stackInputs);
218         CreateStackParam createStack =
219                 createStackParam(stackName, heatTemplate, stackInputs, timeoutMinutes, environment, files, heatFiles);
220         Stack currentStack = createStack(createStack, cloudSiteId, tenantId);
221         currentStack.setStackName(stackName);
222         if (pollForCompletion) {
223             currentStack =
224                     processCreateStack(cloudSiteId, tenantId, timeoutMinutes, backout, currentStack, createStack, true);
225         } else {
226             currentStack =
227                     queryHeatStack(currentStack.getStackName() + "/" + currentStack.getId(), cloudSiteId, tenantId);
228         }
229         return new StackInfoMapper(currentStack).map();
230     }
231
232     /**
233      * @param stackInputs
234      */
235     protected void stripMultiCloudInputs(Map<String, ?> stackInputs) {
236         // Take out the multicloud inputs, if present.
237         for (String key : MsoMulticloudUtils.MULTICLOUD_INPUTS) {
238             if (stackInputs.containsKey(key)) {
239                 stackInputs.remove(key);
240                 if (stackInputs.isEmpty()) {
241                     break;
242                 }
243             }
244         }
245     }
246
247     protected Stack createStack(CreateStackParam stack, String cloudSiteId, String tenantId) throws MsoException {
248         try {
249             OpenStackRequest<Stack> request = getHeatClient(cloudSiteId, tenantId).getStacks().create(stack);
250             saveStackRequest(stack, MDC.get(ONAPLogConstants.MDCs.REQUEST_ID), stack.getStackName());
251             return executeAndRecordOpenstackRequest(request);
252         } catch (OpenStackResponseException e) {
253             if (e.getStatus() == 409) {
254                 MsoStackAlreadyExists me = new MsoStackAlreadyExists(stack.getStackName(), tenantId, cloudSiteId);
255                 me.addContext(CREATE_STACK);
256                 throw me;
257             } else {
258                 logger.error("ERROR STATUS = {},\n{}\n{}", e.getStatus(), e.getMessage(), e.getLocalizedMessage());
259                 throw heatExceptionToMsoException(e, CREATE_STACK);
260             }
261         } catch (OpenStackConnectException e) {
262             throw heatExceptionToMsoException(e, CREATE_STACK);
263         } catch (RuntimeException e) {
264             throw runtimeExceptionToMsoException(e, CREATE_STACK);
265         }
266     }
267
268
269     protected Stack processCreateStack(String cloudSiteId, String tenantId, int timeoutMinutes, boolean backout,
270             Stack heatStack, CreateStackParam stackCreate, boolean keyPairCleanUp) throws MsoException {
271         Stack latestStack = null;
272         try {
273             latestStack = pollStackForStatus(timeoutMinutes, heatStack, CREATE_IN_PROGRESS, cloudSiteId, tenantId);
274         } catch (MsoException me) {
275             logger.error("Exception in Create Stack", me);
276         }
277         return postProcessStackCreate(latestStack, backout, timeoutMinutes, keyPairCleanUp, cloudSiteId, tenantId,
278                 stackCreate);
279     }
280
281     protected Stack postProcessStackCreate(Stack stack, boolean backout, int timeoutMinutes, boolean cleanUpKeyPair,
282             String cloudSiteId, String tenantId, CreateStackParam stackCreate) throws MsoException {
283         if (stack == null) {
284             throw new StackCreationException("Unknown Error in Stack Creation");
285         }
286
287         logger.info("Performing post processing backout: {} cleanUpKeyPair: {}, stack {}", backout, cleanUpKeyPair,
288                 stack);
289         if (!CREATE_COMPLETE.equals(stack.getStackStatus())) {
290             if (cleanUpKeyPair && !Strings.isNullOrEmpty(stack.getStackStatusReason())
291                     && isKeyPairFailure(stack.getStackStatusReason())) {
292                 return handleKeyPairConflict(cloudSiteId, tenantId, stackCreate, timeoutMinutes, backout, stack);
293             }
294             if (!backout) {
295                 logger.info("Status is not CREATE_COMPLETE, stack deletion suppressed");
296                 throw new StackCreationException("Stack rollback suppressed, stack not deleted");
297             } else {
298                 logger.info("Status is not CREATE_COMPLETE, stack deletion will be executed");
299                 String errorMessage = "Stack Creation Failed Openstack Status: " + stack.getStackStatus()
300                         + " Status Reason: " + stack.getStackStatusReason();
301                 try {
302                     Stack deletedStack = handleUnknownCreateStackFailure(stack, timeoutMinutes, cloudSiteId, tenantId);
303                     errorMessage = errorMessage + " , Rollback of Stack Creation completed with status: "
304                             + deletedStack.getStackStatus() + " Status Reason: " + deletedStack.getStackStatusReason();
305                 } catch (StackRollbackException se) {
306                     logger.error("Sync Error Deleting Stack during rollback process", se);
307                     errorMessage = errorMessage + se.getMessage();
308                 } catch (MsoException e) {
309                     logger.error("Sync Error Deleting Stack during rollback", e);
310
311                     errorMessage =
312                             errorMessage + " , Rollback of Stack Creation failed with sync error: " + e.getMessage();
313                 }
314                 throw new StackCreationException(errorMessage);
315             }
316         } else {
317             return stack;
318         }
319
320     }
321
322     protected Stack pollStackForStatus(int timeoutMinutes, Stack stack, String stackStatus, String cloudSiteId,
323             String tenantId) throws MsoException {
324         int pollingFrequency =
325                 Integer.parseInt(this.environment.getProperty(createPollIntervalProp, CREATE_POLL_INTERVAL_DEFAULT));
326         int pollingMultiplier =
327                 Integer.parseInt(this.environment.getProperty(pollingMultiplierProp, POLLING_MULTIPLIER_DEFAULT));
328         int numberOfPollingAttempts = Math.floorDiv((timeoutMinutes * pollingMultiplier), pollingFrequency);
329         Heat heatClient = getHeatClient(cloudSiteId, tenantId);
330         Stack latestStack = null;
331         while (true) {
332             latestStack = queryHeatStack(heatClient, stack.getStackName() + "/" + stack.getId());
333             statusHandler.updateStackStatus(latestStack);
334             logger.debug("Polling: {} ({})", latestStack.getStackStatus(), latestStack.getStackName());
335             if (stackStatus.equals(latestStack.getStackStatus())) {
336                 if (numberOfPollingAttempts <= 0) {
337                     logger.error("Polling of stack timed out with Status: {}", latestStack.getStackStatus());
338                     return latestStack;
339                 }
340                 sleep(pollingFrequency * 1000L);
341                 numberOfPollingAttempts -= 1;
342             } else {
343                 return latestStack;
344             }
345         }
346     }
347
348     protected void saveStackRequest(CreateStackParam request, String requestId, String stackName) {
349         try {
350             ObjectMapper mapper = new ObjectMapper();
351             InfraActiveRequests foundRequest = requestDBClient.getInfraActiveRequestbyRequestId(requestId);
352             CreateStackRequest createStackRequest = new CreateStackRequest();
353             createStackRequest.setEnvironment(request.getEnvironment());
354             createStackRequest.setParameters(request.getParameters());
355             String stackRequest = mapper.writeValueAsString(createStackRequest);
356             CloudApiRequests cloudReq = new CloudApiRequests();
357             cloudReq.setCloudIdentifier(stackName);
358             cloudReq.setRequestBody(stackRequest);
359             cloudReq.setRequestId(requestId);
360             CloudApiRequests foundCloudReq = foundRequest.getCloudApiRequests().stream()
361                     .filter(cloudReqToFind -> stackName.equals(cloudReq.getCloudIdentifier())).findAny().orElse(null);
362             if (foundCloudReq != null) {
363                 foundCloudReq.setRequestBody(stackRequest);
364             } else {
365                 foundRequest.getCloudApiRequests().add(cloudReq);
366             }
367             requestDBClient.updateInfraActiveRequests(foundRequest);
368         } catch (Exception e) {
369             logger.error("Error updating in flight request with Openstack Create Request", e);
370         }
371     }
372
373     protected boolean isKeyPairFailure(String errorMessage) {
374         return Pattern.compile(".*Key pair.*already exists.*").matcher(errorMessage).matches();
375     }
376
377     protected Stack handleUnknownCreateStackFailure(Stack stack, int timeoutMinutes, String cloudSiteId,
378             String tenantId) throws MsoException {
379         if (stack != null && !Strings.isNullOrEmpty(stack.getStackName()) && !Strings.isNullOrEmpty(stack.getId())) {
380             OpenStackRequest<Void> request = getHeatClient(cloudSiteId, tenantId).getStacks()
381                     .deleteByName(stack.getStackName() + "/" + stack.getId());
382             executeAndRecordOpenstackRequest(request);
383             Stack currentStack = pollStackForStatus(timeoutMinutes, stack, DELETE_IN_PROGRESS, cloudSiteId, tenantId);
384             postProcessStackDelete(currentStack);
385             return currentStack;
386         } else {
387             throw new StackCreationException("Cannot Find Stack Name or Id");
388         }
389     }
390
391     protected void postProcessStackDelete(Stack stack) throws MsoException {
392         logger.info("Performing post processing on delete stack {}", stack);
393         if (stack != null && !Strings.isNullOrEmpty(stack.getStackStatus())) {
394             if (!DELETE_COMPLETE.equals(stack.getStackStatus()))
395                 throw new StackRollbackException("Stack Deletion completed with status: " + stack.getStackStatus()
396                         + " Status Reason: " + stack.getStackStatusReason());
397         } else {
398             throw new StackRollbackException("Cannot Find Stack Name or Id");
399         }
400     }
401
402     protected Stack handleKeyPairConflict(String cloudSiteId, String tenantId, CreateStackParam stackCreate,
403             int timeoutMinutes, boolean backout, Stack stack) throws MsoException {
404         logger.info("Keypair conflict found on stack, attempting to clean up");
405         try {
406             Matcher m = Pattern.compile("'([^']+?)'").matcher(stack.getStackStatusReason());
407             if (m.find()) {
408                 novaClient.deleteKeyPair(cloudSiteId, tenantId, m.group(1));
409             }
410         } catch (NovaClientException e) {
411             logger.warn("Could not delete keypair", e);
412         }
413
414         handleUnknownCreateStackFailure(stack, timeoutMinutes, cloudSiteId, tenantId);
415         Stack newStack = createStack(stackCreate, cloudSiteId, tenantId);
416         newStack.setStackName(stackCreate.getStackName());
417         return processCreateStack(cloudSiteId, tenantId, timeoutMinutes, backout, newStack, stackCreate, false);
418     }
419
420     /**
421      * Query for a single stack (by Name) in a tenant. This call will always return a StackInfo object. If the stack
422      * does not exist, an "empty" StackInfo will be returned - containing only the stack name and a status of NOTFOUND.
423      *
424      * @param tenantId The Openstack ID of the tenant in which to query
425      * @param cloudSiteId The cloud identifier (may be a region) in which to query
426      * @param cloudOwner the cloud owner of the cloud site in which to query
427      * @param stackName The name of the stack to query (may be simple or canonical)
428      * @return A StackInfo object
429      * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception.
430      */
431     public StackInfo queryStack(String cloudSiteId, String cloudOwner, String tenantId, String stackName)
432             throws MsoException {
433         logger.debug("Query HEAT stack: {} in tenant {}", stackName, tenantId);
434         Heat heatClient = null;
435         try {
436             heatClient = getHeatClient(cloudSiteId, tenantId);
437         } catch (MsoTenantNotFound e) {
438             // Tenant doesn't exist, so stack doesn't either
439             logger.debug("Tenant with id " + tenantId + "not found.", e);
440             return new StackInfo(stackName, HeatStatus.NOTFOUND);
441         } catch (MsoException me) {
442             // Got an Openstack error. Propagate it
443             logger.error("{} {} Openstack Exception on Token request: ", MessageEnum.RA_CONNECTION_EXCEPTION,
444                     ErrorCode.AvailabilityError.getValue(), me);
445             me.addContext("QueryStack");
446             throw me;
447         }
448
449         // Query the Stack.
450         // An MsoException will propagate transparently to the caller.
451         Stack heatStack = queryHeatStack(heatClient, stackName);
452
453         if (heatStack == null) {
454             // Stack does not exist. Return a StackInfo with status NOTFOUND
455             return new StackInfo(stackName, HeatStatus.NOTFOUND);
456         }
457
458         return new StackInfoMapper(heatStack).map();
459     }
460
461     /**
462      * Delete a stack (by Name/ID) in a tenant. If the stack is not found, it will be considered a successful deletion.
463      * The return value is a StackInfo object which contains the current stack status.
464      *
465      * The client may choose to let the adapter poll Openstack for completion of the stack deletion, or may handle
466      * polling itself via separate query calls. In either case, a StackInfo object will be returned. When polling is
467      * enabled, a final status of NOTFOUND is expected. When not polling, a status of DELETING is expected.
468      *
469      * There is no rollback from a successful stack deletion. A deletion failure will also result in an undefined stack
470      * state - the components may or may not have been all or partially deleted, so the resulting stack must be
471      * considered invalid.
472      *
473      * @param tenantId The Openstack ID of the tenant in which to perform the delete
474      * @param cloudOwner the cloud owner of the cloud site in which to delete the stack
475      * @param cloudSiteId The cloud identifier (may be a region) from which to delete the stack.
476      * @param stackName The name/id of the stack to delete. May be simple or canonical
477      * @param pollForCompletion Indicator that polling should be handled in Java vs. in the client
478      * @return A StackInfo object
479      * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception.
480      * @throws MsoCloudSiteNotFound
481      */
482     public StackInfo deleteStack(String tenantId, String cloudOwner, String cloudSiteId, String stackName,
483             boolean pollForCompletion) throws MsoException {
484         Heat heatClient = null;
485         try {
486             heatClient = getHeatClient(cloudSiteId, tenantId);
487         } catch (MsoTenantNotFound e) {
488             logger.debug("Tenant with id " + tenantId + "not found.", e);
489             return new StackInfo(stackName, HeatStatus.NOTFOUND);
490         } catch (MsoException me) {
491             logger.error("{} {} Openstack Exception on Token request: ", MessageEnum.RA_CONNECTION_EXCEPTION,
492                     ErrorCode.AvailabilityError.getValue(), me);
493             me.addContext(DELETE_STACK);
494             throw me;
495         }
496
497         // OK if stack not found, perform a query first
498         Stack heatStack = queryHeatStack(heatClient, stackName);
499         if (heatStack == null || DELETE_COMPLETE.equals(heatStack.getStackStatus())) {
500             // Not found. Return a StackInfo with status NOTFOUND
501             return new StackInfo(stackName, HeatStatus.NOTFOUND);
502         }
503
504         // Use canonical name "<stack name>/<stack-id>" to delete.
505         // Otherwise, deletion by name returns a 302 redirect.
506         // NOTE: This is specific to the v1 Orchestration API.
507         String canonicalName = heatStack.getStackName() + "/" + heatStack.getId();
508
509         try {
510             OpenStackRequest<Void> request = null;
511             if (null != heatClient) {
512                 request = heatClient.getStacks().deleteByName(canonicalName);
513             } else {
514                 logger.debug("Heat Client is NULL");
515             }
516             executeAndRecordOpenstackRequest(request);
517         } catch (OpenStackResponseException e) {
518             if (e.getStatus() == 404) {
519                 // Not found. We are OK with this. Return a StackInfo with status NOTFOUND
520                 return new StackInfo(stackName, HeatStatus.NOTFOUND);
521             } else {
522                 // Convert the OpenStackResponseException to an MsoOpenstackException
523                 throw heatExceptionToMsoException(e, DELETE_STACK);
524             }
525         } catch (OpenStackConnectException e) {
526             // Error connecting to Openstack instance. Convert to an MsoException
527             throw heatExceptionToMsoException(e, DELETE_STACK);
528         } catch (RuntimeException e) {
529             // Catch-all
530             throw runtimeExceptionToMsoException(e, DELETE_STACK);
531         }
532
533         // Requery the stack for current status.
534         // It will probably still exist with "DELETE_IN_PROGRESS" status.
535         heatStack = queryHeatStack(heatClient, canonicalName);
536         statusHandler.updateStackStatus(heatStack);
537         if (pollForCompletion) {
538             int pollInterval = Integer
539                     .parseInt(this.environment.getProperty(deletePollIntervalProp, "" + DELETE_POLL_INTERVAL_DEFAULT));
540             int pollTimeout = Integer
541                     .parseInt(this.environment.getProperty(deletePollTimeoutProp, "" + DELETE_POLL_INTERVAL_DEFAULT));
542             statusHandler.updateStackStatus(heatStack);
543             // When querying by canonical name, Openstack returns DELETE_COMPLETE status
544             // instead of "404" (which would result from query by stack name).
545             while (heatStack != null && !DELETE_COMPLETE.equals(heatStack.getStackStatus())) {
546                 logger.debug("Stack status: {}", heatStack.getStackStatus());
547
548                 if ("DELETE_FAILED".equals(heatStack.getStackStatus())) {
549                     // Throw a 'special case' of MsoOpenstackException to report the Heat status
550                     String error = "Stack delete error (" + heatStack.getStackStatus() + "): "
551                             + heatStack.getStackStatusReason();
552                     MsoOpenstackException me = new MsoOpenstackException(0, "", error);
553                     me.addContext(DELETE_STACK);
554
555                     // Alarm this condition, stack deletion failed
556
557
558                     throw me;
559                 }
560
561                 if (pollTimeout <= 0) {
562                     logger.error("{} Cloud site: {} Tenant: {} Stack: {} Stack status: {} {} Delete Stack Timeout",
563                             MessageEnum.RA_DELETE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName,
564                             heatStack.getStackStatus(), ErrorCode.AvailabilityError.getValue());
565
566                     // Throw a 'special case' of MsoOpenstackException to report the Heat status
567                     MsoOpenstackException me = new MsoOpenstackException(0, "", "Stack Deletion Timeout");
568                     me.addContext(DELETE_STACK);
569
570                     // Alarm this condition, stack deletion failed
571
572
573                     throw me;
574                 }
575
576                 sleep(pollInterval * 1000L);
577
578                 pollTimeout -= pollInterval;
579                 logger.debug("pollTimeout remaining: {}", pollTimeout);
580
581                 heatStack = queryHeatStack(heatClient, canonicalName);
582             }
583
584             // The stack is gone when this point is reached
585             return new StackInfo(stackName, HeatStatus.NOTFOUND);
586         }
587         // Return the current status (if not polling, the delete may still be in progress)
588         StackInfo stackInfo = new StackInfoMapper(heatStack).map();
589         stackInfo.setName(stackName);
590         return stackInfo;
591     }
592
593     /**
594      * Validate parameters to be passed to Heat template. This method performs three functions: 1. Apply default values
595      * to parameters which have them defined 2. Report any required parameters that are missing. This will generate an
596      * exception in the caller, since stack create/update operations would fail. 3. Report and remove any extraneous
597      * parameters. This will allow clients to pass supersets of parameters and not get errors.
598      *
599      * These functions depend on the HeatTemplate definition from the MSO Catalog DB, along with the input parameter
600      * Map. The output is an updated parameter map. If the parameters are invalid for the template, an
601      * IllegalArgumentException is thrown.
602      */
603     public Map<String, Object> validateStackParams(Map<String, Object> inputParams, HeatTemplate heatTemplate) {
604         // Check that required parameters have been supplied for this template type
605         StringBuilder missingParams = null;
606         List<String> paramList = new ArrayList<>();
607
608         // TODO: Enhance DB to support defaults for Heat Template parameters
609
610         for (HeatTemplateParam parm : heatTemplate.getParameters()) {
611             if (parm.isRequired() && !inputParams.containsKey(parm.getParamName())) {
612                 if (missingParams == null) {
613                     missingParams = new StringBuilder(parm.getParamName());
614                 } else {
615                     missingParams.append("," + parm.getParamName());
616                 }
617             }
618             paramList.add(parm.getParamName());
619         }
620         if (missingParams != null) {
621             // Problem - missing one or more required parameters
622             String error = "Missing Required inputs for HEAT Template: " + missingParams;
623             logger.error("{} for HEAT Template {} Missing Required inputs for HEAT Template: {}",
624                     MessageEnum.RA_MISSING_PARAM, ErrorCode.SchemaError.getValue(), missingParams);
625             throw new IllegalArgumentException(error);
626         }
627
628         // Remove any extraneous parameters (don't throw an error)
629         Map<String, Object> updatedParams = new HashMap<>();
630         List<String> extraParams = new ArrayList<>();
631
632         for (Entry<String, Object> entry : inputParams.entrySet()) {
633             if (!paramList.contains(entry.getKey())) {
634                 // This is not a valid parameter for this template
635                 extraParams.add(entry.getKey());
636             } else {
637                 updatedParams.put(entry.getKey(), entry.getValue());
638             }
639         }
640
641         if (!extraParams.isEmpty()) {
642             logger.warn("{} Heat Stack ({}) extra input params received: {} {}", MessageEnum.RA_GENERAL_WARNING,
643                     heatTemplate.getTemplateName(), extraParams, ErrorCode.DataError.getValue());
644         }
645
646         return updatedParams;
647     }
648
649
650     /**
651      * Get a Heat client for the Openstack Identity service. This requires a 'member'-level userId + password, which
652      * will be retrieved from properties based on the specified cloud Id. The tenant in which to operate must also be
653      * provided.
654      * <p>
655      * On successful authentication, the Heat object will be cached for the tenantID + cloudId so that it can be reused
656      * without reauthenticating with Openstack every time.
657      *
658      * @return an authenticated Heat object
659      */
660     public Heat getHeatClient(String cloudSiteId, String tenantId) throws MsoException {
661         KeystoneAuthHolder keystone = getKeystoneAuthHolder(cloudSiteId, tenantId, "orchestration");
662         Heat heatClient = new Heat(keystone.getServiceUrl());
663         heatClient.token(keystone.getId());
664         return heatClient;
665     }
666
667     /*
668      * Query for a Heat Stack. This function is needed in several places, so a common method is useful. This method
669      * takes an authenticated Heat Client (which internally identifies the cloud & tenant to search), and returns a
670      * Stack object if found, Null if not found, or an MsoOpenstackException if the Openstack API call fails.
671      *
672      * The stack name may be a simple name or a canonical name ("{name}/{id}"). When simple name is used, Openstack
673      * always returns a 302 redirect which results in a 2nd request (to the canonical name). Note that query by
674      * canonical name for a deleted stack returns a Stack object with status "DELETE_COMPLETE" while query by simple
675      * name for a deleted stack returns HTTP 404.
676      *
677      * @param heatClient an authenticated Heat client
678      *
679      * @param stackName the stack name to query
680      *
681      * @return a Stack object that describes the current stack or null if the requested stack doesn't exist.
682      *
683      * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception
684      */
685     public Stack queryHeatStack(Heat heatClient, String stackName) throws MsoException {
686         if (stackName == null) {
687             return null;
688         }
689         try {
690             OpenStackRequest<Stack> request = heatClient.getStacks().byName(stackName);
691             return executeAndRecordOpenstackRequest(request);
692         } catch (OpenStackResponseException e) {
693             logger.error("Error in Query Stack", e);
694             if (e.getStatus() == 404) {
695                 logger.debug("queryHeatStack - stack not found: {}", stackName);
696                 return null;
697             } else {
698                 // Convert the OpenStackResponseException to an MsoOpenstackException
699                 throw heatExceptionToMsoException(e, "QueryStack");
700             }
701         } catch (OpenStackConnectException e) {
702             // Connection to Openstack failed
703             throw heatExceptionToMsoException(e, "QueryAllStack");
704         }
705     }
706
707     public Stack queryHeatStack(String stackName, String cloudSiteId, String tenantId) throws MsoException {
708         if (stackName == null) {
709             return null;
710         }
711         return queryHeatStack(getHeatClient(cloudSiteId, tenantId), stackName);
712     }
713
714
715     public Map<String, Object> queryStackForOutputs(String cloudSiteId, String cloudOwner, String tenantId,
716             String stackName) throws MsoException {
717         logger.debug("MsoHeatUtils.queryStackForOutputs)");
718         StackInfo heatStack = this.queryStack(cloudSiteId, cloudOwner, tenantId, stackName);
719         if (heatStack == null || heatStack.getStatus() == HeatStatus.NOTFOUND) {
720             return null;
721         }
722         return heatStack.getOutputs();
723     }
724
725     public void copyStringOutputsToInputs(Map<String, Object> inputs, Map<String, Object> otherStackOutputs,
726             boolean overWrite) {
727         if (inputs == null || otherStackOutputs == null)
728             return;
729         for (String key : otherStackOutputs.keySet()) {
730             if (!inputs.containsKey(key)) {
731                 Object obj = otherStackOutputs.get(key);
732                 if (obj instanceof String) {
733                     inputs.put(key, (String) otherStackOutputs.get(key));
734                 } else if (obj instanceof JsonNode) {
735                     // This is a bit of mess - but I think it's the least impacting
736                     // let's convert it BACK to a string - then it will get converted back later
737                     try {
738                         String str = this.convertNode((JsonNode) obj);
739                         inputs.put(key, str);
740                     } catch (Exception e) {
741                         logger.debug("DANGER WILL ROBINSON: unable to convert value for JsonNode {} ", key, e);
742                         // effect here is this value will not have been copied to the inputs - and therefore will error
743                         // out downstream
744                     }
745                 } else if (obj instanceof java.util.LinkedHashMap) {
746                     logger.debug("LinkedHashMap - this is showing up as a LinkedHashMap instead of JsonNode");
747                     try {
748                         String str = JSON_MAPPER.writeValueAsString(obj);
749                         inputs.put(key, str);
750                     } catch (Exception e) {
751                         logger.debug("DANGER WILL ROBINSON: unable to convert value for LinkedHashMap {} ", key, e);
752                     }
753                 } else if (obj instanceof Integer) {
754                     try {
755                         String str = "" + obj;
756                         inputs.put(key, str);
757                     } catch (Exception e) {
758                         logger.debug("DANGER WILL ROBINSON: unable to convert value for Integer {} ", key, e);
759                     }
760                 } else {
761                     try {
762                         String str = obj.toString();
763                         inputs.put(key, str);
764                     } catch (Exception e) {
765                         logger.debug("DANGER WILL ROBINSON: unable to convert value for Other {} ({}) ", key,
766                                 e.getMessage(), e);
767                         // effect here is this value will not have been copied to the inputs - and therefore will error
768                         // out downstream
769                     }
770                 }
771             }
772         }
773         return;
774     }
775
776     private String convertNode(final JsonNode node) {
777         try {
778             final Object obj = JSON_MAPPER.treeToValue(node, Object.class);
779             return JSON_MAPPER.writeValueAsString(obj);
780         } catch (Exception e) {
781             logger.debug("Error converting json to string {} ", e.getMessage(), e);
782         }
783         return "[Error converting json to string]";
784     }
785
786
787     protected StringBuilder getOutputsAsStringBuilder(Stack heatStack) {
788         // This should only be used as a utility to print out the stack outputs
789         // to the log
790         StringBuilder sb = new StringBuilder("");
791         if (heatStack == null) {
792             sb.append("(heatStack is null)");
793             return sb;
794         }
795         List<Output> outputList = heatStack.getOutputs();
796         if (outputList == null || outputList.isEmpty()) {
797             sb.append("(outputs is empty)");
798             return sb;
799         }
800         Map<String, Object> outputs = new HashMap<>();
801         for (Output outputItem : outputList) {
802             outputs.put(outputItem.getOutputKey(), outputItem.getOutputValue());
803         }
804         int counter = 0;
805         sb.append("OUTPUTS:\n");
806         for (String key : outputs.keySet()) {
807             sb.append("outputs[").append(counter++).append("]: ").append(key).append("=");
808             Object obj = outputs.get(key);
809             if (obj instanceof String) {
810                 sb.append((String) obj).append(" (a string)");
811             } else if (obj instanceof JsonNode) {
812                 sb.append(this.convertNode((JsonNode) obj)).append(" (a JsonNode)");
813             } else if (obj instanceof java.util.LinkedHashMap) {
814                 try {
815                     String str = JSON_MAPPER.writeValueAsString(obj);
816                     sb.append(str).append(" (a java.util.LinkedHashMap)");
817                 } catch (Exception e) {
818                     logger.debug("Exception :", e);
819                     sb.append("(a LinkedHashMap value that would not convert nicely)");
820                 }
821             } else if (obj instanceof Integer) {
822                 String str = "";
823                 try {
824                     str = obj.toString() + " (an Integer)\n";
825                 } catch (Exception e) {
826                     logger.debug("Exception :", e);
827                     str = "(an Integer unable to call .toString() on)";
828                 }
829                 sb.append(str);
830             } else if (obj instanceof ArrayList) {
831                 String str = "";
832                 try {
833                     str = obj.toString() + " (an ArrayList)";
834                 } catch (Exception e) {
835                     logger.debug("Exception :", e);
836                     str = "(an ArrayList unable to call .toString() on?)";
837                 }
838                 sb.append(str);
839             } else if (obj instanceof Boolean) {
840                 String str = "";
841                 try {
842                     str = obj.toString() + " (a Boolean)";
843                 } catch (Exception e) {
844                     logger.debug("Exception :", e);
845                     str = "(an Boolean unable to call .toString() on?)";
846                 }
847                 sb.append(str);
848             } else {
849                 String str = "";
850                 try {
851                     str = obj.toString() + " (unknown Object type)";
852                 } catch (Exception e) {
853                     logger.debug("Exception :", e);
854                     str = "(a value unable to call .toString() on?)";
855                 }
856                 sb.append(str);
857             }
858             sb.append("\n");
859         }
860         sb.append("[END]");
861         return sb;
862     }
863
864
865     public void copyBaseOutputsToInputs(Map<String, Object> inputs, Map<String, Object> otherStackOutputs,
866             List<String> paramNames, Map<String, String> aliases) {
867         if (inputs == null || otherStackOutputs == null)
868             return;
869         for (String key : otherStackOutputs.keySet()) {
870             if (paramNames != null) {
871                 if (!paramNames.contains(key) && !aliases.containsKey(key)) {
872                     logger.debug("\tParameter {} is NOT defined to be in the template - do not copy to inputs", key);
873                     continue;
874                 }
875                 if (aliases.containsKey(key)) {
876                     logger.debug("Found an alias! Will move {} to {}", key, aliases.get(key));
877                     Object obj = otherStackOutputs.get(key);
878                     key = aliases.get(key);
879                     otherStackOutputs.put(key, obj);
880                 }
881             }
882             if (!inputs.containsKey(key)) {
883                 Object obj = otherStackOutputs.get(key);
884                 logger.debug("\t**Adding {} to inputs (.toString()={}", key, obj.toString());
885                 if (obj instanceof String) {
886                     logger.debug("\t\t**A String");
887                     inputs.put(key, obj);
888                 } else if (obj instanceof Integer) {
889                     logger.debug("\t\t**An Integer");
890                     inputs.put(key, obj);
891                 } else if (obj instanceof JsonNode) {
892                     logger.debug("\t\t**A JsonNode");
893                     inputs.put(key, obj);
894                 } else if (obj instanceof Boolean) {
895                     logger.debug("\t\t**A Boolean");
896                     inputs.put(key, obj);
897                 } else if (obj instanceof java.util.LinkedHashMap) {
898                     logger.debug("\t\t**A java.util.LinkedHashMap **");
899                     inputs.put(key, obj);
900                 } else if (obj instanceof java.util.ArrayList) {
901                     logger.debug("\t\t**An ArrayList");
902                     inputs.put(key, obj);
903                 } else {
904                     logger.debug("\t\t**UNKNOWN OBJECT TYPE");
905                     inputs.put(key, obj);
906                 }
907             } else {
908                 logger.debug("key={} is already in the inputs - will not overwrite", key);
909             }
910         }
911         return;
912     }
913
914     public List<String> convertCdlToArrayList(String cdl) {
915         String cdl2 = cdl.trim();
916         String cdl3;
917         if (cdl2.startsWith("[") && cdl2.endsWith("]")) {
918             cdl3 = cdl2.substring(1, cdl2.lastIndexOf("]"));
919         } else {
920             cdl3 = cdl2;
921         }
922         return new ArrayList<>(Arrays.asList(cdl3.split(",")));
923     }
924
925     /**
926      * New with 1707 - this method will convert all the String *values* of the inputs to their "actual" object type
927      * (based on the param type: in the db - which comes from the template): (heat variable type) -> java Object type
928      * string -> String number -> Integer json -> marshal object to json comma_delimited_list -> ArrayList boolean ->
929      * Boolean if any of the conversions should fail, we will default to adding it to the inputs as a string - see if
930      * Openstack can handle it. Also, will remove any params that are extra. Any aliases will be converted to their
931      * appropriate name (anyone use this feature?)
932      * 
933      * @param inputs - the Map<String, String> of the inputs received on the request
934      * @param template the HeatTemplate object - this is so we can also verify if the param is valid for this template
935      * @return HashMap<String, Object> of the inputs, cleaned and converted
936      */
937     public Map<String, Object> convertInputMap(Map<String, Object> inputs, HeatTemplate template) {
938         HashMap<String, Object> newInputs = new HashMap<>();
939         HashMap<String, HeatTemplateParam> params = new HashMap<>();
940         HashMap<String, HeatTemplateParam> paramAliases = new HashMap<>();
941
942         if (inputs == null) {
943             return new HashMap<>();
944         }
945         try {
946             Set<HeatTemplateParam> paramSet = template.getParameters();
947         } catch (Exception e) {
948             logger.debug("Exception occurred in convertInputMap {} :", e.getMessage(), e);
949         }
950
951         for (HeatTemplateParam htp : template.getParameters()) {
952             params.put(htp.getParamName(), htp);
953             if (htp.getParamAlias() != null && !"".equals(htp.getParamAlias())) {
954                 logger.debug("\tFound ALIAS {} -> {}", htp.getParamName(), htp.getParamAlias());
955                 paramAliases.put(htp.getParamAlias(), htp);
956             }
957         }
958
959         for (String key : inputs.keySet()) {
960             boolean alias = false;
961             String realName = null;
962             if (!params.containsKey(key)) {
963                 // add check here for an alias
964                 if (!paramAliases.containsKey(key)) {
965                     continue;
966                 } else {
967                     alias = true;
968                     realName = paramAliases.get(key).getParamName();
969                 }
970             }
971             String type = params.get(key).getParamType();
972             if (type == null || "".equals(type)) {
973                 logger.debug("**PARAM_TYPE is null/empty for {}, will default to string", key);
974                 type = "string";
975             }
976             if ("string".equalsIgnoreCase(type)) {
977                 // Easiest!
978                 String str = inputs.get(key) != null ? inputs.get(key).toString() : null;
979                 if (alias)
980                     newInputs.put(realName, str);
981                 else
982                     newInputs.put(key, str);
983             } else if ("number".equalsIgnoreCase(type)) {
984                 String integerString = inputs.get(key) != null ? inputs.get(key).toString() : null;
985                 Integer anInteger = null;
986                 try {
987                     anInteger = Integer.parseInt(integerString);
988                 } catch (Exception e) {
989                     logger.debug("Unable to convert {} to an integer!!", integerString, e);
990                     anInteger = null;
991                 }
992                 if (anInteger != null) {
993                     if (alias)
994                         newInputs.put(realName, anInteger);
995                     else
996                         newInputs.put(key, anInteger);
997                 } else {
998                     if (alias)
999                         newInputs.put(realName, integerString);
1000                     else
1001                         newInputs.put(key, integerString);
1002                 }
1003             } else if ("json".equalsIgnoreCase(type)) {
1004                 Object jsonObj = inputs.get(key);
1005                 Object json;
1006                 try {
1007                     if (jsonObj instanceof String) {
1008                         json = JSON_MAPPER.readTree(jsonObj.toString());
1009                     } else {
1010                         // will already marshal to json without intervention
1011                         json = jsonObj;
1012                     }
1013                 } catch (IOException e) {
1014                     logger.error("failed to map to json, directly converting to string instead", e);
1015                     json = jsonObj.toString();
1016                 }
1017                 if (alias)
1018                     newInputs.put(realName, json);
1019                 else
1020                     newInputs.put(key, json);
1021             } else if ("comma_delimited_list".equalsIgnoreCase(type)) {
1022                 String commaSeparated = inputs.get(key) != null ? inputs.get(key).toString() : null;
1023                 try {
1024                     List<String> anArrayList = this.convertCdlToArrayList(commaSeparated);
1025                     if (alias)
1026                         newInputs.put(realName, anArrayList);
1027                     else
1028                         newInputs.put(key, anArrayList);
1029                 } catch (Exception e) {
1030                     logger.debug("Unable to convert {} to an ArrayList!!", commaSeparated, e);
1031                     if (alias)
1032                         newInputs.put(realName, commaSeparated);
1033                     else
1034                         newInputs.put(key, commaSeparated);
1035                 }
1036             } else if ("boolean".equalsIgnoreCase(type)) {
1037                 String booleanString = inputs.get(key) != null ? inputs.get(key).toString() : null;
1038                 Boolean aBool = Boolean.valueOf(booleanString);
1039                 if (alias)
1040                     newInputs.put(realName, aBool);
1041                 else
1042                     newInputs.put(key, aBool);
1043             } else {
1044                 // it's null or something undefined - just add it back as a String
1045                 String str = inputs.get(key).toString();
1046                 if (alias)
1047                     newInputs.put(realName, str);
1048                 else
1049                     newInputs.put(key, str);
1050             }
1051         }
1052         return newInputs;
1053     }
1054
1055     /*
1056      * This helpful method added for Valet
1057      */
1058     public String getCloudSiteKeystoneUrl(String cloudSiteId) throws MsoCloudSiteNotFound {
1059         String keystoneUrl = null;
1060         try {
1061             CloudSite cloudSite =
1062                     cloudConfig.getCloudSite(cloudSiteId).orElseThrow(() -> new MsoCloudSiteNotFound(cloudSiteId));
1063             CloudIdentity cloudIdentity = cloudSite.getIdentityService();
1064             keystoneUrl = cloudIdentity.getIdentityUrl();
1065         } catch (Exception e) {
1066             throw new MsoCloudSiteNotFound(cloudSiteId);
1067         }
1068         if (keystoneUrl == null || keystoneUrl.isEmpty()) {
1069             throw new MsoCloudSiteNotFound(cloudSiteId);
1070         }
1071         return keystoneUrl;
1072     }
1073
1074
1075     /*******************************************************************************
1076      *
1077      * Methods (and associated utilities) to implement the VduPlugin interface
1078      *
1079      *******************************************************************************/
1080
1081     /**
1082      * VduPlugin interface for instantiate function.
1083      *
1084      * Translate the VduPlugin parameters to the corresponding 'createStack' parameters, and then invoke the existing
1085      * function.
1086      */
1087     @Override
1088     public VduInstance instantiateVdu(CloudInfo cloudInfo, String instanceName, Map<String, Object> inputs,
1089             VduModelInfo vduModel, boolean rollbackOnFailure) throws VduException {
1090         String cloudSiteId = cloudInfo.getCloudSiteId();
1091         String cloudOwner = cloudInfo.getCloudOwner();
1092         String tenantId = cloudInfo.getTenantId();
1093
1094         // Translate the VDU ModelInformation structure to that which is needed for
1095         // creating the Heat stack. Loop through the artifacts, looking specifically
1096         // for MAIN_TEMPLATE and ENVIRONMENT. Any other artifact will
1097         // be attached as a FILE.
1098         String heatTemplate = null;
1099         Map<String, Object> nestedTemplates = new HashMap<>();
1100         Map<String, Object> files = new HashMap<>();
1101         String heatEnvironment = null;
1102
1103         for (VduArtifact vduArtifact : vduModel.getArtifacts()) {
1104             if (vduArtifact.getType() == ArtifactType.MAIN_TEMPLATE) {
1105                 heatTemplate = new String(vduArtifact.getContent());
1106             } else if (vduArtifact.getType() == ArtifactType.NESTED_TEMPLATE) {
1107                 nestedTemplates.put(vduArtifact.getName(), new String(vduArtifact.getContent()));
1108             } else if (vduArtifact.getType() == ArtifactType.ENVIRONMENT) {
1109                 heatEnvironment = new String(vduArtifact.getContent());
1110             }
1111         }
1112
1113         try {
1114             StackInfo stackInfo =
1115                     createStack(cloudSiteId, cloudOwner, tenantId, instanceName, vduModel, heatTemplate, inputs, true, // poll
1116                                                                                                                        // for
1117                                                                                                                        // completion
1118                             vduModel.getTimeoutMinutes(), heatEnvironment, nestedTemplates, files, rollbackOnFailure);
1119
1120             // Populate a vduInstance from the StackInfo
1121             return stackInfoToVduInstance(stackInfo);
1122         } catch (Exception e) {
1123             throw new VduException("MsoHeatUtils (instantiateVDU): createStack Exception", e);
1124         }
1125     }
1126
1127
1128     /**
1129      * VduPlugin interface for query function.
1130      */
1131     @Override
1132     public VduInstance queryVdu(CloudInfo cloudInfo, String instanceId) throws VduException {
1133         String cloudSiteId = cloudInfo.getCloudSiteId();
1134         String cloudOwner = cloudInfo.getCloudOwner();
1135         String tenantId = cloudInfo.getTenantId();
1136
1137         try {
1138             // Query the Cloudify Deployment object and populate a VduInstance
1139             StackInfo stackInfo = queryStack(cloudSiteId, cloudOwner, tenantId, instanceId);
1140
1141             return stackInfoToVduInstance(stackInfo);
1142         } catch (Exception e) {
1143             throw new VduException("MsoHeatUtile (queryVdu): queryStack Exception ", e);
1144         }
1145     }
1146
1147
1148     /**
1149      * VduPlugin interface for delete function.
1150      */
1151     @Override
1152     public VduInstance deleteVdu(CloudInfo cloudInfo, String instanceId, int timeoutMinutes) throws VduException {
1153         String cloudSiteId = cloudInfo.getCloudSiteId();
1154         String cloudOwner = cloudInfo.getCloudOwner();
1155         String tenantId = cloudInfo.getTenantId();
1156
1157         try {
1158             // Delete the Heat stack
1159             StackInfo stackInfo = deleteStack(tenantId, cloudOwner, cloudSiteId, instanceId, true);
1160
1161             // Populate a VduInstance based on the deleted Cloudify Deployment object
1162             VduInstance vduInstance = stackInfoToVduInstance(stackInfo);
1163
1164             // Override return state to DELETED (HeatUtils sets to NOTFOUND)
1165             vduInstance.getStatus().setState(VduStateType.DELETED);
1166
1167             return vduInstance;
1168         } catch (Exception e) {
1169             throw new VduException("Delete VDU Exception", e);
1170         }
1171     }
1172
1173
1174     /**
1175      * VduPlugin interface for update function.
1176      *
1177      * Update is currently not supported in the MsoHeatUtils implementation of VduPlugin. Just return a VduException.
1178      *
1179      */
1180     @Override
1181     public VduInstance updateVdu(CloudInfo cloudInfo, String instanceId, Map<String, Object> inputs,
1182             VduModelInfo vduModel, boolean rollbackOnFailure) throws VduException {
1183         throw new VduException("MsoHeatUtils: updateVdu interface not supported");
1184     }
1185
1186
1187     /*
1188      * Convert the local DeploymentInfo object (Cloudify-specific) to a generic VduInstance object
1189      */
1190     protected VduInstance stackInfoToVduInstance(StackInfo stackInfo) {
1191         VduInstance vduInstance = new VduInstance();
1192
1193         // The full canonical name as the instance UUID
1194         vduInstance.setVduInstanceId(stackInfo.getCanonicalName());
1195         vduInstance.setVduInstanceName(stackInfo.getName());
1196
1197         // Copy inputs and outputs
1198         vduInstance.setInputs(stackInfo.getParameters());
1199         vduInstance.setOutputs(stackInfo.getOutputs());
1200
1201         // Translate the status elements
1202         vduInstance.setStatus(stackStatusToVduStatus(stackInfo));
1203
1204         return vduInstance;
1205     }
1206
1207     private VduStatus stackStatusToVduStatus(StackInfo stackInfo) {
1208         VduStatus vduStatus = new VduStatus();
1209
1210         // Map the status fields to more generic VduStatus.
1211         // There are lots of HeatStatus values, so this is a bit long...
1212         HeatStatus heatStatus = stackInfo.getStatus();
1213         String statusMessage = stackInfo.getStatusMessage();
1214
1215         if (heatStatus == HeatStatus.INIT || heatStatus == HeatStatus.BUILDING) {
1216             vduStatus.setState(VduStateType.INSTANTIATING);
1217             vduStatus.setLastAction((new PluginAction("create", IN_PROGRESS, statusMessage)));
1218         } else if (heatStatus == HeatStatus.NOTFOUND) {
1219             vduStatus.setState(VduStateType.NOTFOUND);
1220         } else if (heatStatus == HeatStatus.CREATED) {
1221             vduStatus.setState(VduStateType.INSTANTIATED);
1222             vduStatus.setLastAction((new PluginAction("create", "complete", statusMessage)));
1223         } else if (heatStatus == HeatStatus.UPDATED) {
1224             vduStatus.setState(VduStateType.INSTANTIATED);
1225             vduStatus.setLastAction((new PluginAction("update", "complete", statusMessage)));
1226         } else if (heatStatus == HeatStatus.UPDATING) {
1227             vduStatus.setState(VduStateType.UPDATING);
1228             vduStatus.setLastAction((new PluginAction("update", IN_PROGRESS, statusMessage)));
1229         } else if (heatStatus == HeatStatus.DELETING) {
1230             vduStatus.setState(VduStateType.DELETING);
1231             vduStatus.setLastAction((new PluginAction("delete", IN_PROGRESS, statusMessage)));
1232         } else if (heatStatus == HeatStatus.FAILED) {
1233             vduStatus.setState(VduStateType.FAILED);
1234             vduStatus.setErrorMessage(stackInfo.getStatusMessage());
1235         } else {
1236             vduStatus.setState(VduStateType.UNKNOWN);
1237         }
1238
1239         return vduStatus;
1240     }
1241
1242     public Resources queryStackResources(String cloudSiteId, String tenantId, String stackName, int nestedDepth)
1243             throws MsoException {
1244         Heat heatClient = getHeatClient(cloudSiteId, tenantId);
1245         OpenStackRequest<Resources> request =
1246                 heatClient.getResources().listResources(stackName).queryParam("nested_depth", nestedDepth);
1247         return executeAndRecordOpenstackRequest(request, false);
1248     }
1249
1250     public Events queryStackEvents(String cloudSiteId, String tenantId, String stackName, String stackId,
1251             int nestedDepth) throws MsoException {
1252         Heat heatClient = getHeatClient(cloudSiteId, tenantId);
1253         OpenStackRequest<Events> request =
1254                 heatClient.getEvents().listEvents(stackName, stackId).queryParam("nested_depth", nestedDepth);
1255         return executeAndRecordOpenstackRequest(request, false);
1256     }
1257
1258     public Stacks queryStacks(String cloudSiteId, String tenantId, int limit, String marker)
1259             throws MsoCloudSiteNotFound, HeatClientException {
1260         Heat heatClient;
1261         try {
1262             heatClient = getHeatClient(cloudSiteId, tenantId);
1263         } catch (MsoException e) {
1264             logger.error("Error Creating Heat Client", e);
1265             throw new HeatClientException("Error Creating Heat Client", e);
1266         }
1267         OpenStackRequest<Stacks> request =
1268                 heatClient.getStacks().list().queryParam("limit", limit).queryParam("marker", marker);
1269         return executeAndRecordOpenstackRequest(request, false);
1270     }
1271
1272     public <R> R executeHeatClientRequest(String url, String cloudSiteId, String tenantId, Class<R> returnType)
1273             throws MsoException {
1274         Heat heatClient = getHeatClient(cloudSiteId, tenantId);
1275         OpenStackRequest<R> request = heatClient.get(url, returnType);
1276         return executeAndRecordOpenstackRequest(request, false);
1277     }
1278
1279     protected void sleep(long time) {
1280         try {
1281             Thread.sleep(time);
1282         } catch (InterruptedException e) {
1283             logger.debug("Thread interrupted while sleeping", e);
1284             Thread.currentThread().interrupt();
1285         }
1286     }
1287
1288 }