diff options
author | Benjamin, Max (mb388a) <mb388a@us.att.com> | 2018-07-30 15:56:09 -0400 |
---|---|---|
committer | Benjamin, Max (mb388a) <mb388a@us.att.com> | 2018-07-31 11:09:25 -0400 |
commit | 5a6a6de6f1a26a1897e4917a0df613e25a24eb70 (patch) | |
tree | 59a968f27b4b603aacc9d5e7b51fb598aeec5321 /adapters/mso-adapter-utils/src/main/java/org/onap | |
parent | b6dc38501f3b746426b42d9de4cc883d894149e8 (diff) |
Containerization feature of SO
Change-Id: I95381232eeefcd247a66a5cec370a8ce1c288e18
Issue-ID: SO-670
Signed-off-by: Benjamin, Max (mb388a) <mb388a@us.att.com>
Diffstat (limited to 'adapters/mso-adapter-utils/src/main/java/org/onap')
35 files changed, 8008 insertions, 0 deletions
diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/adapters/vdu/VduPlugin.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/adapters/vdu/VduPlugin.java new file mode 100644 index 0000000000..ff30c0ee70 --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/adapters/vdu/VduPlugin.java @@ -0,0 +1,186 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.adapters.vdu; + +/** + * This interface defines a common API for template-based cloud deployments. + * The methods here should be adaptable for Openstack (Heat), Cloudify (TOSCA), + * Aria (TOSCA), Multi-VIM (TBD), and others (e.g. Azure Resource Manager). + * + * The deployed instances are referred to here as Virtual Deployment Units (VDUs). + * The package of templates that define a give VDU is referred to as its blueprint. + * + * Template-based orchestrators all follow a similar template/blueprint model. + * - One main template that is the top level definition + * - Optional nested templates referenced/included by the main template + * - Optional files attached to the template package, typically containing + * configuration files, install scripts, orchestration scripts, etc. + * + * The main template also defines the required inputs for creating a new instance, + * and output values exposed by successfully deployed instances. Inputs and outputs + * may include simple or complex (JSON) data types. + * + * Each implementation of this interface is expected to understand the MSO CloudConfig + * to obtain the credentials for its sub-orchestrator and the targeted cloud. + * The sub-orchestrator may have different credentials from the cloud (e.g. an Aria + * instance in front of an Openstack cloud) or they may be the same (e.g. Heat) + */ +import java.util.Map; + +public interface VduPlugin { + + /** + * The instantiateVdu interface deploys a new VDU instance from a vdu model package. + * + * For some VIMs, this may be a single command (e.g. Heat -> create stack) or may + * require a series of API calls (e.g. Cloudify -> upload blueprint, create deployment, + * execute install workflow). These details are hidden within the plug-in implementation. + * The instantiation should be fully completed before returning. On failures, this + * method is expected to back out the attempt, leaving the cloud in its previous state. + * + * It is expected that parameters have been validated and contain at minimum the + * required parameters for the given template with no extra parameters. + * + * The VDU name supplied by the caller will be globally unique, and identify the artifact + * in A&AI. Inventory is managed by the higher levels invoking this function. + * + * @param cloudInfo The target cloud + tenant identifiers for the VDU. + * @param instanceName A unique name for the VDU instance to update. + * @param inputs A map of key/value inputs. Values may be strings, numbers, or JSON objects. + * Will completely replace any inputs provided on the original instantiation. + * @param vduModel Object containing the collection of templates and files that comprise + * the blueprint for this VDU. + * @param rollbackOnFailure Flag to preserve or roll back the update on Failure. Should normally + * be True except in troubleshooting/debug cases. Might not be supported in all plug-ins. + * + * @return A VduInstance object + * @throws VduException Thrown if the sub-orchestrator API calls fail or if a timeout occurs. + * Various subclasses of VduException may be thrown. + */ + public VduInstance instantiateVdu ( + CloudInfo cloudInfo, + String instanceName, + Map<String,Object> inputs, + VduModelInfo vduModel, + boolean rollbackOnFailure) + throws VduException; + + /** + * Query a deployed VDU instance. This call will return a VduInstance object, or null + * if the deployment does not exist. + * + * Some VIM orchestrators identify deployment instances by string UUIDs, and others + * by integers. In the latter case, the ID will be passed in as a numeric string. + * + * The returned VduInstance object contains the input and output parameter maps, + * as well as other properties of the deployment (name, status, last action, etc.). + * + * @param cloudInfo The target cloud + tenant identifiers for the VDU. + * @param vduInstanceId The ID of the deployment to query + * + * @return A VduInstance object + * @throws VduException Thrown if the sub-orchestrator API calls fail or if a timeout occurs. + * Various subclasses of VduException may be thrown. + */ + public VduInstance queryVdu ( + CloudInfo cloudInfo, + String vduInstanceId) + throws VduException; + + + /** + * Delete a VDU instance by ID. If the VIM sub-orchestrator supports pre-installation + * of blueprints/models, the blueprint itself may remain installed. This is recommended, + * since other VDU instances may be using it. + * + * Some VIM orchestrators identify deployment instances by string UUIDs, and others + * by integers. In the latter case, the ID will be passed in as a numeric string. + * + * For some VIMs, deletion may be a single command (e.g. Heat -> delete stack) or a + * series of API calls (e.g. Cloudify -> execute uninstall workflow, delete deployment). + * These details are hidden within the plug-in implementation. The deletion should be + * fully completed before returning. + * + * The successful return is a VduInstance object which contains the state of the VDU + * just prior to deletion, with a status of DELETED. If the deployment was not found, + * the VduInstance object should be empty (with a status of NOTFOUND). + * There is no rollback from a successful deletion. + * + * A deletion failure will result in an undefined deployment state - the components may + * or may not have been all or partially uninstalled, so the resulting deployment must + * be considered invalid. + * + * @param cloudInfo The target cloud + tenant identifiers for the VDU. + * @param instanceId The unique id of the deployment to delete. + * @param timeoutMinutes Timeout after which the delete action will be cancelled. + * Consider sending the entire model here, if it may be of use to the plug-in? + * + * @return A VduInstance object, representing its state just prior to deletion. + * + * @throws VduException Thrown if the API calls fail or if a timeout occurs. + * Various subclasses of VduException may be thrown. + */ + public VduInstance deleteVdu ( + CloudInfo cloudInfo, + String instanceId, + int timeoutMinutes) + throws VduException; + + + /** + * The updateVdu interface attempts to update a VDU in-place, using either new inputs or + * a new model definition (i.e. updated templates/blueprints). This depends on the + * capabilities of the targeted sub-orchestrator, as not all implementations are expected + * to support this ability. It is primary included initially only for Heat. + * + * It is expected that parameters have been validated and contain at minimum the required + * parameters for the given template with no extra parameters. The VDU instance name cannot + * be updated. + * + * The update should be fully completed before returning. The successful return is a + * VduInstance object containing the updated VDU state. + * + * An update failure will result in an undefined deployment state - the components may + * or may not have been all or partially modified, deleted, recreated, etc. So the resulting + * VDU must be considered invalid. + * + * @param cloudInfo The target cloud + tenant identifiers for the VDU. + * @param instanceId The unique ID for the VDU instance to update. + * @param inputs A map of key/value inputs. Values may be strings, numbers, or JSON objects. + * Will completely replace any inputs provided on the original instantiation. + * @param vduModel Object containing the collection of templates and files that comprise + * the blueprint for this VDU. + * @param rollbackOnFailure Flag to preserve or roll back the update on Failure. Should normally + * be True except in troubleshooting/debug cases. Might not be supported in all plug-ins. + * + * @return A VduInfo object + * @throws VduException Thrown if the sub-orchestrator API calls fail or if a timeout occurs. + * Various subclasses of VduException may be thrown. + */ + public VduInstance updateVdu ( + CloudInfo cloudInfo, + String instanceId, + Map<String,Object> inputs, + VduModelInfo vduModel, + boolean rollbackOnFailure) + throws VduException; + +}
\ No newline at end of file diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/Application.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/Application.java new file mode 100644 index 0000000000..bc04b09588 --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/Application.java @@ -0,0 +1,38 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.cloud; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@SpringBootApplication(scanBasePackages = { "org.onap"}) +@EnableJpaRepositories({"org.onap.so.db.catalog.data.repository", "org.onap.so.db.request.data.repository"}) +@EntityScan({"org.onap.so.db.catalog.beans", "org.onap.so.db.request.beans"}) +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + System.getProperties().setProperty("mso.db", "MARIADB"); + System.getProperties().setProperty("server.name", "Springboot"); + } +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/AuthenticationType.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/AuthenticationType.java new file mode 100644 index 0000000000..7cb2222525 --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/AuthenticationType.java @@ -0,0 +1,25 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.cloud; + +public enum AuthenticationType { + USERNAME_PASSWORD, RACKSPACE_APIKEY; +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/CloudConfig.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/CloudConfig.java new file mode 100644 index 0000000000..ef5f8232e0 --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/CloudConfig.java @@ -0,0 +1,216 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.cloud; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; + +import javax.annotation.PostConstruct; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.EqualsBuilder; + +/** + * JavaBean JSON class for a CloudConfig. This bean maps a JSON-format cloud + * configuration file to Java. The CloudConfig contains information about + * Openstack cloud configurations. It includes: + * - CloudIdentity objects,representing DCP nodes (Openstack Identity Service) + * - CloudSite objects, representing LCP nodes (Openstack Compute & other services) + * + * Note that this is only used to access Cloud Configurations loaded from a JSON + * config file, so there are no explicit property setters. + * + * This class also contains methods to query cloud sites and/or identity + * services by ID. + * + */ + +@Configuration +@JsonRootName("cloud_config") +@ConfigurationProperties(prefix="cloud_config") +public class CloudConfig { + + private static final String CLOUD_SITE_VERSION = "2.5"; + private static final String DEFAULT_CLOUD_SITE_ID = "default"; + + @JsonProperty("identity_services") + private Map<String, CloudIdentity> identityServices = new HashMap<>(); + + @JsonProperty("cloud_sites") + private Map <String, CloudSite> cloudSites = new HashMap<>(); + + @JsonProperty("cloudify_managers") + private Map <String, CloudifyManager> cloudifyManagers = new HashMap<>(); + + @PostConstruct + private void init() { + for (Entry<String, CloudIdentity> entry : identityServices.entrySet()) { + entry.getValue().setId(entry.getKey()); + } + + for (Entry<String, CloudSite> entry : cloudSites.entrySet()) { + entry.getValue().setId(entry.getKey()); + } + + for (Entry<String, CloudifyManager> entry : cloudifyManagers.entrySet()) { + entry.getValue().setId(entry.getKey()); + } + } + + /** + * Get a map of all identity services that have been loaded. + */ + public Map<String, CloudIdentity> getIdentityServices() { + return identityServices; + } + + /** + * Get a map of all cloud sites that have been loaded. + */ + public Map<String, CloudSite> getCloudSites() { + return cloudSites; + } + + /** + * Get a Map of all CloudifyManagers that have been loaded. + * @return the Map + */ + public Map<String,CloudifyManager> getCloudifyManagers() { + return cloudifyManagers; + } + + /** + * Get a specific CloudSites, based on an ID. The ID is first checked + * against the regions, and if no match is found there, then against + * individual entries to try and find one with a CLLI that matches the ID + * and an AIC version of 2.5. + * + * @param id the ID to match + * @return an Optional of CloudSite object. + */ + public synchronized Optional<CloudSite> getCloudSite(String id) { + if (id == null) { + return Optional.empty(); + } + if (cloudSites.containsKey(id)) { + return Optional.ofNullable(cloudSites.get(id)); + } else { + return getCloudSiteWithClli(id); + } + } + + public String getCloudSiteId(CloudSite cloudSite) { + for(Entry<String, CloudSite> entry : this.getCloudSites().entrySet()){ + if(entry.getValue().equals(cloudSite)) + return entry.getKey(); + } + return null; + } + + /** + * Get a specific CloudSites, based on a CLLI and (optional) version, which + * will be matched against the aic_version field of the CloudSite. + * + * @param clli + * the CLLI to match + * @param version + * the version to match; may be null in which case any version + * matches + * @return a CloudSite, or null of no match found + */ + private Optional<CloudSite> getCloudSiteWithClli(String clli) { + Optional <CloudSite> cloudSiteOptional = cloudSites.values().stream().filter(cs -> + cs.getClli() != null && clli.equals(cs.getClli()) && (CLOUD_SITE_VERSION.equals(cs.getAicVersion()))) + .findAny(); + if (cloudSiteOptional.isPresent()) { + return cloudSiteOptional; + } else { + return getDefaultCloudSite(clli); + } + } + + private Optional<CloudSite> getDefaultCloudSite(String clli) { + Optional<CloudSite> cloudSiteOpt = cloudSites.values().stream() + .filter(cs -> cs.getId().equalsIgnoreCase(DEFAULT_CLOUD_SITE_ID)).findAny(); + if (cloudSiteOpt.isPresent()) { + CloudSite defaultCloudSite = cloudSiteOpt.get(); + CloudSite clone = new CloudSite(defaultCloudSite); + clone.setRegionId(clli); + clone.setId(clli); + return Optional.of(clone); + } else { + return Optional.empty(); + } + } + + /** + * Get a specific CloudIdentity, based on an ID. + * + * @param id + * the ID to match + * @return a CloudIdentity, or null of no match found + */ + public CloudIdentity getIdentityService(String id) { + return identityServices.get(id); + } + + /** + * Get a specific CloudifyManager, based on an ID. + * @param id the ID to match + * @return a CloudifyManager, or null of no match found + */ + public CloudifyManager getCloudifyManager (String id) { + return cloudifyManagers.get(id); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("identityServices", getIdentityServices()).append("cloudSites", getCloudSites()).toString(); + } + + @Override + public boolean equals(final Object other) { + if (other == null) { + return false; + } + if (!getClass().equals(other.getClass())) { + return false; + } + CloudConfig castOther = (CloudConfig) other; + return new EqualsBuilder().append(getIdentityServices(), castOther.getIdentityServices()) + .append(getCloudSites(), castOther.getCloudSites()).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(1, 31).append(getIdentityServices()).append(getCloudSites()).toHashCode(); + } +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/CloudConfigIdentityMapper.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/CloudConfigIdentityMapper.java new file mode 100644 index 0000000000..f554aa46cd --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/CloudConfigIdentityMapper.java @@ -0,0 +1,30 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ +package org.onap.so.cloud; + +/** + * This interface provides the method signature for mapping registration. + * All mappings should be registered by the implementing class. + */ +@FunctionalInterface +public interface CloudConfigIdentityMapper { + + public void registerAllMappings(); +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/CloudIdentity.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/CloudIdentity.java new file mode 100644 index 0000000000..188a93025e --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/CloudIdentity.java @@ -0,0 +1,203 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.cloud; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.openpojo.business.annotation.BusinessKey; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +import java.util.Comparator; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * JavaBean JSON class for a CloudIdentity. This bean represents a cloud identity + * service instance (i.e. a DCP node) in the NVP/AIC cloud. It will be loaded via + * CloudConfig object, of which it is a component (a CloudConfig JSON configuration + * file may contain multiple CloudIdentity definitions). + * + * Note that this is only used to access Cloud Configurations loaded from a + * JSON config file, so there are no explicit setters. + * + */ +public class CloudIdentity { + + @JsonProperty + @BusinessKey + private String id; + @JsonProperty("identity_url") + @BusinessKey + private String identityUrl; + @JsonProperty("mso_id") + @BusinessKey + private String msoId; + @JsonProperty("mso_pass") + @BusinessKey + private String msoPass; + @JsonProperty("admin_tenant") + @BusinessKey + private String adminTenant; + @JsonProperty("member_role") + @BusinessKey + private String memberRole; + @JsonProperty("tenant_metadata") + @BusinessKey + private Boolean tenantMetadata; + @JsonProperty("identity_server_type") + @BusinessKey + private ServerType identityServerType; + @JsonProperty("identity_authentication_type") + @BusinessKey + private AuthenticationType identityAuthenticationType; + + public CloudIdentity() {} + + public String getId () { + return id; + } + + public void setId (String id) { + this.id = id; + } + + public String getIdentityUrl() { + return this.identityUrl; + } + public void setIdentityUrl(String url) { + this.identityUrl = url; + } + + public String getMsoId () { + return msoId; + } + + public void setMsoId (String id) { + this.msoId = id; + } + + public String getMsoPass () { + return msoPass; + } + + public void setMsoPass (String pwd) { + this.msoPass = pwd; + } + + public String getAdminTenant () { + return adminTenant; + } + + public void setAdminTenant (String tenant) { + this.adminTenant = tenant; + } + + public String getMemberRole () { + return memberRole; + } + + public void setMemberRole (String role) { + this.memberRole = role; + } + + public Boolean hasTenantMetadata () { + return tenantMetadata; + } + + public void setTenantMetadata (Boolean meta) { + this.tenantMetadata = meta; + } + + public ServerType getIdentityServerType() { + return this.identityServerType; + } + public void setIdentityServerType(ServerType ist) { + this.identityServerType = ist; + } + public String getIdentityServerTypeAsString() { + return this.identityServerType.toString(); + } + /** + * @return the identityAuthenticationType + */ + public AuthenticationType getIdentityAuthenticationType() { + return identityAuthenticationType; + } + + /** + * @param identityAuthenticationType the identityAuthenticationType to set + */ + public void setIdentityAuthenticationType(AuthenticationType identityAuthenticationType) { + this.identityAuthenticationType = identityAuthenticationType; + } + + @Override + public CloudIdentity clone() { + CloudIdentity cloudIdentityCopy = new CloudIdentity(); + + cloudIdentityCopy.id = this.id; + cloudIdentityCopy.identityUrl = this.identityUrl; + cloudIdentityCopy.msoId = this.msoId; + cloudIdentityCopy.msoPass = this.msoPass; + cloudIdentityCopy.adminTenant = this.adminTenant; + cloudIdentityCopy.memberRole = this.memberRole; + cloudIdentityCopy.tenantMetadata = this.tenantMetadata; + cloudIdentityCopy.identityServerType = this.identityServerType; + cloudIdentityCopy.identityAuthenticationType = this.identityAuthenticationType; + + return cloudIdentityCopy; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append("id", getId()) + .append("identityUrl", getIdentityUrl()).append("msoId", getMsoId()) + .append("adminTenant", getAdminTenant()).append("memberRole", getMemberRole()) + .append("tenantMetadata", hasTenantMetadata()).append("identityServerType", getIdentityServerType()) + .append("identityAuthenticationType", getIdentityAuthenticationType()).toString(); + } + + @Override + public boolean equals(final Object other) { + if (other == null) { + return false; + } + if (!getClass().equals(other.getClass())) { + return false; + } + CloudIdentity castOther = (CloudIdentity) other; + return new EqualsBuilder().append(getId(), castOther.getId()) + .append(getIdentityUrl(), castOther.getIdentityUrl()).append(getMsoId(), castOther.getMsoId()) + .append(getMsoPass(), castOther.getMsoPass()).append(getAdminTenant(), castOther.getAdminTenant()) + .append(getMemberRole(), castOther.getMemberRole()) + .append(hasTenantMetadata(), castOther.hasTenantMetadata()) + .append(getIdentityServerType(), castOther.getIdentityServerType()) + .append(getIdentityAuthenticationType(), castOther.getIdentityAuthenticationType()).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(1, 31).append(getId()).append(getIdentityUrl()).append(getMsoId()) + .append(getMsoPass()).append(getAdminTenant()).append(getMemberRole()).append(hasTenantMetadata()) + .append(getIdentityServerType()).append(getIdentityAuthenticationType()).toHashCode(); + } +}
\ No newline at end of file diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/CloudSite.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/CloudSite.java new file mode 100644 index 0000000000..f38403d0cd --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/CloudSite.java @@ -0,0 +1,196 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.cloud; + + +import java.util.Comparator; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.openpojo.business.annotation.BusinessKey; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * JavaBean JSON class for a CloudSite. This bean represents a cloud location + * (i.e. and LCP node) in the NVP/AIC cloud. It will be loaded via CloudConfig + * object, of which it is a component (a CloudConfig JSON configuration file + * will contain multiple CloudSite definitions). + * + * Note that this is only used to access Cloud Configurations loaded from a + * JSON config file, so there are no explicit setters. + * + */ +public class CloudSite { + @JsonProperty + @BusinessKey + private String id; + @JsonProperty("region_id") + @BusinessKey + private String regionId; + @JsonProperty("identity_service_id") + @BusinessKey + private String identityServiceId; + @JsonProperty("aic_version") + @BusinessKey + private String aicVersion; + @JsonProperty("clli") + @BusinessKey + private String clli; + @JsonProperty("cloudify_id") + @BusinessKey + private String cloudifyId; + @JsonProperty("platform") + @BusinessKey + private String platform; + @JsonProperty("orchestrator") + @BusinessKey + private String orchestrator; + + // Derived property (set by CloudConfig loader based on identityServiceId) + private CloudIdentity identityService; + // Derived property (set by CloudConfig loader based on cloudifyId) + private CloudifyManager cloudifyManager; + + public CloudSite() { + + } + + public CloudSite(CloudSite site) { + this.aicVersion = site.getAicVersion(); + this.clli = site.getClli(); + this.cloudifyId = this.getCloudifyId(); + this.cloudifyManager = this.getCloudifyManager(); + this.id = site.getId(); + this.identityService = site.getIdentityService(); + this.identityServiceId = site.getIdentityServiceId(); + this.orchestrator = site.getOrchestrator(); + this.platform = site.getPlatform(); + this.regionId = this.getRegionId(); + } + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public String getRegionId() { + return regionId; + } + + public void setRegionId(String regionId) { + this.regionId = regionId; + } + + public String getIdentityServiceId() { + return identityServiceId; + } + + public void setIdentityServiceId(String identityServiceId) { + this.identityServiceId = identityServiceId; + } + public String getAicVersion() { + return aicVersion; + } + + public void setAicVersion(String aicVersion) { + this.aicVersion = aicVersion; + } + + public String getClli() { + return clli; + } + + public void setClli(String clli) { + this.clli = clli; + } + + public String getCloudifyId() { + return cloudifyId; + } + + public void setCloudifyId (String id) { + this.cloudifyId = id; + } + + public String getPlatform() { + return platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + + public String getOrchestrator() { + return orchestrator; + } + + public void setOrchestrator(String orchestrator) { + this.orchestrator = orchestrator; + } + + public CloudIdentity getIdentityService () { + return identityService; + } + + public void setIdentityService (CloudIdentity identity) { + this.identityService = identity; + } + + public CloudifyManager getCloudifyManager () { + return cloudifyManager; + } + + public void setCloudifyManager (CloudifyManager cloudify) { + this.cloudifyManager = cloudify; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append("regionId", getRegionId()) + .append("identityServiceId", getIdentityServiceId()).append("aicVersion", getAicVersion()) + .append("clli", getClli()).append("cloudifyId", getCloudifyId()).append("platform", getPlatform()) + .append("orchestrator", getOrchestrator()).toString(); + } + + @Override + public boolean equals(final Object other) { + if (other == null) { + return false; + } + if (!getClass().equals(other.getClass())) { + return false; + } + CloudSite castOther = (CloudSite) other; + return new EqualsBuilder().append(getRegionId(), castOther.getRegionId()) + .append(getIdentityServiceId(), castOther.getIdentityServiceId()) + .append(getAicVersion(), castOther.getAicVersion()).append(getClli(), castOther.getClli()).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(1, 31).append(getRegionId()).append(getIdentityServiceId()).append(getAicVersion()) + .append(getClli()).toHashCode(); + } +}
\ No newline at end of file diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/CloudifyManager.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/CloudifyManager.java new file mode 100644 index 0000000000..1bf3f136b0 --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/CloudifyManager.java @@ -0,0 +1,153 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.cloud; + +import java.security.GeneralSecurityException; +import java.util.Comparator; + +import org.onap.so.logger.MessageEnum; +import org.onap.so.logger.MsoLogger; +import org.onap.so.utils.CryptoUtils; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.openpojo.business.annotation.BusinessKey; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.EqualsBuilder; + +/** + * JavaBean JSON class for a Cloudify Manager. This bean represents a Cloudify + * node through which TOSCA-based VNFs may be deployed. Each CloudSite in the + * CloudConfig may have a Cloudify Manager for deployments using TOSCA blueprints. + * Cloudify Managers may support multiple Cloud Sites, but each site will have + * at most one Cloudify Manager. + * + * This does not replace the ability to use the CloudSite directly via Openstack. + * + * Note that this is only used to access Cloud Configurations loaded from a + * JSON config file, so there are no explicit setters. + * + * @author JC1348 + */ +public class CloudifyManager { + + private static MsoLogger LOGGER = MsoLogger.getMsoLogger (MsoLogger.Catalog.RA, CloudifyManager.class); + + @BusinessKey + @JsonProperty + private String id; + + @BusinessKey + @JsonProperty ("cloudify_url") + private String cloudifyUrl; + + @BusinessKey + @JsonProperty("username") + private String username; + + @BusinessKey + @JsonProperty("password") + private String password; + + @BusinessKey + @JsonProperty("version") + private String version; + + public CloudifyManager() {} + + public String getId() { + return id; + } + public void setId(String id) { + this.id = id; + } + + public String getCloudifyUrl() { + return cloudifyUrl; + } + + public void setCloudifyUrl(String cloudifyUrl) { + this.cloudifyUrl = cloudifyUrl; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + @Override + public CloudifyManager clone() { + CloudifyManager cloudifyManagerCopy = new CloudifyManager(); + cloudifyManagerCopy.id = this.id; + cloudifyManagerCopy.cloudifyUrl = this.cloudifyUrl; + cloudifyManagerCopy.username = this.username; + cloudifyManagerCopy.password = this.password; + cloudifyManagerCopy.version = this.version; + return cloudifyManagerCopy; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE).append("id", getId()) + .append("cloudifyUrl", getCloudifyUrl()).append("username", getUsername()) + .append("password", getPassword()).append("version", getVersion()).toString(); + } + + @Override + public boolean equals(final Object other) { + if (other == null) { + return false; + } + if (!getClass().equals(other.getClass())) { + return false; + } + CloudifyManager castOther = (CloudifyManager) other; + return new EqualsBuilder().append(getId(), castOther.getId()) + .append(getCloudifyUrl(), castOther.getCloudifyUrl()).append(getUsername(), castOther.getUsername()) + .append(getPassword(), castOther.getPassword()).append(getVersion(), castOther.getVersion()).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(1, 31).append(getId()).append(getCloudifyUrl()).append(getUsername()) + .append(getPassword()).append(getVersion()).toHashCode(); + } +}
\ No newline at end of file diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/ServerType.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/ServerType.java new file mode 100644 index 0000000000..ac59018c6b --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/ServerType.java @@ -0,0 +1,25 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.cloud; + +public enum ServerType { + KEYSTONE, ORM; +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/authentication/AuthenticationMethodFactory.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/authentication/AuthenticationMethodFactory.java new file mode 100644 index 0000000000..5c648eb5e3 --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/authentication/AuthenticationMethodFactory.java @@ -0,0 +1,53 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.cloud.authentication; + +import org.onap.so.cloud.AuthenticationType; +import org.onap.so.cloud.CloudIdentity; +import org.onap.so.cloud.authentication.models.RackspaceAuthentication; +import org.onap.so.utils.CryptoUtils; +import org.springframework.stereotype.Component; + +import com.woorea.openstack.keystone.model.Authentication; +import com.woorea.openstack.keystone.model.authentication.UsernamePassword; + +/** + * This factory manages all the wrappers associated to authentication types. + * + */ +@Component +public final class AuthenticationMethodFactory { + + public final Authentication getAuthenticationFor(CloudIdentity cloudIdentity) { + if (cloudIdentity == null) { + throw new IllegalArgumentException("Cloud identity cannot be null"); + } + if ((cloudIdentity.getIdentityAuthenticationType() == null)|| ("".equals(cloudIdentity.getIdentityAuthenticationType().toString()))) { + throw new IllegalArgumentException("Cloud identity authentication type cannot be null or empty, provided value is " + cloudIdentity.getIdentityAuthenticationType() + "."); + } + AuthenticationType authenticationType = cloudIdentity.getIdentityAuthenticationType(); + if (AuthenticationType.RACKSPACE_APIKEY.equals(authenticationType)) { + return new RackspaceAuthentication (cloudIdentity.getMsoId (), CryptoUtils.decryptCloudConfigPassword(cloudIdentity.getMsoPass ())); + } else { + return new UsernamePassword (cloudIdentity.getMsoId (), CryptoUtils.decryptCloudConfigPassword(cloudIdentity.getMsoPass ())); + } + } +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/authentication/models/RackspaceAuthentication.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/authentication/models/RackspaceAuthentication.java new file mode 100644 index 0000000000..009c9a4c6c --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloud/authentication/models/RackspaceAuthentication.java @@ -0,0 +1,102 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.cloud.authentication.models; + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; +import com.woorea.openstack.keystone.model.Authentication; + +@JsonRootName("auth") +public class RackspaceAuthentication extends Authentication { + + /** + * + */ + private static final long serialVersionUID = 5451283386875662918L; + + @JsonIgnore + private String tenantId; + + @JsonIgnore + private String tenantName; + + public static final class Token implements Serializable{ + + /** + * + */ + private static final long serialVersionUID = -4448875265818207908L; + private String username; + private String apiKey; + + /** + * @return the username + */ + public String getUsername() { + return username; + } + /** + * @param username the username to set + */ + public void setUsername(String username) { + this.username = username; + } + /** + * @return the apiKey + */ + public String getApiKey() { + return apiKey; + } + /** + * @param apiKey the apiKey to set + */ + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + } + + @JsonProperty("RAX-KSKEY:apiKeyCredentials") + private Token token = new Token(); + + public RackspaceAuthentication (String username, String apiKey) { + this.token.username = username; + this.token.apiKey = apiKey; + + } + + /** + * @return the token + */ + public Token getToken() { + return token; + } + + /** + * @param token the token to set + */ + public void setToken(Token token) { + this.token = token; + } + +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/beans/DeploymentInfo.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/beans/DeploymentInfo.java new file mode 100644 index 0000000000..c6e29d05d7 --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/beans/DeploymentInfo.java @@ -0,0 +1,186 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.cloudify.beans; + +import java.util.HashMap; +import java.util.Map; + +import org.onap.so.cloudify.v3.model.Deployment; +import org.onap.so.cloudify.v3.model.DeploymentOutputs; +import org.onap.so.cloudify.v3.model.Execution; + +/* + * This Java bean class relays Heat stack status information to ActiveVOS processes. + * + * This bean is returned by all Heat-specific adapter operations (create, query, delete) + */ + +public class DeploymentInfo { + // Set defaults for everything + private String id = ""; + private DeploymentStatus status = DeploymentStatus.NOTFOUND; + private Map<String,Object> outputs = new HashMap<String,Object>(); + private Map<String,Object> inputs = new HashMap<String,Object>(); + private String lastAction; + private String actionStatus; + private String errorMessage; + + public DeploymentInfo () { + } + + public DeploymentInfo (String id, Map<String,Object> outputs) { + this.id = id; + if (outputs != null) this.outputs = outputs; + } + + public DeploymentInfo (String id) { + this.id = id; + } + + public DeploymentInfo (String id, DeploymentStatus status) { + this.id = id; + this.status = status; + } + + public DeploymentInfo (Deployment deployment) { + this(deployment, null, null); + } + + /** + * Construct a DeploymentInfo object from a deployment and the latest Execution action + * @param deployment + * @param execution + */ + public DeploymentInfo (Deployment deployment, DeploymentOutputs outputs, Execution execution) + { + if (deployment == null) { + this.id = null; + return; + } + + this.id = deployment.getId(); + + if (outputs != null) + this.outputs = outputs.getOutputs(); + + if (deployment.getInputs() != null) + this.inputs = deployment.getInputs(); + + if (execution != null) { + this.lastAction = execution.getWorkflowId(); + this.actionStatus = execution.getStatus(); + this.errorMessage = execution.getError(); + + // Compute the status based on the last workflow + if (lastAction.equals("install")) { + if (actionStatus.equals("terminated")) + this.status = DeploymentStatus.INSTALLED; + else if (actionStatus.equals("failed")) + this.status = DeploymentStatus.FAILED; + else if (actionStatus.equals("started") || actionStatus.equals("pending")) + this.status = DeploymentStatus.INSTALLING; + else + this.status = DeploymentStatus.UNKNOWN; + } + else if (lastAction.equals("uninstall")) { + if (actionStatus.equals("terminated")) + this.status = DeploymentStatus.CREATED; + else if (actionStatus.equals("failed")) + this.status = DeploymentStatus.FAILED; + else if (actionStatus.equals("started") || actionStatus.equals("pending")) + this.status = DeploymentStatus.UNINSTALLING; + else + this.status = DeploymentStatus.UNKNOWN; + } + else { + // Could have more cases in the future for different actions. + this.status = DeploymentStatus.UNKNOWN; + } + } + else { + this.status = DeploymentStatus.CREATED; + } + } + + public String getId() { + return id; + } + + public void setId (String id) { + this.id = id; + } + + public DeploymentStatus getStatus() { + return status; + } + + public void setStatus (DeploymentStatus status) { + this.status = status; + } + + public Map<String,Object> getOutputs () { + return outputs; + } + + public void setOutputs (Map<String,Object> outputs) { + this.outputs = outputs; + } + + public Map<String,Object> getInputs () { + return inputs; + } + + public void setInputs (Map<String,Object> inputs) { + this.inputs = inputs; + } + + public String getLastAction() { + return lastAction; + } + + public String getActionStatus() { + return actionStatus; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void saveExecutionStatus (Execution execution) { + this.lastAction = execution.getWorkflowId(); + this.actionStatus = execution.getStatus(); + this.errorMessage = execution.getError(); + } + + @Override + public String toString() { + return "DeploymentInfo {" + + "id='" + id + '\'' + + ", inputs='" + inputs + '\'' + + ", outputs='" + outputs + '\'' + + ", lastAction='" + lastAction + '\'' + + ", status='" + status + '\'' + + ", errorMessage='" + errorMessage + '\'' + + '}'; + } + +} + diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/beans/DeploymentStatus.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/beans/DeploymentStatus.java new file mode 100644 index 0000000000..5aa47e9d6b --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/beans/DeploymentStatus.java @@ -0,0 +1,31 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.cloudify.beans; + + +/* + * Enum status values to capture the state of a deployment, based on last known workflow + * (assume only INSTALL and UNINSTALL at this point). + */ +public enum DeploymentStatus { + NOTFOUND, CREATED, INSTALLED, FAILED, INSTALLING, UNINSTALLING, UNKNOWN +} + diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/exceptions/MsoBlueprintAlreadyExists.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/exceptions/MsoBlueprintAlreadyExists.java new file mode 100644 index 0000000000..d5d5684b0f --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/exceptions/MsoBlueprintAlreadyExists.java @@ -0,0 +1,33 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.cloudify.exceptions; + +public class MsoBlueprintAlreadyExists extends MsoCloudifyException { + + private static final long serialVersionUID = 1L; + + // Constructor to create a new MsoCloudifyException instance + public MsoBlueprintAlreadyExists (String blueprintId, String cloud) { + // Set the detailed error as the Exception 'message' + super(409, "Conflict", "Blueprint " + blueprintId + " already exists in Cloudify Manager supporting cloud site + " + cloud); + } + +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/exceptions/MsoCloudifyException.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/exceptions/MsoCloudifyException.java new file mode 100644 index 0000000000..992df5fd6a --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/exceptions/MsoCloudifyException.java @@ -0,0 +1,86 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.cloudify.exceptions; + +import org.onap.so.openstack.exceptions.MsoException; +import org.onap.so.openstack.exceptions.MsoExceptionCategory; + +/** + * OpenStack exception. + */ +public class MsoCloudifyException extends MsoException +{ + + /** + * Serialization id. + */ + private static final long serialVersionUID = 3313636124141766495L; + + private int statusCode; + private String statusMessage; + private String errorDetail; + private boolean pendingWorkflow; + + /** + * Constructor to create a new MsoOpenstackException instance + * @param code the error code + * @param message the error message + * @param detail error details + */ + public MsoCloudifyException (int code, String message, String detail) { + // Set the detailed error as the Exception 'message' + super(detail); + super.category = MsoExceptionCategory.OPENSTACK; + + this.statusCode = code; + this.statusMessage = message; + this.errorDetail = detail; + this.pendingWorkflow = false; + } + + /** + * Constructor to propagate the caught exception (mostly for stack trace) + * @param code the error code + * @param message the error message + * @param detail error details + * @param e the cause + */ + public MsoCloudifyException (int code, String message, String detail, Exception e) { + // Set the detailed error as the Exception 'message' + super(detail, e); + super.category = MsoExceptionCategory.OPENSTACK; + + this.statusCode = code; + this.statusMessage = message; + this.errorDetail = detail; + this.pendingWorkflow = false; + } + + public void setPendingWorkflow (boolean pendingWorkflow) { + this.pendingWorkflow = pendingWorkflow; + } + + @Override + public String toString () { + String error = "" + statusCode + " " + statusMessage + ": " + errorDetail + (pendingWorkflow ? " [workflow pending]" : ""); + return error; + } +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/exceptions/MsoCloudifyManagerNotFound.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/exceptions/MsoCloudifyManagerNotFound.java new file mode 100644 index 0000000000..0c795478cd --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/exceptions/MsoCloudifyManagerNotFound.java @@ -0,0 +1,33 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.cloudify.exceptions; + +public class MsoCloudifyManagerNotFound extends MsoCloudifyException { + + private static final long serialVersionUID = 1L; + + // Constructor to create a new MsoCloudifyException instance + public MsoCloudifyManagerNotFound (String cloudSiteId) { + // Set the detailed error as the Exception 'message' + super(0, "Cloudify Manager Not Found", "No Cloudify Manager configured for cloud site " + cloudSiteId); + } + +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/exceptions/MsoCloudifyTimeout.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/exceptions/MsoCloudifyTimeout.java new file mode 100644 index 0000000000..7dcd69d0a4 --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/exceptions/MsoCloudifyTimeout.java @@ -0,0 +1,64 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.cloudify.exceptions; + +import org.onap.so.cloudify.v3.model.Execution; +import org.onap.so.openstack.exceptions.MsoException; +import org.onap.so.openstack.exceptions.MsoExceptionCategory; + +/** + * MSO Exception when a Cloudify workflow execution times out waiting for completion. + * Exception includes the last known state of the workflow execution. + */ +public class MsoCloudifyTimeout extends MsoException +{ + + /** + * Serialization id. + */ + private static final long serialVersionUID = 3313636124141766495L; + + private Execution execution; + + /** + * Constructor to create a new MsoOpenstackException instance + * @param code the error code + * @param message the error message + * @param detail error details + */ + public MsoCloudifyTimeout (Execution execution) { + // Set the detailed error as the Exception 'message' + super("Cloudify Workflow Timeout for workflow " + execution.getWorkflowId() + " on deployment " + execution.getDeploymentId()); + super.category = MsoExceptionCategory.OPENSTACK; + + this.execution = execution; + } + + public Execution getExecution() { + return this.execution; + } + + @Override + public String toString () { + String error = "Workflow timeout: workflow=" + execution.getWorkflowId() + ",deployment=" + execution.getDeploymentId(); + return error; + } +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/exceptions/MsoCloudifyWorkflowException.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/exceptions/MsoCloudifyWorkflowException.java new file mode 100644 index 0000000000..a84da50dc4 --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/exceptions/MsoCloudifyWorkflowException.java @@ -0,0 +1,54 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.cloudify.exceptions; + +/** + * Reports an error with a Cloudify Workflow execution. + * @author JC1348 + * + */ +public class MsoCloudifyWorkflowException extends MsoCloudifyException { + + private static final long serialVersionUID = 1L; + + private String workflowStatus; + private boolean workflowStillRunning = false; + + // Constructor to create a new MsoCloudifyException instance + public MsoCloudifyWorkflowException (String message, String deploymentId, String workflowId, String workflowStatus) + { + super(0, "Workflow Exception", "Workflow " + workflowId + " failed on deployment " + deploymentId + ": " + message); + this.workflowStatus = workflowStatus; + if (workflowStatus.equals("pending") || workflowStatus.equals("started") || + workflowStatus.equals("cancelling") || workflowStatus.equals("force_cancelling")) + { + workflowStillRunning = true; + } + } + + public String getWorkflowStatus() { + return workflowStatus; + } + + public boolean isWorkflowStillRunning () { + return workflowStillRunning; + } +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/exceptions/MsoDeploymentAlreadyExists.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/exceptions/MsoDeploymentAlreadyExists.java new file mode 100644 index 0000000000..4f5685efb3 --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/exceptions/MsoDeploymentAlreadyExists.java @@ -0,0 +1,33 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.cloudify.exceptions; + +public class MsoDeploymentAlreadyExists extends MsoCloudifyException { + + private static final long serialVersionUID = 1L; + + // Constructor to create a new MsoCloudifyException instance + public MsoDeploymentAlreadyExists (String deploymentId, String cloud) { + // Set the detailed error as the Exception 'message' + super(409, "Conflict", "Deployment " + deploymentId + " already exists in Cloudify Manager suppporting cloud " + cloud); + } + +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/utils/MsoCloudifyUtils.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/utils/MsoCloudifyUtils.java new file mode 100644 index 0000000000..aa8e37f12b --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/cloudify/utils/MsoCloudifyUtils.java @@ -0,0 +1,1408 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.cloudify.utils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.onap.so.adapters.vdu.CloudInfo; +import org.onap.so.adapters.vdu.PluginAction; +import org.onap.so.adapters.vdu.VduArtifact; +import org.onap.so.adapters.vdu.VduArtifact.ArtifactType; +import org.onap.so.adapters.vdu.VduException; +import org.onap.so.adapters.vdu.VduInstance; +import org.onap.so.adapters.vdu.VduModelInfo; +import org.onap.so.adapters.vdu.VduPlugin; +import org.onap.so.adapters.vdu.VduStateType; +import org.onap.so.adapters.vdu.VduStatus; +import org.onap.so.cloud.CloudConfig; +import org.onap.so.cloud.CloudSite; +import org.onap.so.cloud.CloudifyManager; +import org.onap.so.cloudify.base.client.CloudifyBaseException; +import org.onap.so.cloudify.base.client.CloudifyClientTokenProvider; +import org.onap.so.cloudify.base.client.CloudifyConnectException; +import org.onap.so.cloudify.base.client.CloudifyRequest; +import org.onap.so.cloudify.base.client.CloudifyResponseException; +import org.onap.so.cloudify.beans.DeploymentInfo; +import org.onap.so.cloudify.beans.DeploymentStatus; +import org.onap.so.cloudify.exceptions.MsoCloudifyException; +import org.onap.so.cloudify.exceptions.MsoCloudifyManagerNotFound; +import org.onap.so.cloudify.exceptions.MsoDeploymentAlreadyExists; +import org.onap.so.cloudify.v3.client.BlueprintsResource.GetBlueprint; +import org.onap.so.cloudify.v3.client.BlueprintsResource.UploadBlueprint; +import org.onap.so.cloudify.v3.client.Cloudify; +import org.onap.so.cloudify.v3.client.DeploymentsResource.CreateDeployment; +import org.onap.so.cloudify.v3.client.DeploymentsResource.DeleteDeployment; +import org.onap.so.cloudify.v3.client.DeploymentsResource.GetDeployment; +import org.onap.so.cloudify.v3.client.DeploymentsResource.GetDeploymentOutputs; +import org.onap.so.cloudify.v3.client.ExecutionsResource.CancelExecution; +import org.onap.so.cloudify.v3.client.ExecutionsResource.GetExecution; +import org.onap.so.cloudify.v3.client.ExecutionsResource.ListExecutions; +import org.onap.so.cloudify.v3.client.ExecutionsResource.StartExecution; +import org.onap.so.cloudify.v3.model.AzureConfig; +import org.onap.so.cloudify.v3.model.Blueprint; +import org.onap.so.cloudify.v3.model.CancelExecutionParams; +import org.onap.so.cloudify.v3.model.CloudifyError; +import org.onap.so.cloudify.v3.model.CreateDeploymentParams; +import org.onap.so.cloudify.v3.model.Deployment; +import org.onap.so.cloudify.v3.model.DeploymentOutputs; +import org.onap.so.cloudify.v3.model.Execution; +import org.onap.so.cloudify.v3.model.Executions; +import org.onap.so.cloudify.v3.model.OpenstackConfig; +import org.onap.so.cloudify.v3.model.StartExecutionParams; +import org.onap.so.config.beans.PoConfig; +import org.onap.so.db.catalog.beans.HeatTemplateParam; +import org.onap.so.logger.MessageEnum; +import org.onap.so.logger.MsoAlarmLogger; +import org.onap.so.logger.MsoLogger; +import org.onap.so.openstack.exceptions.MsoAdapterException; +import org.onap.so.openstack.exceptions.MsoCloudSiteNotFound; +import org.onap.so.openstack.exceptions.MsoException; +import org.onap.so.openstack.exceptions.MsoExceptionCategory; +import org.onap.so.openstack.exceptions.MsoIOException; +import org.onap.so.openstack.exceptions.MsoOpenstackException; +import org.onap.so.openstack.utils.MsoCommonUtils; +import org.onap.so.utils.CryptoUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +@Component +public class MsoCloudifyUtils extends MsoCommonUtils implements VduPlugin{ + + private static final String CLOUDIFY_ERROR = "CloudifyError"; + private static final String CLOUDIFY = "Cloudify"; + private static final String CREATE_DEPLOYMENT = "CreateDeployment"; + private static final String DELETE_DEPLOYMENT = "DeleteDeployment"; + private static final String TERMINATED = "terminated"; + private static final String CANCELLED = "cancelled"; + + // Fetch cloud configuration each time (may be cached in CloudConfig class) + @Autowired + protected CloudConfig cloudConfig; + + @Autowired + private Environment environment; + + @Autowired + private PoConfig poConfig; + + private static final MsoLogger LOGGER = MsoLogger.getMsoLogger (MsoLogger.Catalog.RA, MsoCloudifyUtils.class); + + // Properties names and variables (with default values) + protected String createPollIntervalProp = "ecomp.mso.adapters.po.pollInterval"; + private String deletePollIntervalProp = "ecomp.mso.adapters.po.pollInterval"; + + protected String createPollIntervalDefault = "15"; + private String deletePollIntervalDefault = "15"; + + private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); + + /** + * Create a new Deployment from a specified blueprint, and install it in the specified + * cloud location and tenant. The blueprint identifier and parameter map are passed in + * as arguments, along with the cloud access credentials. The blueprint should have been + * previously uploaded to Cloudify. + * + * It is expected that parameters have been validated and contain at minimum the required + * parameters for the given template with no extra (undefined) parameters.. + * + * The deployment ID supplied by the caller must be unique in the scope of the Cloudify + * tenant (not the Openstack tenant). However, it should also be globally unique, as it + * will be the identifier for the resource going forward in Inventory. This latter is + * managed by the higher levels invoking this function. + * + * This function executes the "install" workflow on the newly created workflow. Cloudify + * will be polled for completion unless the client requests otherwise. + * + * An error will be thrown if the requested Deployment already exists in the specified + * Cloudify instance. + * + * @param cloudSiteId The cloud (may be a region) in which to create the stack. + * @param tenantId The Openstack ID of the tenant in which to create the Stack + * @param deploymentId The identifier (name) of the deployment to create + * @param blueprintId The blueprint from which to create the deployment. + * @param inputs A map of key/value inputs + * @param pollForCompletion Indicator that polling should be handled in Java vs. in the client + * @param timeoutMinutes Timeout after which the "install" will be cancelled + * @param environment An optional yaml-format string to specify environmental parameters + * @param backout Flag to delete deployment on install Failure - defaulted to True + * @return A DeploymentInfo object + * @throws MsoCloudifyException Thrown if the Cloudify API call returns an exception. + * @throws MsoIOException Thrown on Cloudify connection errors. + */ + + public DeploymentInfo createAndInstallDeployment (String cloudSiteId, + String tenantId, + String deploymentId, + String blueprintId, + Map <String, ? extends Object> inputs, + boolean pollForCompletion, + int timeoutMinutes, + boolean backout) throws MsoException + { + // Obtain the cloud site information where we will create the stack + Optional<CloudSite> cloudSite = cloudConfig.getCloudSite (cloudSiteId); + if (!cloudSite.isPresent()) { + throw new MsoCloudSiteNotFound (cloudSiteId); + } + + Cloudify cloudify = getCloudifyClient (cloudSite.get()); + + LOGGER.debug ("Ready to Create Deployment (" + deploymentId + ") with input params: " + inputs); + + // Build up the inputs, including: + // - from provided "environment" file + // - passed in by caller + // - special input for cloud-specific Credentials + Map<String,Object> expandedInputs = new HashMap<> (inputs); + + String platform = cloudSite.get().getPlatform(); + if (platform == null || platform.equals("") || platform.equalsIgnoreCase("OPENSTACK")) { + // Create the Cloudify OpenstackConfig with the credentials + OpenstackConfig openstackConfig = getOpenstackConfig (cloudSite.get(), tenantId); + expandedInputs.put("openstack_config", openstackConfig); + } else if (platform.equalsIgnoreCase("AZURE")) { + // Create Cloudify AzureConfig with the credentials + AzureConfig azureConfig = getAzureConfig (cloudSite.get(), tenantId); + expandedInputs.put("azure_config", azureConfig); + } + + // Build up the parameters to create a new deployment + CreateDeploymentParams deploymentParams = new CreateDeploymentParams(); + deploymentParams.setBlueprintId(blueprintId); + deploymentParams.setInputs(expandedInputs); + + Deployment deployment = null; + try { + CreateDeployment createDeploymentRequest = cloudify.deployments().create(deploymentId, deploymentParams); + LOGGER.debug (createDeploymentRequest.toString()); + + deployment = executeAndRecordCloudifyRequest (createDeploymentRequest); + } + catch (CloudifyResponseException e) { + // Since this came on the 'Create Deployment' command, nothing was changed + // in the cloud. Return the error as an exception. + if (e.getStatus () == 409) { + // Deployment already exists. Return a specific error for this case + MsoException me = new MsoDeploymentAlreadyExists (deploymentId, cloudSiteId); + me.addContext (CREATE_DEPLOYMENT); + throw me; + } else { + // Convert the CloudifyResponseException to an MsoException + LOGGER.debug("ERROR STATUS = " + e.getStatus() + ",\n" + e.getMessage() + "\n" + e.getLocalizedMessage()); + MsoException me = cloudifyExceptionToMsoException (e, CREATE_DEPLOYMENT); + me.setCategory (MsoExceptionCategory.OPENSTACK); + throw me; + } + } catch (CloudifyConnectException e) { + // Error connecting to Cloudify instance. Convert to an MsoException + throw cloudifyExceptionToMsoException (e, CREATE_DEPLOYMENT); + } catch (RuntimeException e) { + // Catch-all + throw runtimeExceptionToMsoException (e, CREATE_DEPLOYMENT); + } + + /* + * It can take some time for Cloudify to be ready to execute a workflow + * on the deployment. Sleep 30 seconds based on observation of behavior + * in a Cloudify VM instance (delay due to "create_deployment_environment"). + */ + sleep(30000); + + /* + * Next execute the "install" workflow. + * Note - this assumes there are no additional parameters required for the workflow. + */ + int createPollInterval = Integer.parseInt(this.environment.getProperty(createPollIntervalProp, createPollIntervalDefault)); + int pollTimeout = (timeoutMinutes * 60) + createPollInterval; + + Execution installWorkflow = null; + + try { + installWorkflow = executeWorkflow (cloudify, deploymentId, "install", null, pollForCompletion, pollTimeout, createPollInterval); + + if (installWorkflow.getStatus().equals(TERMINATED)) { + // Success! + // Create and return a DeploymentInfo structure. Include the Runtime outputs + DeploymentOutputs outputs = getDeploymentOutputs (cloudify, deploymentId); + return new DeploymentInfo (deployment, outputs, installWorkflow); + } + else { + // The workflow completed with errors. Must try to back it out. + if (!backout) + { + LOGGER.warn(MessageEnum.RA_CREATE_STACK_ERR, "Deployment installation failed, backout deletion suppressed", "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Exception in Deployment Installation, backout suppressed"); + } + else { + // Poll on delete if we rollback - use same values for now + int deletePollInterval = createPollInterval; + int deletePollTimeout = pollTimeout; + + try { + // Run the uninstall to undo the install + Execution uninstallWorkflow = executeWorkflow (cloudify, deploymentId, "uninstall", null, pollForCompletion, deletePollTimeout, deletePollInterval); + + if (uninstallWorkflow.getStatus().equals(TERMINATED)) + { + // The uninstall completed. Delete the deployment itself + DeleteDeployment deleteRequest = cloudify.deployments().deleteByName(deploymentId); + executeAndRecordCloudifyRequest (deleteRequest); + } + else { + // Didn't uninstall successfully. Log this error + LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "Create Deployment: Cloudify error rolling back deployment install: " + installWorkflow.getError(), "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Create Stack: Cloudify error rolling back deployment installation"); + } + } + catch (Exception e) { + // Catch-all for backout errors trying to uninstall/delete + // Log this error, and return the original exception + LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "Create Stack: Nested exception rolling back deployment install: " + e, "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Create Stack: Nested exception rolling back deployment installation"); + } + } + + MsoCloudifyException me = new MsoCloudifyException (0, "Workflow Execution Failed", installWorkflow.getError()); + me.addContext (CREATE_DEPLOYMENT); + alarmLogger.sendAlarm(CLOUDIFY_ERROR, MsoAlarmLogger.CRITICAL, me.getContextMessage()); + throw me; + } + } + catch (MsoException me) { + // Install failed. Unless requested otherwise, back out the deployment + + if (!backout) + { + LOGGER.warn(MessageEnum.RA_CREATE_STACK_ERR, "Deployment installation failed, backout deletion suppressed", "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Exception in Deployment Installation, backout suppressed"); + } + else { + // Poll on delete if we rollback - use same values for now + int deletePollInterval = createPollInterval; + int deletePollTimeout = pollTimeout; + + try { + // Run the uninstall to undo the install. + // Always try to run it, as it should be idempotent + executeWorkflow (cloudify, deploymentId, "uninstall", null, pollForCompletion, deletePollTimeout, deletePollInterval); + + // Delete the deployment itself + DeleteDeployment deleteRequest = cloudify.deployments().deleteByName(deploymentId); + executeAndRecordCloudifyRequest (deleteRequest); + } + catch (Exception e) { + // Catch-all for backout errors trying to uninstall/delete + // Log this error, and return the original exception + LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "Create Stack: Nested exception rolling back deployment install: " + e, "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Create Stack: Nested exception rolling back deployment installation"); + + } + } + + // Propagate the original exception from Stack Query. + me.addContext (CREATE_DEPLOYMENT); + alarmLogger.sendAlarm(CLOUDIFY_ERROR, MsoAlarmLogger.CRITICAL, me.getContextMessage()); + throw me; + } + } + + + /* + * Get the runtime Outputs of a deployment. + * Return the Map of tag/value outputs. + */ + private DeploymentOutputs getDeploymentOutputs (Cloudify cloudify, String deploymentId) + throws MsoException + { + // Build and send the Cloudify request + DeploymentOutputs deploymentOutputs = null; + try { + GetDeploymentOutputs queryDeploymentOutputs = cloudify.deployments().outputsById(deploymentId); + LOGGER.debug (queryDeploymentOutputs.toString()); + + deploymentOutputs = executeAndRecordCloudifyRequest(queryDeploymentOutputs); + } + catch (CloudifyConnectException ce) { + // Couldn't connect to Cloudify + LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "QueryDeploymentOutputs: Cloudify connection failure: " + ce, "", "", MsoLogger.ErrorCode.BusinessProcesssError, "QueryDeploymentOutputs: Cloudify connection failure"); + throw new MsoIOException (ce.getMessage(), ce); + } + catch (CloudifyResponseException re) { + if (re.getStatus () == 404) { + // No Outputs + return null; + } + throw new MsoCloudifyException (re.getStatus(), re.getMessage(), re.getLocalizedMessage(), re); + } + catch (Exception e) { + // Catch-all + throw new MsoAdapterException (e.getMessage(), e); + } + + return deploymentOutputs; + } + + /* + * Execute a workflow on a deployment. Handle polling for completion with timeout. + * Return the final Execution object with status. + * Throw an exception on Errors. + * Question - how does the client know whether rollback needs to be done? + */ + private Execution executeWorkflow (Cloudify cloudify, String deploymentId, String workflowId, Map<String,Object> workflowParams, boolean pollForCompletion, int timeout, int pollInterval) + throws MsoCloudifyException + { + LOGGER.debug("Executing '" + workflowId + "' workflow on deployment '" + deploymentId + "'"); + + StartExecutionParams executeParams = new StartExecutionParams(); + executeParams.setWorkflowId(workflowId); + executeParams.setDeploymentId(deploymentId); + executeParams.setParameters(workflowParams); + + Execution execution = null; + String executionId = null; + String command = "start"; + Exception savedException = null; + + try { + StartExecution executionRequest = cloudify.executions().start(executeParams); + LOGGER.debug (executionRequest.toString()); + execution = executeAndRecordCloudifyRequest (executionRequest); + executionId = execution.getId(); + + if (!pollForCompletion) { + // Client did not request polling, so just return the Execution object + return execution; + } + + // Enter polling loop + boolean timedOut = false; + int pollTimeout = timeout; + + String status = execution.getStatus(); + + // Create a reusable cloudify query request + GetExecution queryExecution = cloudify.executions().byId(executionId); + command = "query"; + + while (!timedOut && !(status.equals(TERMINATED) || status.equals("failed") || status.equals(CANCELLED))) + { + // workflow is still running; check for timeout + if (pollTimeout <= 0) { + LOGGER.debug ("workflow " + execution.getWorkflowId() + " timed out on deployment " + execution.getDeploymentId()); + timedOut = true; + continue; + } + + sleep(pollInterval * 1000L); + + pollTimeout -= pollInterval; + LOGGER.debug("pollTimeout remaining: " + pollTimeout); + + execution = queryExecution.execute(); + status = execution.getStatus(); + } + + // Broke the loop. Check again for a terminal state + if (status.equals(TERMINATED)){ + // Success! + LOGGER.debug ("Workflow '" + workflowId + "' completed successfully on deployment '" + deploymentId + "'"); + return execution; + } + else if (status.equals("failed")){ + // Workflow failed. Log it and return the execution object (don't throw exception here) + LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "Cloudify workflow failure: " + execution.getError(), "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Execute Workflow: Failed: " + execution.getError()); + return execution; + } + else if (status.equals(CANCELLED)){ + // Workflow was cancelled, leaving the deployment in an indeterminate state. Log it and return the execution object (don't throw exception here) + LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "Cloudify workflow cancelled. Deployment is in an indeterminate state", "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Execute Workflow cancelled: " + workflowId); + return execution; + } + else { + // Can only get here after a timeout + LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "Cloudify workflow timeout", "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Execute Workflow: Timed Out"); + } + } + catch (CloudifyConnectException ce) { + LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "Execute Workflow (" + command + "): Cloudify connection failure: " + ce, "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Execute Workflow (" + command + "): Cloudify connection failure"); + savedException = ce; + } + catch (CloudifyResponseException re) { + LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "Execute Workflow (" + command + "): Cloudify response error: " + re, "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Execute Workflow (" + command + "): Cloudify error" + re.getMessage()); + savedException = re; + } + catch (RuntimeException e) { + // Catch-all + LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "Execute Workflow (" + command + "): Unexpected error: " + e, "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Execute Workflow (" + command + "): Internal error" + e.getMessage()); + savedException = e; + } + + // Get to this point ONLY on an error or timeout + // The cloudify execution is still running (we've not received a terminal status), + // so try to Cancel it. + CancelExecutionParams cancelParams = new CancelExecutionParams(); + cancelParams.setAction("cancel"); + // TODO: Use force_cancel? + + Execution cancelExecution = null; + + try { + CancelExecution cancelRequest = cloudify.executions().cancel(executionId, cancelParams); + LOGGER.debug (cancelRequest.toString()); + cancelExecution = cancelRequest.execute(); + + // Enter polling loop + boolean timedOut = false; + int cancelTimeout = timeout; // TODO: For now, just use same timeout + + String status = cancelExecution.getStatus(); + + // Poll for completion. Create a reusable cloudify query request + GetExecution queryExecution = cloudify.executions().byId(executionId); + + while (!timedOut && !status.equals(CANCELLED)) + { + // workflow is still running; check for timeout + if (cancelTimeout <= 0) { + LOGGER.debug ("Cancel timeout for workflow " + workflowId + " on deployment " + deploymentId); + timedOut = true; + continue; + } + + sleep(pollInterval * 1000L); + + cancelTimeout -= pollInterval; + LOGGER.debug("pollTimeout remaining: " + cancelTimeout); + + execution = queryExecution.execute(); + status = execution.getStatus(); + } + + // Broke the loop. Check again for a terminal state + if (status.equals(CANCELLED)){ + // Finished cancelling. Return the original exception + LOGGER.debug ("Cancel workflow " + workflowId + " completed on deployment " + deploymentId); + throw new MsoCloudifyException (-1, "", "", savedException); + } + else { + // Can only get here after a timeout + LOGGER.debug ("Cancel workflow " + workflowId + " timeout out on deployment " + deploymentId); + MsoCloudifyException exception = new MsoCloudifyException (-1, "", "", savedException); + exception.setPendingWorkflow(true); + throw exception; + } + } + catch (Exception e) { + // Catch-all. Log the message and throw the original exception +// LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "Execute Workflow (" + command + "): Unexpected error: " + e, "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Execute Workflow (" + command + "): Internal error" + e.getMessage()); + LOGGER.debug ("Cancel workflow " + workflowId + " failed for deployment " + deploymentId + ": " + e.getMessage()); + MsoCloudifyException exception = new MsoCloudifyException (-1, "", "", savedException); + exception.setPendingWorkflow(true); + throw exception; + } + } + + + + /** + * Query for a Cloudify Deployment (by Name). This call will always return a + * DeploymentInfo object. If the deployment does not exist, an "empty" DeploymentInfo will be + * returned - containing only the deployment ID and a special status of NOTFOUND. + * + * @param tenantId The Openstack ID of the tenant in which to query + * @param cloudSiteId The cloud identifier (may be a region) in which to query + * @param stackName The name of the stack to query (may be simple or canonical) + * @return A StackInfo object + * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception. + */ + public DeploymentInfo queryDeployment (String cloudSiteId, String tenantId, String deploymentId) + throws MsoException + { + LOGGER.debug ("Query Cloudify Deployment: " + deploymentId + " in tenant " + tenantId); + + // Obtain the cloud site information where we will create the stack + Optional<CloudSite> cloudSite = cloudConfig.getCloudSite (cloudSiteId); + if (!cloudSite.isPresent()) { + throw new MsoCloudSiteNotFound (cloudSiteId); + } + + Cloudify cloudify = getCloudifyClient (cloudSite.get()); + + // Build and send the Cloudify request + Deployment deployment = null; + DeploymentOutputs outputs = null; + try { + GetDeployment queryDeployment = cloudify.deployments().byId(deploymentId); + LOGGER.debug (queryDeployment.toString()); + +// deployment = queryDeployment.execute(); + deployment = executeAndRecordCloudifyRequest(queryDeployment); + + outputs = getDeploymentOutputs (cloudify, deploymentId); + + // Next look for the latest execution + ListExecutions listExecutions = cloudify.executions().listFiltered ("deployment_id=" + deploymentId, "-created_at"); + Executions executions = listExecutions.execute(); + + // If no executions, does this give NOT_FOUND or empty set? + if (executions.getItems().isEmpty()) { + return new DeploymentInfo (deployment); + } + else { + return new DeploymentInfo (deployment, outputs, executions.getItems().get(0)); + } + } + catch (CloudifyConnectException ce) { + // Couldn't connect to Cloudify + LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "QueryDeployment: Cloudify connection failure: " + ce, "", "", MsoLogger.ErrorCode.BusinessProcesssError, "QueryDeployment: Cloudify connection failure"); + throw new MsoIOException (ce.getMessage(), ce); + } + catch (CloudifyResponseException re) { + if (re.getStatus () == 404) { + // Got a NOT FOUND error. React differently based on deployment vs. execution + if (deployment != null) { + // Got NOT_FOUND on the executions. Assume this is a valid "empty" set + return new DeploymentInfo (deployment, outputs, null); + } else { + // Deployment not found. Default status of a DeploymentInfo object is NOTFOUND + return new DeploymentInfo (deploymentId); + } + } + throw new MsoCloudifyException (re.getStatus(), re.getMessage(), re.getLocalizedMessage(), re); + } + catch (Exception e) { + // Catch-all + throw new MsoAdapterException (e.getMessage(), e); + } + } + + + /** + * Delete a Cloudify deployment (by ID). If the deployment is not found, it will be + * considered a successful deletion. The return value is a DeploymentInfo object which + * contains the last deployment status. + * + * There is no rollback from a successful deletion. A deletion failure will + * also result in an undefined deployment state - the components may or may not have been + * all or partially deleted, so the resulting deployment must be considered invalid. + * + * @param tenantId The Openstack ID of the tenant in which to perform the delete + * @param cloudSiteId The cloud identifier (may be a region) from which to delete the stack. + * @param stackName The name/id of the stack to delete. May be simple or canonical + * @param pollForCompletion Indicator that polling should be handled in Java vs. in the client + * @return A StackInfo object + * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception. + * @throws MsoCloudSiteNotFound + */ + public DeploymentInfo uninstallAndDeleteDeployment (String cloudSiteId, + String tenantId, + String deploymentId, + int timeoutMinutes) throws MsoException + { + // Obtain the cloud site information where we will create the stack + Optional<CloudSite> cloudSite = cloudConfig.getCloudSite (cloudSiteId); + if (!cloudSite.isPresent()) { + throw new MsoCloudSiteNotFound (cloudSiteId); + } + + Cloudify cloudify = getCloudifyClient (cloudSite.get()); + + LOGGER.debug ("Ready to Uninstall/Delete Deployment (" + deploymentId + ")"); + + // Query first to save the trouble if deployment not found + Deployment deployment = null; + try { + GetDeployment queryDeploymentRequest = cloudify.deployments().byId(deploymentId); + LOGGER.debug (queryDeploymentRequest.toString()); + + deployment = executeAndRecordCloudifyRequest (queryDeploymentRequest); + } + catch (CloudifyResponseException e) { + // Since this came on the 'Create Deployment' command, nothing was changed + // in the cloud. Return the error as an exception. + if (e.getStatus () == 404) { + // Deployment doesn't exist. Return a "NOTFOUND" DeploymentInfo object + // TODO: Should return NULL? + LOGGER.debug("Deployment requested for deletion does not exist: " + deploymentId); + return new DeploymentInfo (deploymentId, DeploymentStatus.NOTFOUND); + } else { + // Convert the CloudifyResponseException to an MsoOpenstackException + LOGGER.debug("ERROR STATUS = " + e.getStatus() + ",\n" + e.getMessage() + "\n" + e.getLocalizedMessage()); + MsoException me = cloudifyExceptionToMsoException (e, DELETE_DEPLOYMENT); + me.setCategory (MsoExceptionCategory.INTERNAL); + throw me; + } + } catch (CloudifyConnectException e) { + // Error connecting to Cloudify instance. Convert to an MsoException + throw cloudifyExceptionToMsoException (e, DELETE_DEPLOYMENT); + } catch (RuntimeException e) { + // Catch-all + throw runtimeExceptionToMsoException (e, DELETE_DEPLOYMENT); + } + + /* + * Query the outputs before deleting so they can be returned as well + */ + DeploymentOutputs outputs = getDeploymentOutputs (cloudify, deploymentId); + + /* + * Next execute the "uninstall" workflow. + * Note - this assumes there are no additional parameters required for the workflow. + */ + // TODO: No deletePollInterval that I'm aware of. Use the create interval + int deletePollInterval = Integer.parseInt(this.environment.getProperty (deletePollIntervalProp, deletePollIntervalDefault)); + int pollTimeout = (timeoutMinutes * 60) + deletePollInterval; + + Execution uninstallWorkflow = null; + + try { + uninstallWorkflow = executeWorkflow (cloudify, deploymentId, "uninstall", null, true, pollTimeout, deletePollInterval); + + if (uninstallWorkflow.getStatus().equals(TERMINATED)) { + // Successful uninstall. + LOGGER.debug("Uninstall successful for deployment " + deploymentId); + } + else { + // The uninstall workflow completed with an error. Must fail the request, but will + // leave the deployment in an indeterminate state, as cloud resources may still exist. + MsoCloudifyException me = new MsoCloudifyException (0, "Uninstall Workflow Failed", uninstallWorkflow.getError()); + me.addContext (DELETE_DEPLOYMENT); + alarmLogger.sendAlarm(CLOUDIFY_ERROR, MsoAlarmLogger.CRITICAL, me.getContextMessage()); + throw me; + } + } + catch (MsoException me) { + // Uninstall workflow has failed. + // Must fail the deletion... may leave the deployment in an inconclusive state + me.addContext (DELETE_DEPLOYMENT); + alarmLogger.sendAlarm(CLOUDIFY_ERROR, MsoAlarmLogger.CRITICAL, me.getContextMessage()); + throw me; + } + + // At this point, the deployment has been successfully uninstalled. + // Next step is to delete the deployment itself + try { + DeleteDeployment deleteRequest = cloudify.deployments().deleteByName(deploymentId); + LOGGER.debug(deleteRequest.toString()); + + // The delete request returns the deleted deployment + deployment = deleteRequest.execute(); + + } + catch (CloudifyConnectException ce) { + // Failed to delete. Must fail the request, but will leave the (uninstalled) + // deployment in Cloudify DB. + MsoCloudifyException me = new MsoCloudifyException (0, "Deployment Delete Failed", ce.getMessage(), ce); + me.addContext (DELETE_DEPLOYMENT); + alarmLogger.sendAlarm(CLOUDIFY_ERROR, MsoAlarmLogger.CRITICAL, me.getContextMessage()); + throw me; + } + catch (CloudifyResponseException re) { + // Failed to delete. Must fail the request, but will leave the (uninstalled) + // deployment in the Cloudify DB. + MsoCloudifyException me = new MsoCloudifyException (re.getStatus(), re.getMessage(), re.getMessage(), re); + me.addContext (DELETE_DEPLOYMENT); + alarmLogger.sendAlarm(CLOUDIFY_ERROR, MsoAlarmLogger.CRITICAL, me.getContextMessage()); + throw me; + } + catch (Exception e) { + // Catch-all + MsoAdapterException ae = new MsoAdapterException (e.getMessage(), e); + ae.addContext (DELETE_DEPLOYMENT); + alarmLogger.sendAlarm(CLOUDIFY_ERROR, MsoAlarmLogger.CRITICAL, ae.getContextMessage()); + throw ae; + } + + // Return the deleted deployment info (with runtime outputs) along with the completed uninstall workflow status + return new DeploymentInfo (deployment, outputs, uninstallWorkflow); + } + + + /** + * Check if a blueprint is available for use at a targeted cloud site. + * This requires checking the Cloudify Manager which is servicing that + * cloud site to see if the specified blueprint has been loaded. + * + * @param cloudSiteId The cloud site where the blueprint is needed + * @param blueprintId The ID for the blueprint in Cloudify + */ + public boolean isBlueprintLoaded (String cloudSiteId, String blueprintId) + throws MsoException + { + // Obtain the cloud site information where we will load the blueprint + Optional<CloudSite> cloudSite = cloudConfig.getCloudSite (cloudSiteId); + if (!cloudSite.isPresent()) { + throw new MsoCloudSiteNotFound (cloudSiteId); + } + + Cloudify cloudify = getCloudifyClient (cloudSite.get()); + + GetBlueprint getRequest = cloudify.blueprints().getMetadataById(blueprintId); + try { + Blueprint bp = getRequest.execute(); + LOGGER.debug("Blueprint exists: " + bp.getId()); + return true; + } + catch (CloudifyResponseException ce) { + if (ce.getStatus() == 404) { + return false; + } else { + throw ce; + } + } catch (Exception e) { + throw e; + } + } + + /** + * Upload a blueprint to the Cloudify Manager that is servicing a Cloud Site. + * The blueprint currently must be structured as a single directory with all + * of the required files. One of those files is designated the "main file" + * for the blueprint. Files are provided as byte arrays, though expect only + * text files will be distributed from ASDC and stored by MSO. + * + * Cloudify requires a single root directory in its blueprint zip files. + * The requested blueprint ID will also be used as the directory. + * All of the files will be added to this directory in the zip file. + */ + public void uploadBlueprint (String cloudSiteId, + String blueprintId, + String mainFileName, + Map<String,byte[]> blueprintFiles, + boolean failIfExists) + throws MsoException + { + // Obtain the cloud site information where we will load the blueprint + Optional<CloudSite> cloudSite = cloudConfig.getCloudSite (cloudSiteId); + if (!cloudSite.isPresent()) { + throw new MsoCloudSiteNotFound (cloudSiteId); + } + + Cloudify cloudify = getCloudifyClient (cloudSite.get()); + + boolean blueprintUploaded = uploadBlueprint (cloudify, blueprintId, mainFileName, blueprintFiles); + + if (!blueprintUploaded && failIfExists) { + throw new MsoAdapterException ("Blueprint already exists"); + } + } + + /* + * Common method to load a blueprint. May be called from + */ + protected boolean uploadBlueprint (Cloudify cloudify, String blueprintId, String mainFileName, Map<String,byte[]> blueprintFiles) + throws MsoException + { + // Check if it already exists. If so, return false. + GetBlueprint getRequest = cloudify.blueprints().getMetadataById(blueprintId); + try { + Blueprint bp = getRequest.execute(); + LOGGER.debug("Blueprint " + bp.getId() + " already exists."); + return false; + } + catch (CloudifyResponseException ce) { + if (ce.getStatus() == 404) { + // This is the expected result. + LOGGER.debug("Verified that Blueprint doesn't exist yet"); + } else { + throw ce; + } + } catch (Exception e) { + throw e; + } + + // Create a blueprint ZIP file in memory + ByteArrayOutputStream zipBuffer = new ByteArrayOutputStream(); + ZipOutputStream zipOut = new ZipOutputStream(zipBuffer); + + try { + // Put the root directory + String rootDir = blueprintId + ((blueprintId.endsWith("/") ? "" : "/")); + zipOut.putNextEntry(new ZipEntry (rootDir)); + zipOut.closeEntry(); + + for (String fileName : blueprintFiles.keySet()) { + ZipEntry ze = new ZipEntry (rootDir + fileName); + zipOut.putNextEntry (ze); + zipOut.write (blueprintFiles.get(fileName)); + zipOut.closeEntry(); + } + zipOut.close(); + } + catch (IOException e) { + // Since we're writing to a byte array, this should never happen + } + LOGGER.debug ("Blueprint zip file size: " + zipBuffer.size()); + + // Ready to upload the blueprint zip + + try (InputStream blueprintStream = new ByteArrayInputStream (zipBuffer.toByteArray())) { + UploadBlueprint uploadRequest = cloudify.blueprints().uploadFromStream(blueprintId, mainFileName, blueprintStream); + Blueprint blueprint = uploadRequest.execute(); + System.out.println("Successfully uploaded blueprint " + blueprint.getId()); + } + catch (CloudifyResponseException | CloudifyConnectException e) { + throw cloudifyExceptionToMsoException (e, "UPLOAD_BLUEPRINT"); + } + catch (RuntimeException e) { + // Catch-all + throw runtimeExceptionToMsoException (e, "UPLOAD_BLUEPRINT"); + } catch (IOException e) { + // for try-with-resources + throw ioExceptionToMsoException(e, "UPLOAD_BLUEPRINT"); + } + + return true; + } + + + + // --------------------------------------------------------------- + // PRIVATE FUNCTIONS FOR USE WITHIN THIS CLASS + + /** + * Get a Cloudify client for the specified cloud site. + * Everything that is required can be found in the Cloud Config. + * + * @param cloudSite + * @return a Cloudify object + */ + public Cloudify getCloudifyClient (CloudSite cloudSite) throws MsoException + { + CloudifyManager cloudifyConfig = cloudConfig.getCloudifyManager(cloudSite.getCloudifyId()); + if (cloudifyConfig == null) { + throw new MsoCloudifyManagerNotFound (cloudConfig.getCloudSiteId(cloudSite)); + } + + // Get a Cloudify client + // Set a Token Provider to fetch tokens from Cloudify itself. + String cloudifyUrl = cloudifyConfig.getCloudifyUrl(); + Cloudify cloudify = new Cloudify (cloudifyUrl); + cloudify.setTokenProvider(new CloudifyClientTokenProvider(cloudifyUrl, cloudifyConfig.getUsername(), CryptoUtils.decryptCloudConfigPassword(cloudifyConfig.getPassword()))); + + return cloudify; + } + + + /* + * Query for a Cloudify Deployment. This function is needed in several places, so + * a common method is useful. This method takes an authenticated CloudifyClient + * (which internally identifies the cloud & tenant to search), and returns + * a Deployment object if found, Null if not found, or an MsoCloudifyException + * if the Cloudify API call fails. + * + * @param cloudifyClient an authenticated Cloudify client + * + * @param deploymentId the deployment to query + * + * @return a Deployment object or null if the requested deployment doesn't exist. + * + * @throws MsoCloudifyException Thrown if the Cloudify API call returns an exception + */ + protected Deployment queryDeployment (Cloudify cloudify, String deploymentId) throws MsoException { + if (deploymentId == null) { + return null; + } + try { + GetDeployment request = cloudify.deployments().byId (deploymentId); + return executeAndRecordCloudifyRequest (request); + } catch (CloudifyResponseException e) { + if (e.getStatus () == 404) { + LOGGER.debug ("queryDeployment - not found: " + deploymentId); + return null; + } else { + // Convert the CloudifyResponseException to an MsoCloudifyException + throw cloudifyExceptionToMsoException (e, "QueryDeployment"); + } + } catch (CloudifyConnectException e) { + // Connection to Openstack failed + throw cloudifyExceptionToMsoException (e, "QueryDeployment"); + } + } + + + public void copyStringOutputsToInputs(Map<String, String> inputs, + Map<String, Object> otherStackOutputs, boolean overWrite) { + if (inputs == null || otherStackOutputs == null) + return; + + for (Map.Entry<String, Object> entry : otherStackOutputs.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (value instanceof JsonNode) { + // This is a bit of mess - but I think it's the least impacting + // let's convert it BACK to a string - then it will get converted back later + try { + inputs.put(key, this.convertNode((JsonNode) value)); + } catch (Exception e) { + LOGGER.debug("WARNING: unable to convert JsonNode output value for "+ key); + //effect here is this value will not have been copied to the inputs - and therefore will error out downstream + } + } else if (value instanceof java.util.LinkedHashMap) { + LOGGER.debug("LinkedHashMap - this is showing up as a LinkedHashMap instead of JsonNode"); + try { + inputs.put(key, JSON_MAPPER.writeValueAsString(value)); + } catch (Exception e) { + LOGGER.debug("WARNING: unable to convert LinkedHashMap output value for "+ key); + } + } else { + // just try to cast it - could be an integer or some such + try { + inputs.put(key, (String) value); + } catch (Exception e) { + LOGGER.debug("WARNING: unable to convert output value for "+ key); + //effect here is this value will not have been copied to the inputs - and therefore will error out downstream + } + } + } + return; + } + + /* + * Normalize an input value to an Object, based on the target parameter type. + * If the type is not recognized, it will just be returned unchanged (as a string). + */ + public Object convertInputValue (String inputValue, HeatTemplateParam templateParam) + { + String type = templateParam.getParamType(); + LOGGER.debug("Parameter: " + templateParam.getParamName() + " is of type " + type); + + if (type.equalsIgnoreCase("number")) { + try { + return Integer.valueOf(inputValue); + } + catch (Exception e) { + LOGGER.debug("Unable to convert " + inputValue + " to an integer!"); + return null; + } + } else if (type.equalsIgnoreCase("json")) { + try { + return new ObjectMapper().readTree(inputValue); + } + catch (Exception e) { + LOGGER.debug("Unable to convert " + inputValue + " to a JsonNode!"); + return null; + } + } else if (type.equalsIgnoreCase("boolean")) { + return new Boolean(inputValue); + } + + // Nothing else matched. Return the original string + return inputValue; + } + + + private String convertNode(final JsonNode node) { + try { + final Object obj = JSON_MAPPER.treeToValue(node, Object.class); + return JSON_MAPPER.writeValueAsString(obj); + } catch (JsonParseException jpe) { + LOGGER.debug("Error converting json to string " + jpe.getMessage()); + } catch (Exception e) { + LOGGER.debug("Error converting json to string " + e.getMessage()); + } + return "[Error converting json to string]"; + } + + + /* + * Method to execute a Cloudify command and track its execution time. + * For the metrics log, a category of "Cloudify" is used along with a + * sub-category that identifies the specific call (using the real + * cloudify-client classname of the CloudifyRequest<T> parameter). + */ + + + protected <T> T executeAndRecordCloudifyRequest (CloudifyRequest <T> request) { + + String requestType; + if (request.getClass ().getEnclosingClass () != null) { + requestType = request.getClass ().getEnclosingClass ().getSimpleName () + "." + + request.getClass ().getSimpleName (); + } else { + requestType = request.getClass ().getSimpleName (); + } + + int retryDelay = poConfig.getRetryDelay(); + int retryCount = poConfig.getRetryCount(); + String retryCodes = poConfig.getRetryCodes(); + + // Run the actual command. All exceptions will be propagated + while (true) + { + try { + return request.execute (); + } + catch (CloudifyResponseException e) { + boolean retry = false; + if (retryCodes != null ) { + int code = e.getStatus(); + LOGGER.debug ("Config values RetryDelay:" + retryDelay + " RetryCount:" + retryCount + " RetryCodes:" + retryCodes + " ResponseCode:" + code); + for (String rCode : retryCodes.split (",")) { + try { + if (retryCount > 0 && code == Integer.parseInt (rCode)) + { + retryCount--; + retry = true; + LOGGER.debug ("CloudifyResponseException ResponseCode:" + code + " request:" + requestType + " Retry indicated. Attempts remaining:" + retryCount); + break; + } + } catch (NumberFormatException e1) { + LOGGER.error (MessageEnum.RA_CONFIG_EXC, "No retries. Exception in parsing retry code in config:" + rCode, "", "", MsoLogger.ErrorCode.SchemaError, "Exception in parsing retry code in config"); + throw e; + } + } + } + if (retry) + { + sleep(retryDelay * 1000L); + } + else + throw e; // exceeded retryCount or code is not retryable + } + catch (CloudifyConnectException e) { + // Connection to Cloudify failed + if (retryCount > 0) + { + retryCount--; + LOGGER.debug (" request:" + requestType + " Retry indicated. Attempts remaining:" + retryCount); + sleep(retryDelay * 1000L); + } + else + throw e; + + } + } + } + /* + * Convert an Exception on a Cloudify call to an MsoCloudifyException. + * This method supports CloudifyResponseException and CloudifyConnectException. + */ + protected MsoException cloudifyExceptionToMsoException (CloudifyBaseException e, String context) { + MsoException me = null; + + if (e instanceof CloudifyResponseException) { + CloudifyResponseException re = (CloudifyResponseException) e; + + try { + // Failed Cloudify calls return an error entity body. + CloudifyError error = re.getResponse ().getErrorEntity (CloudifyError.class); + LOGGER.error (MessageEnum.RA_CONNECTION_EXCEPTION, CLOUDIFY, "Cloudify Error on " + context + ": " + error.getErrorCode(), CLOUDIFY, "", MsoLogger.ErrorCode.DataError, "Exception - Cloudify Error on " + context); + String fullError = error.getErrorCode() + ": " + error.getMessage(); + LOGGER.debug(fullError); + me = new MsoCloudifyException (re.getStatus(), + re.getMessage(), + fullError); + } catch (Exception e2) { + // Couldn't parse the body as a "CloudifyError". Report the original HTTP error. + LOGGER.error (MessageEnum.RA_CONNECTION_EXCEPTION, CLOUDIFY, "HTTP Error on " + context + ": " + re.getStatus() + "," + e.getMessage(), CLOUDIFY, "", MsoLogger.ErrorCode.DataError, "Exception - HTTP Error on " + context, e2); + me = new MsoCloudifyException (re.getStatus (), re.getMessage (), ""); + } + + // Add the context of the error + me.addContext (context); + + // Generate an alarm for 5XX and higher errors. + if (re.getStatus () >= 500) { + alarmLogger.sendAlarm (CLOUDIFY_ERROR, MsoAlarmLogger.CRITICAL, me.getContextMessage ()); + } + } else if (e instanceof CloudifyConnectException) { + CloudifyConnectException ce = (CloudifyConnectException) e; + + me = new MsoIOException (ce.getMessage ()); + me.addContext (context); + + // Generate an alarm for all connection errors. + alarmLogger.sendAlarm ("CloudifyIOError", MsoAlarmLogger.CRITICAL, me.getContextMessage ()); + LOGGER.error(MessageEnum.RA_CONNECTION_EXCEPTION, CLOUDIFY, "Cloudify connection error on " + context + ": " + e, CLOUDIFY, "", MsoLogger.ErrorCode.DataError, "Cloudify connection error on " + context); + } + + return me; + } + + + + /******************************************************************************* + * + * Methods (and associated utilities) to implement the VduPlugin interface + * + *******************************************************************************/ + + /** + * VduPlugin interface for instantiate function. + * + * This one is a bit more complex, in that it will first upload the blueprint if needed, + * then create the Cloudify deployment and execute the install workflow. + * + * This implementation also merges any parameters defined in the ENV file with the other + * other input parameters for any undefined parameters). + * The basic MsoCloudifyUtils separates blueprint management from deploument actions, + * but the VduPlugin does not declare blueprint management operations. + */ + @Override + public VduInstance instantiateVdu ( + CloudInfo cloudInfo, + String instanceName, + Map<String,Object> inputs, + VduModelInfo vduModel, + boolean rollbackOnFailure) + throws VduException + { + String cloudSiteId = cloudInfo.getCloudSiteId(); + String tenantId = cloudInfo.getTenantId(); + + // Translate the VDU ModelInformation structure to that which is needed for + // creating and uploading a blueprint. Use the model customization UUID as + // the blueprint identifier. + + String blueprintId = vduModel.getModelCustomizationUUID(); + + try { + + if (! isBlueprintLoaded (cloudSiteId, blueprintId)) { + LOGGER.debug ("Blueprint " + blueprintId + " is not loaded. Will upload it now."); + + // Prepare the blueprint inputs. Need the set of blueprint templates and files, + // plus the main blueprint name. + Map<String,byte[]> blueprintFiles = new HashMap<>(); + String mainTemplate = ""; + + // Add all of the blueprint artifacts from the VDU model + List<VduArtifact> vduArtifacts = vduModel.getArtifacts(); + for (VduArtifact vduArtifact: vduArtifacts) + { + // Add all artifacts to the blueprint, with one exception. + // ENVIRONMENT files will be processed later as additional parameters. + + ArtifactType artifactType = vduArtifact.getType(); + if (artifactType != ArtifactType.ENVIRONMENT) { + blueprintFiles.put(vduArtifact.getName(), vduArtifact.getContent()); + + if (artifactType == ArtifactType.MAIN_TEMPLATE) { + mainTemplate = vduArtifact.getName(); + } + } + } + + // Upload the blueprint package + uploadBlueprint(cloudSiteId, blueprintId, mainTemplate, blueprintFiles, false); + } + } + catch (Exception e) { + throw new VduException ("CloudifyUtils (instantiateVDU): blueprint Exception", e); + } + + + // Next, create and install a new deployment based on the blueprint. + // For Cloudify, the deploymentId is specified by the client. Just use the instance name + // as the ID. + + try { + // Query the Cloudify Deployment object and populate a VduInstance + DeploymentInfo deployment = createAndInstallDeployment (cloudSiteId, + tenantId, + instanceName, + blueprintId, + inputs, + true, // (poll for completion) + vduModel.getTimeoutMinutes(), + rollbackOnFailure); + + return deploymentInfoToVduInstance(deployment); + } + catch (Exception e) { + throw new VduException ("CloudifyUtils (instantiateVDU): Create-and-install-deployment Exception", e); + } + } + + + /** + * VduPlugin interface for query function. + */ + @Override + public VduInstance queryVdu (CloudInfo cloudInfo, String instanceId) + throws VduException + { + String cloudSiteId = cloudInfo.getCloudSiteId(); + String tenantId = cloudInfo.getTenantId(); + + try { + // Query the Cloudify Deployment object and populate a VduInstance + DeploymentInfo deployment = queryDeployment (cloudSiteId, tenantId, instanceId); + + return deploymentInfoToVduInstance(deployment); + } + catch (Exception e) { + throw new VduException ("Query VDU Exception", e); + } + } + + + /** + * VduPlugin interface for delete function. + */ + @Override + public VduInstance deleteVdu (CloudInfo cloudInfo, String instanceId, int timeoutMinutes) + throws VduException + { + String cloudSiteId = cloudInfo.getCloudSiteId(); + String tenantId = cloudInfo.getTenantId(); + + try { + // Uninstall and delete the Cloudify Deployment + DeploymentInfo deployment = uninstallAndDeleteDeployment (cloudSiteId, tenantId, instanceId, timeoutMinutes); + + // Populate a VduInstance based on the deleted Cloudify Deployment object + return deploymentInfoToVduInstance(deployment); + } + catch (Exception e) { + throw new VduException ("Delete VDU Exception", e); + } + } + + + /** + * VduPlugin interface for update function. + * + * Update is currently not supported in the MsoCloudifyUtils implementation. + * Just return a VduException. + * + */ + @Override + public VduInstance updateVdu ( + CloudInfo cloudInfo, + String instanceId, + Map<String,Object> inputs, + VduModelInfo vduModel, + boolean rollbackOnFailure) + throws VduException + { + throw new VduException ("CloudifyUtils: updateVDU interface not supported"); + } + + + /* + * Convert the local DeploymentInfo object (Cloudify-specific) to a generic VduInstance object + */ + protected VduInstance deploymentInfoToVduInstance (DeploymentInfo deployment) + { + VduInstance vduInstance = new VduInstance(); + + // only one ID in Cloudify, use for both VDU name and ID + vduInstance.setVduInstanceId(deployment.getId()); + vduInstance.setVduInstanceName(deployment.getId()); + + // Copy inputs and outputs + vduInstance.setInputs(deployment.getInputs()); + vduInstance.setOutputs(deployment.getOutputs()); + + // Translate the status elements + vduInstance.setStatus(deploymentStatusToVduStatus (deployment)); + + return vduInstance; + } + + protected VduStatus deploymentStatusToVduStatus (DeploymentInfo deployment) + { + VduStatus vduStatus = new VduStatus(); + + // Determine the status based on last action & status + // DeploymentInfo object should be enhanced to report a better status internally. + DeploymentStatus status = deployment.getStatus(); + + if (status == null) { + vduStatus.setState(VduStateType.UNKNOWN); + } + else if (status == DeploymentStatus.NOTFOUND) { + vduStatus.setState(VduStateType.NOTFOUND); + } + else if (status == DeploymentStatus.INSTALLED) { + vduStatus.setState(VduStateType.INSTANTIATED); + } + else if (status == DeploymentStatus.CREATED) { + // Deployment exists but is not installed. This shouldn't really happen, + // since create + install or uninstall + delete are always done together. + // But account for it anyway, assuming the operation is still in progress. + String lastAction = deployment.getLastAction(); + if (lastAction == null) + vduStatus.setState(VduStateType.INSTANTIATING); + else + vduStatus.setState(VduStateType.DELETING); + } + else if (status == DeploymentStatus.FAILED) { + vduStatus.setState(VduStateType.FAILED); + } else { + vduStatus.setState(VduStateType.UNKNOWN); + } + + vduStatus.setErrorMessage(deployment.getErrorMessage()); + vduStatus.setLastAction(new PluginAction(deployment.getLastAction(), deployment.getActionStatus(), deployment.getErrorMessage())); + + return vduStatus; + } + + /* + * Return an OpenstackConfig object as expected by Cloudify Openstack Plug-in. + * Base the values on the CloudSite definition. + */ + protected OpenstackConfig getOpenstackConfig (CloudSite cloudSite, String tenantId) { + OpenstackConfig openstackConfig = new OpenstackConfig(); + openstackConfig.setRegion (cloudSite.getRegionId()); + openstackConfig.setAuthUrl (cloudConfig.getIdentityService(cloudSite.getIdentityServiceId()).getIdentityUrl()); + openstackConfig.setUsername (cloudConfig.getIdentityService(cloudSite.getIdentityServiceId()).getMsoId()); + openstackConfig.setPassword (CryptoUtils.decryptCloudConfigPassword(cloudConfig.getIdentityService(cloudSite.getIdentityServiceId()).getMsoPass())); + openstackConfig.setTenantName (tenantId); + return openstackConfig; + } + + /* + * Return an Azure object as expected by Cloudify Azure Plug-in. + * Base the values on the CloudSite definition. + */ + protected AzureConfig getAzureConfig (CloudSite cloudSite, String tenantId) { + AzureConfig azureConfig = new AzureConfig(); + // TODO: Use adminTenant for now, instead of adding another element + azureConfig.setSubscriptionId (cloudSite.getIdentityService().getAdminTenant()); + azureConfig.setTenantId (tenantId); + azureConfig.setClientId (cloudSite.getIdentityService().getMsoId()); + azureConfig.setClientSecret (cloudSite.getIdentityService().getMsoPass()); + return azureConfig; + } + + private void sleep(long time) { + try { + Thread.sleep(time); + } catch (InterruptedException e) { + LOGGER.debug("Thread interrupted while sleeping!", e); + Thread.currentThread().interrupt(); + } + } +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/config/beans/PoConfig.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/config/beans/PoConfig.java new file mode 100644 index 0000000000..3098a5410a --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/config/beans/PoConfig.java @@ -0,0 +1,53 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.config.beans; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix="adapters.po") +public class PoConfig { + + private String retryCodes; + private int retryDelay; + private int retryCount; + + public String getRetryCodes() { + return retryCodes; + } + public void setRetryCodes(String retryCodes) { + this.retryCodes = retryCodes; + } + public int getRetryDelay() { + return retryDelay; + } + public void setRetryDelay(int retryDelay) { + this.retryDelay = retryDelay; + } + public int getRetryCount() { + return retryCount; + } + public void setRetryCount(int retryCount) { + this.retryCount = retryCount; + } + +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/beans/HeatCacheEntry.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/beans/HeatCacheEntry.java new file mode 100644 index 0000000000..5eaca976d0 --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/beans/HeatCacheEntry.java @@ -0,0 +1,60 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.openstack.beans; + +import java.io.Serializable; +import java.util.Calendar; + +import com.woorea.openstack.heat.Heat; + +/* + * An entry in the Heat Client Cache. It saves the Heat client object + * along with the token expiration. After this interval, this cache + * item will no longer be used. + */ +public class HeatCacheEntry implements Serializable { + + private static final long serialVersionUID = 1L; + + private String heatUrl; + private String token; + private Calendar expires; + + public HeatCacheEntry (String heatUrl, String token, Calendar expires) { + this.heatUrl = heatUrl; + this.token = token; + this.expires = expires; + } + + public Heat getHeatClient () { + Heat heatClient = new Heat (heatUrl); + heatClient.token (token); + return heatClient; + } + + public boolean isExpired () { + if (expires == null) { + return true; + } + + return System.currentTimeMillis() > expires.getTimeInMillis(); + } +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/beans/NeutronCacheEntry.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/beans/NeutronCacheEntry.java new file mode 100644 index 0000000000..d89fd1a73f --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/beans/NeutronCacheEntry.java @@ -0,0 +1,67 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.openstack.beans; + +import java.io.Serializable; +import java.util.Calendar; + +/* + * An entry in the Neutron Client Cache. It saves the Neutron client object + * along with the token expiration. After this interval, this cache + * item will no longer be used. + */ +public class NeutronCacheEntry implements Serializable { + private static final long serialVersionUID = 1L; + + private String neutronUrl; + private String token; + private Calendar expires; + + public NeutronCacheEntry (String neutronUrl, String token, Calendar expires) { + this.neutronUrl = neutronUrl; + this.token = token; + this.expires = expires; + } + + public String getNeutronUrl() { + return neutronUrl; + } + + public void setNeutronUrl(String neutronUrl) { + this.neutronUrl = neutronUrl; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public boolean isExpired() { + if (expires == null) { + return true; + } + + return System.currentTimeMillis() > expires.getTimeInMillis(); + } +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/beans/VnfRollback.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/beans/VnfRollback.java new file mode 100644 index 0000000000..bb8aa92281 --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/beans/VnfRollback.java @@ -0,0 +1,216 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.openstack.beans; + +import org.onap.so.entity.MsoRequest; +import org.springframework.stereotype.Component; +/** + * Javabean representing the rollback criteria following a "Create VNF" + * operation. This structure can be passed back to the "Rollback VNF" + * operation to undo the effects of the create. + * + * + */ +@Component +public class VnfRollback { + private String vnfId; + private String tenantId; + private String cloudSiteId; + private boolean tenantCreated = false; + private boolean vnfCreated = false; + private MsoRequest msoRequest; + private String volumeGroupName; + private String volumeGroupId; + private String requestType; + private String volumeGroupHeatStackId; + private String baseGroupHeatStackId; + private boolean isBase = false; + private String vfModuleStackId; + private String modelCustomizationUuid; //NOTE: this is the vfModule's modelCustomizationUuid + private String mode = "HEAT"; + + public VnfRollback() {} + + /** + * For backwards compatibility... orchestration mode defaults to HEAT + * + * @param vnfId + * @param tenantId + * @param cloudSiteId + * @param tenantCreated + * @param vnfCreated + * @param msoRequest + * @param volumeGroupName + * @param volumeGroupId + * @param requestType + * @param modelCustomizationUuid + */ + public VnfRollback(String vnfId, String tenantId, String cloudSiteId, + boolean tenantCreated, boolean vnfCreated, + MsoRequest msoRequest, + String volumeGroupName, String volumeGroupId, String requestType, String modelCustomizationUuid) { + super(); + this.vnfId = vnfId; + this.tenantId = tenantId; + this.cloudSiteId = cloudSiteId; + this.tenantCreated = tenantCreated; + this.vnfCreated = vnfCreated; + this.msoRequest = msoRequest; + this.volumeGroupName = volumeGroupName; + this.volumeGroupId = volumeGroupId; + this.requestType = requestType; + this.modelCustomizationUuid = modelCustomizationUuid; + } + + /** + * For backwards compatibility... orchestration mode defaults to HEAT + * + * @param vnfId + * @param tenantId + * @param cloudSiteId + * @param tenantCreated + * @param vnfCreated + * @param msoRequest + * @param volumeGroupName + * @param volumeGroupId + * @param requestType + * @param modelCustomizationUuid + */ + public VnfRollback(String vnfId, String tenantId, String cloudSiteId, + boolean tenantCreated, boolean vnfCreated, + MsoRequest msoRequest, String volumeGroupName, String volumeGroupId, + String requestType, String modelCustomizationUuid, String orchestrationMode) { + super(); + this.vnfId = vnfId; + this.tenantId = tenantId; + this.cloudSiteId = cloudSiteId; + this.tenantCreated = tenantCreated; + this.vnfCreated = vnfCreated; + this.msoRequest = msoRequest; + this.volumeGroupName = volumeGroupName; + this.volumeGroupId = volumeGroupId; + this.requestType = requestType; + this.modelCustomizationUuid = modelCustomizationUuid; + this.mode = orchestrationMode; + } + + public String getVnfId() { + return vnfId; + } + public void setVnfId(String vnfId) { + this.vnfId = vnfId; + } + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + public String getCloudSiteId() { + return cloudSiteId; + } + public void setCloudSiteId(String cloudId) { + this.cloudSiteId = cloudId; + } + public boolean getTenantCreated() { + return tenantCreated; + } + public void setTenantCreated(boolean tenantCreated) { + this.tenantCreated = tenantCreated; + } + public boolean getVnfCreated() { + return vnfCreated; + } + public void setVnfCreated(boolean vnfCreated) { + this.vnfCreated = vnfCreated; + } + public MsoRequest getMsoRequest() { + return msoRequest; + } + public void setMsoRequest (MsoRequest msoRequest) { + this.msoRequest = msoRequest; + } + public String getVolumeGroupName() { + return this.volumeGroupName; + } + public void setVolumeGroupName(String volumeGroupName) { + this.volumeGroupName = volumeGroupName; + } + public String getVolumeGroupId() { + return this.volumeGroupId; + } + public void setVolumeGroupId(String volumeGroupId) { + this.volumeGroupId = volumeGroupId; + } + public String getRequestType() { + return this.requestType; + } + public void setRequestType(String requestType) { + this.requestType = requestType; + } + public String getVolumeGroupHeatStackId() { + return this.volumeGroupHeatStackId; + } + public void setVolumeGroupHeatStackId(String volumeGroupHeatStackId) { + this.volumeGroupHeatStackId = volumeGroupHeatStackId; + } + + public String getBaseGroupHeatStackId() { + return this.baseGroupHeatStackId; + } + public void setBaseGroupHeatStackId(String baseGroupHeatStackId) { + this.baseGroupHeatStackId = baseGroupHeatStackId; + } + + public boolean isBase() { + return this.isBase; + } + public void setIsBase(boolean isBase) { + this.isBase = isBase; + } + public String getVfModuleStackId() { + return this.vfModuleStackId; + } + public void setVfModuleStackId(String vfModuleStackId) { + this.vfModuleStackId = vfModuleStackId; + } + public String getModelCustomizationUuid() { + return this.modelCustomizationUuid; + } + public void setModelCustomizationUuid(String modelCustomizationUuid) { + this.modelCustomizationUuid = modelCustomizationUuid; + } + public String getMode() { + return this.mode; + } + public void setMode(String mode) { + this.mode = mode; + } + @Override + public String toString() { + return "VnfRollback: cloud=" + cloudSiteId + ", tenant=" + tenantId + + ", vnf=" + vnfId + ", tenantCreated=" + tenantCreated + + ", vnfCreated=" + vnfCreated + ", requestType = " + requestType + + ", modelCustomizationUuid=" + this.modelCustomizationUuid + + ", mode=" + mode; + } +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoCommonUtils.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoCommonUtils.java new file mode 100644 index 0000000000..98793601d0 --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoCommonUtils.java @@ -0,0 +1,303 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.openstack.utils; + + +import java.io.IOException; + +import org.onap.so.config.beans.PoConfig; +import org.onap.so.logger.MessageEnum; +import org.onap.so.logger.MsoAlarmLogger; +import org.onap.so.logger.MsoLogger; +import org.onap.so.openstack.exceptions.MsoAdapterException; +import org.onap.so.openstack.exceptions.MsoException; +import org.onap.so.openstack.exceptions.MsoExceptionCategory; +import org.onap.so.openstack.exceptions.MsoIOException; +import org.onap.so.openstack.exceptions.MsoOpenstackException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.woorea.openstack.base.client.OpenStackBaseException; +import com.woorea.openstack.base.client.OpenStackConnectException; +import com.woorea.openstack.base.client.OpenStackRequest; +import com.woorea.openstack.base.client.OpenStackResponseException; +import com.woorea.openstack.heat.model.Explanation; +import com.woorea.openstack.keystone.model.Error; +import com.woorea.openstack.quantum.model.NeutronError; + +@Component("CommonUtils") +public class MsoCommonUtils { + + private static MsoLogger logger = MsoLogger.getMsoLogger(MsoLogger.Catalog.RA, MsoCommonUtils.class); + protected static MsoAlarmLogger alarmLogger = new MsoAlarmLogger(); + + @Autowired + private PoConfig poConfig; + /* + * Method to execute an Openstack command and track its execution time. + * For the metrics log, a category of "Openstack" is used along with a + * sub-category that identifies the specific call (using the real + * openstack-java-sdk classname of the OpenStackRequest<T> parameter). + */ + + protected <T> T executeAndRecordOpenstackRequest (OpenStackRequest <T> request) { + + int limit; + + long start = System.currentTimeMillis (); + String requestType; + if (request.getClass ().getEnclosingClass () != null) { + requestType = request.getClass ().getEnclosingClass ().getSimpleName () + "." + + request.getClass ().getSimpleName (); + } else { + requestType = request.getClass ().getSimpleName (); + } + + int retryDelay = poConfig.getRetryDelay(); + int retryCount = poConfig.getRetryCount(); + String retryCodes = poConfig.getRetryCodes(); + + // Run the actual command. All exceptions will be propagated + while (true) + { + try { + return request.execute (); + } + catch (OpenStackResponseException e) { + boolean retry = false; + if (retryCodes != null ) { + int code = e.getStatus(); + logger.debug ("Config values RetryDelay:" + retryDelay + " RetryCount:" + retryCount + " RetryCodes:" + retryCodes + " ResponseCode:" + code); + for (String rCode : retryCodes.split (",")) { + try { + if (retryCount > 0 && code == Integer.parseInt (rCode)) + { + retryCount--; + retry = true; + logger.debug ("OpenStackResponseException ResponseCode:" + code + " request:" + requestType + " Retry indicated. Attempts remaining:" + retryCount); + break; + } + } catch (NumberFormatException e1) { + logger.error (MessageEnum.RA_CONFIG_EXC, "No retries. Exception in parsing retry code in config:" + rCode, "", "", MsoLogger.ErrorCode.SchemaError, "Exception in parsing retry code in config"); + throw e; + } + } + } + if (retry) + { + try { + Thread.sleep (retryDelay * 1000L); + } catch (InterruptedException e1) { + logger.debug ("Thread interrupted while sleeping", e1); + Thread.currentThread().interrupt(); + } + } + else + throw e; // exceeded retryCount or code is not retryable + } + catch (OpenStackConnectException e) { + // Connection to Openstack failed + if (retryCount > 0) + { + retryCount--; + logger.debug (" request:" + requestType + " Retry indicated. Attempts remaining:" + retryCount); + try { + Thread.sleep (retryDelay * 1000L); + } catch (InterruptedException e1) { + logger.debug ("Thread interrupted while sleeping", e1); + Thread.currentThread().interrupt(); + } + } + else + throw e; + + } + } + } + + /* + * Convert an Openstack Exception on a Keystone call to an MsoException. + * This method supports both OpenstackResponseException and OpenStackConnectException. + */ + protected MsoException keystoneErrorToMsoException (OpenStackBaseException e, String context) { + MsoException me = null; + + if (e instanceof OpenStackResponseException) { + OpenStackResponseException re = (OpenStackResponseException) e; + + try { + // Failed Keystone calls return an Error entity body. + Error error = re.getResponse ().getErrorEntity (Error.class); + logger.error (MessageEnum.RA_CONNECTION_EXCEPTION, "Openstack Keystone Error on " + context + ": " + error, "Openstack", "", MsoLogger.ErrorCode.DataError, "Openstack Keystone Error on " + context); + me = new MsoOpenstackException (error.getCode (), error.getTitle (), error.getMessage ()); + } catch (Exception e2) { + // Can't parse the body as an "Error". Report the HTTP error + logger.error (MessageEnum.RA_CONNECTION_EXCEPTION, "HTTP Error on " + context + ": " + re.getStatus() + "," + re.getMessage(), "Openstack", "", MsoLogger.ErrorCode.DataError, "HTTP Error on " + context, e2); + me = new MsoOpenstackException (re.getStatus (), re.getMessage (), ""); + } + + // Add the context of the error + me.addContext (context); + + // Generate an alarm for 5XX and higher errors. + if (re.getStatus () >= 500) { + alarmLogger.sendAlarm ("KeystoneError", MsoAlarmLogger.CRITICAL, me.getContextMessage ()); + } + } else if (e instanceof OpenStackConnectException) { + OpenStackConnectException ce = (OpenStackConnectException) e; + + me = new MsoIOException (ce.getMessage ()); + me.addContext (context); + + // Generate an alarm for all connection errors. + logger.error(MessageEnum.RA_GENERAL_EXCEPTION_ARG, "Openstack Keystone connection error on " + context + ": " + e, "Openstack", "", MsoLogger.ErrorCode.DataError, "Openstack Keystone connection error on " + context); + alarmLogger.sendAlarm ("KeystoneIOError", MsoAlarmLogger.CRITICAL, me.getContextMessage ()); + } + + return me; + } + + /* + * Convert an Openstack Exception on a Heat call to an MsoOpenstackException. + * This method supports both OpenstackResponseException and OpenStackConnectException. + */ + protected MsoException heatExceptionToMsoException (OpenStackBaseException e, String context) { + MsoException me = null; + + if (e instanceof OpenStackResponseException) { + OpenStackResponseException re = (OpenStackResponseException) e; + + try { + // Failed Heat calls return an Explanation entity body. + Explanation explanation = re.getResponse ().getErrorEntity (Explanation.class); + logger.error (MessageEnum.RA_CONNECTION_EXCEPTION, "OpenStack", "Openstack Error on " + context + ": " + explanation.toString(), "Openstack", "", MsoLogger.ErrorCode.DataError, "Exception - Openstack Error on " + context); + String fullError = explanation.getExplanation() + ", error.type=" + explanation.getError().getType() + ", error.message=" + explanation.getError().getMessage(); + logger.debug(fullError); + me = new MsoOpenstackException (explanation.getCode (), + explanation.getTitle (), + //explanation.getExplanation ()); + fullError); + } catch (Exception e2) { + // Couldn't parse the body as an "Explanation". Report the original HTTP error. + logger.error (MessageEnum.RA_CONNECTION_EXCEPTION, "OpenStack", "HTTP Error on " + context + ": " + re.getStatus() + "," + e.getMessage(), "Openstack", "", MsoLogger.ErrorCode.DataError, "Exception - HTTP Error on " + context, e2); + me = new MsoOpenstackException (re.getStatus (), re.getMessage (), ""); + } + + // Add the context of the error + me.addContext (context); + + // Generate an alarm for 5XX and higher errors. + if (re.getStatus () >= 500) { + alarmLogger.sendAlarm ("HeatError", MsoAlarmLogger.CRITICAL, me.getContextMessage ()); + } + } else if (e instanceof OpenStackConnectException) { + OpenStackConnectException ce = (OpenStackConnectException) e; + + me = new MsoIOException (ce.getMessage ()); + me.addContext (context); + + // Generate an alarm for all connection errors. + alarmLogger.sendAlarm ("HeatIOError", MsoAlarmLogger.CRITICAL, me.getContextMessage ()); + logger.error(MessageEnum.RA_CONNECTION_EXCEPTION, "OpenStack", "Openstack Heat connection error on " + context + ": " + e, "Openstack", "", MsoLogger.ErrorCode.DataError, "Openstack Heat connection error on " + context); + } + + return me; + } + + /* + * Convert an Openstack Exception on a Neutron call to an MsoOpenstackException. + * This method supports both OpenstackResponseException and OpenStackConnectException. + */ + protected MsoException neutronExceptionToMsoException (OpenStackBaseException e, String context) { + MsoException me = null; + + if (e instanceof OpenStackResponseException) { + OpenStackResponseException re = (OpenStackResponseException) e; + + try { + // Failed Neutron calls return an NeutronError entity body + NeutronError error = re.getResponse ().getErrorEntity (NeutronError.class); + logger.error (MessageEnum.RA_CONNECTION_EXCEPTION, "OpenStack", "Openstack Neutron Error on " + context + ": " + error, "Openstack", "", MsoLogger.ErrorCode.DataError, "Openstack Neutron Error on " + context); + me = new MsoOpenstackException (re.getStatus (), error.getType (), error.getMessage ()); + } catch (Exception e2) { + // Couldn't parse body as a NeutronError. Report the HTTP error. + logger.error (MessageEnum.RA_CONNECTION_EXCEPTION, "OpenStack", "HTTP Error on " + context + ": " + re.getStatus() + "," + e.getMessage(), "Openstack", "", MsoLogger.ErrorCode.DataError, "Openstack HTTP Error on " + context, e2); + me = new MsoOpenstackException (re.getStatus (), re.getMessage (), null); + } + + // Add the context of the error + me.addContext (context); + + // Generate an alarm for 5XX and higher errors. + if (re.getStatus () >= 500) { + alarmLogger.sendAlarm ("NeutronError", MsoAlarmLogger.CRITICAL, me.getContextMessage ()); + } + } else if (e instanceof OpenStackConnectException) { + OpenStackConnectException ce = (OpenStackConnectException) e; + + me = new MsoIOException (ce.getMessage ()); + me.addContext (context); + + // Generate an alarm for all connection errors. + alarmLogger.sendAlarm ("NeutronIOError", MsoAlarmLogger.CRITICAL, me.getContextMessage ()); + logger.error(MessageEnum.RA_CONNECTION_EXCEPTION, "OpenStack", "Openstack Neutron Connection error on "+ context + ": " + e, "OpenStack", "", MsoLogger.ErrorCode.DataError, "Openstack Neutron Connection error on "+ context); + } + + return me; + } + + /* + * Convert a Java Runtime Exception to an MsoException. + * All Runtime exceptions will be translated into an MsoAdapterException, + * which captures internal errors. + * Alarms will be generated on all such exceptions. + */ + protected MsoException runtimeExceptionToMsoException (RuntimeException e, String context) { + MsoAdapterException me = new MsoAdapterException (e.getMessage (), e); + me.addContext (context); + me.setCategory (MsoExceptionCategory.INTERNAL); + + // Always generate an alarm for internal exceptions + logger.error(MessageEnum.RA_GENERAL_EXCEPTION_ARG, "An exception occured on "+ context + ": " + e, "OpenStack", "", MsoLogger.ErrorCode.DataError, "An exception occured on "+ context); + alarmLogger.sendAlarm ("AdapterInternalError", MsoAlarmLogger.CRITICAL, me.getContextMessage ()); + + return me; + } + + protected MsoException ioExceptionToMsoException(IOException e, String context) { + MsoAdapterException me = new MsoAdapterException (e.getMessage (), e); + me.addContext (context); + me.setCategory (MsoExceptionCategory.INTERNAL); + + // Always generate an alarm for internal exceptions + logger.error(MessageEnum.RA_GENERAL_EXCEPTION_ARG, "An exception occured on "+ context + ": " + e, "OpenStack", "", MsoLogger.ErrorCode.DataError, "An exception occured on "+ context); + alarmLogger.sendAlarm ("AdapterInternalError", MsoAlarmLogger.CRITICAL, me.getContextMessage ()); + + return me; + } + + public boolean isNullOrEmpty (String s) { + return s == null || s.isEmpty(); + } + + + +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoHeatEnvironmentEntry.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoHeatEnvironmentEntry.java new file mode 100644 index 0000000000..c95e62dad0 --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoHeatEnvironmentEntry.java @@ -0,0 +1,257 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * Copyright (C) 2017 Huawei Technologies Co., Ltd. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.openstack.utils; + + + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +import org.onap.so.db.catalog.beans.HeatTemplateParam; +import org.onap.so.logger.MsoLogger; + +public class MsoHeatEnvironmentEntry { + + private static final MsoLogger LOGGER = MsoLogger.getMsoLogger (MsoLogger.Catalog.RA, MsoHeatEnvironmentEntry.class); + + private Set<MsoHeatEnvironmentParameter> parameters = null; + private Set<MsoHeatEnvironmentResource> resources = null; + private StringBuilder rawEntry = null; + private boolean valid = true; + private String errorString = null; + private StringBuilder resourceRegistryEntryRaw = null; + + public MsoHeatEnvironmentEntry() { + super(); + } + + public MsoHeatEnvironmentEntry(StringBuilder sb) { + this(); + this.rawEntry = sb; + this.processRawEntry(); + } + + private void processRawEntry() { + try { + if (this.rawEntry == null || "".equals(this.rawEntry.toString())) + return; + byte[] b = this.rawEntry.toString().getBytes(); + MsoYamlEditorWithEnvt yaml = new MsoYamlEditorWithEnvt(b); + this.parameters = yaml.getParameterListFromEnvt(); + //this.resources = yaml.getResourceListFromEnvt(); + StringBuilder sb = this.getResourceRegistryRawEntry(); + if (sb == null) { + this.resourceRegistryEntryRaw = new StringBuilder(""); + } else { + this.resourceRegistryEntryRaw = sb; + } + } catch (Exception e) { + LOGGER.debug("Exception:", e); + this.valid = false; + this.errorString = e.getMessage(); + //e.printStackTrace(); + } + } + + public boolean isValid() { + return this.valid; + } + public String getErrorString() { + return this.errorString; + } + + public Set<MsoHeatEnvironmentParameter> getParameters() { + return this.parameters; + } + public Set<MsoHeatEnvironmentResource> getResources() { + return this.resources; + } + public void setParameters(Set<MsoHeatEnvironmentParameter> paramSet) { + if (paramSet == null) { + this.parameters = null; + } else { + this.parameters = paramSet; + } + } + public void setResources(Set<MsoHeatEnvironmentResource> resourceSet) { + if (resourceSet == null) { + this.resources = null; + } else { + this.resources = resourceSet; + } + } + + public void addParameter(MsoHeatEnvironmentParameter hep) { + if (this.parameters == null) { + this.parameters = new HashSet<>(); + } + this.parameters.add(hep); + } + public void addResource(MsoHeatEnvironmentResource her) { + if (this.resources == null) { + this.resources = new HashSet<>(); + } + this.resources.add(her); + } + + public int getNumberOfParameters() { + return this.parameters.size(); + } + public int getNumberOfResources() { + return this.resources.size(); + } + + public boolean hasResources() { + if (this.resources != null && this.resources.size() > 0) { + return true; + } + return false; + } + public boolean hasParameters() { + if (this.parameters != null && this.parameters.size() > 0) { + return true; + } + return false; + } + + public boolean containsParameter(String paramName) { + boolean contains = false; + if (this.parameters == null || this.parameters.size() < 1) { + return false; + } + if (this.parameters.contains(new MsoHeatEnvironmentParameter(paramName))) { + contains = true; + } + return contains; + } + + public boolean containsParameter(String paramName, String paramAlias) { + if (this.containsParameter(paramName)) { + return true; + } + if (this.containsParameter(paramAlias)) { + return true; + } + return false; + } + + @Override + public String toString() { + return "MsoHeatEnvironmentEntry{" + "parameters=" + parameters + + ", resourceRegistryEntryRaw='" + resourceRegistryEntryRaw + '\'' + + '}'; + } + + public StringBuilder toFullStringExcludeNonParams(Set<HeatTemplateParam> params) { + // Basically give back the envt - but exclude the params that aren't in the HeatTemplate + + StringBuilder sb = new StringBuilder(); + ArrayList<String> paramNameList = new ArrayList<String>(params.size()); + for (HeatTemplateParam htp : params) { + paramNameList.add(htp.getParamName()); + } + + if (this.hasParameters()) { + sb.append("parameters:\n"); + for (MsoHeatEnvironmentParameter hep : this.parameters) { + String paramName = hep.getName(); + if (paramNameList.contains(paramName)) { + // This parameter *is* in the Heat Template - so include it: + sb.append(" " + hep.getName() + ": " + hep.getValue() + "\n"); + // New - 1607 - if any of the params mapped badly - JUST RETURN THE ORIGINAL ENVT! + if (hep.getValue().startsWith("_BAD")) { + return this.rawEntry; + } + } + } + sb.append("\n"); + } +// if (this.hasResources()) { +// sb.append("resource_registry:\n"); +// for (MsoHeatEnvironmentResource her : this.resources) { +// sb.append(" \"" + her.getName() + "\": " + her.getValue() + "\n"); +// } +// } + sb.append("\n"); + sb.append(this.resourceRegistryEntryRaw); + return sb; + } + + public StringBuilder toFullString() { + StringBuilder sb = new StringBuilder(); + + if (this.hasParameters()) { + sb.append("parameters:\n"); + for (MsoHeatEnvironmentParameter hep : this.parameters) { + sb.append(" " + hep.getName() + ": " + hep.getValue() + "\n"); + } + sb.append("\n"); + } +// if (this.hasResources()) { +// sb.append("resource_registry:\n"); +// for (MsoHeatEnvironmentResource her : this.resources) { +// sb.append(" \"" + her.getName() + "\": " + her.getValue() + "\n"); +// } +// } + sb.append("\n"); + sb.append(this.resourceRegistryEntryRaw); + return sb; + } + + public StringBuilder getRawEntry() { + return this.rawEntry; + } + + private StringBuilder getResourceRegistryRawEntry() { + + if (this.rawEntry == null) { + return null; + } + + StringBuilder sb = new StringBuilder(); + int indexOf = this.rawEntry.indexOf("resource_registry:"); + if (indexOf < 0) { // no resource_registry: + return null; + } + sb.append(this.rawEntry.substring(indexOf)); + return sb; + } + + public void setHPAParameters(StringBuilder hpasb) { + try { + MsoYamlEditorWithEnvt yaml = new MsoYamlEditorWithEnvt(hpasb.toString().getBytes()); + Set<MsoHeatEnvironmentParameter> hpaParams = yaml.getParameterListFromEnvt(); + for (MsoHeatEnvironmentParameter hpaparam : hpaParams) { + for (MsoHeatEnvironmentParameter param : this.parameters) { + if (param.getName() == hpaparam.getName()) { + param.setValue(hpaparam.getValue()); + } + } + } + } catch (Exception e) { + LOGGER.debug("Exception:", e); + this.errorString = e.getMessage(); + //e.printStackTrace(); + } + } +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoHeatEnvironmentParameter.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoHeatEnvironmentParameter.java new file mode 100644 index 0000000000..7e4c9d00c4 --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoHeatEnvironmentParameter.java @@ -0,0 +1,77 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.openstack.utils; + +import java.util.Objects; + +public class MsoHeatEnvironmentParameter { + + private String name; + private String value; + + public MsoHeatEnvironmentParameter(String name, String value) { + super(); + this.name = name; + this.value = value; + } + public MsoHeatEnvironmentParameter(String name) { + // Allow to initialize with a null value + this(name, null); + } + public MsoHeatEnvironmentParameter() { + this(null, null); + } + + public String getName() { + return this.name; + } + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return this.value; + } + public void setValue(String value) { + this.value = value; + } + public String toString() { + return this.name + ": " + this.value; + } + + public boolean equals(Object o) { + if (!(o instanceof MsoHeatEnvironmentParameter)) { + return false; + } + if (this == o) { + return true; + } + MsoHeatEnvironmentParameter hep = (MsoHeatEnvironmentParameter) o; + // If the name of the parameter is the same, then they're equal + return hep.getName().equals(this.getName()); + } + + public int hashCode() { + return Objects.hashCode(this.name); + } + + +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoHeatEnvironmentResource.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoHeatEnvironmentResource.java new file mode 100644 index 0000000000..c174b58f95 --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoHeatEnvironmentResource.java @@ -0,0 +1,96 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * Copyright (C) 2017 Huawei Technologies Co., Ltd. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.openstack.utils; + +import org.onap.so.logger.MsoLogger; + +public class MsoHeatEnvironmentResource { + + private static final MsoLogger LOGGER = MsoLogger.getMsoLogger (MsoLogger.Catalog.RA, MsoHeatEnvironmentResource.class); + + private String name; + private String value; + + public MsoHeatEnvironmentResource(String name, String value) { + super(); + this.name = name; + this.value = value; + } + public MsoHeatEnvironmentResource(String name) { + // Allow to initialize with a null value + this(name, null); + } + public MsoHeatEnvironmentResource() { + this(null, null); + } + + public String getName() { + return this.name; + } + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return this.value; + } + public void setValue(String value) { + this.value = value; + } + + @Override + public String toString() { + return "\"" + + this.name + + "\": " + + this.value; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof MsoHeatEnvironmentResource)) { + return false; + } + if (this == o) { + return true; + } + MsoHeatEnvironmentResource her = (MsoHeatEnvironmentResource) o; + // If the name of the parameter is the same, then they're equal + if (her.getName().equals(this.getName())) { + return true; + } + return false; + } + + @Override + public int hashCode() { + int result = 0; + try { + result = this.name.hashCode(); + } catch (Exception e) { + LOGGER.debug("Exception:", e); + } + return result; + } + + +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoHeatUtils.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoHeatUtils.java new file mode 100644 index 0000000000..e5ece20cb7 --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoHeatUtils.java @@ -0,0 +1,1790 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * Copyright (C) 2017 Huawei Technologies Co., Ltd. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.openstack.utils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.onap.so.adapters.vdu.CloudInfo; +import org.onap.so.adapters.vdu.PluginAction; +import org.onap.so.adapters.vdu.VduArtifact; +import org.onap.so.adapters.vdu.VduArtifact.ArtifactType; +import org.onap.so.adapters.vdu.VduException; +import org.onap.so.adapters.vdu.VduInstance; +import org.onap.so.adapters.vdu.VduModelInfo; +import org.onap.so.adapters.vdu.VduPlugin; +import org.onap.so.adapters.vdu.VduStateType; +import org.onap.so.adapters.vdu.VduStatus; +import org.onap.so.cloud.CloudConfig; +import org.onap.so.cloud.CloudIdentity; +import org.onap.so.cloud.CloudSite; +import org.onap.so.cloud.authentication.AuthenticationMethodFactory; +import org.onap.so.db.catalog.beans.HeatTemplate; +import org.onap.so.db.catalog.beans.HeatTemplateParam; +import org.onap.so.logger.MessageEnum; +import org.onap.so.logger.MsoAlarmLogger; +import org.onap.so.logger.MsoLogger; +import org.onap.so.openstack.beans.HeatCacheEntry; +import org.onap.so.openstack.beans.HeatStatus; +import org.onap.so.openstack.beans.StackInfo; +import org.onap.so.openstack.exceptions.MsoAdapterException; +import org.onap.so.openstack.exceptions.MsoCloudSiteNotFound; +import org.onap.so.openstack.exceptions.MsoException; +import org.onap.so.openstack.exceptions.MsoIOException; +import org.onap.so.openstack.exceptions.MsoOpenstackException; +import org.onap.so.openstack.exceptions.MsoStackAlreadyExists; +import org.onap.so.openstack.exceptions.MsoTenantNotFound; +import org.onap.so.openstack.mappers.StackInfoMapper; +import org.onap.so.utils.CryptoUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.woorea.openstack.base.client.OpenStackConnectException; +import com.woorea.openstack.base.client.OpenStackRequest; +import com.woorea.openstack.base.client.OpenStackResponseException; +import com.woorea.openstack.heat.Heat; +import com.woorea.openstack.heat.model.CreateStackParam; +import com.woorea.openstack.heat.model.Stack; +import com.woorea.openstack.heat.model.Stack.Output; +import com.woorea.openstack.heat.model.Stacks; +import com.woorea.openstack.keystone.Keystone; +import com.woorea.openstack.keystone.model.Access; +import com.woorea.openstack.keystone.model.Authentication; +import com.woorea.openstack.keystone.utils.KeystoneUtils; + +@Primary +@Component +public class MsoHeatUtils extends MsoCommonUtils implements VduPlugin{ + + private static final String TOKEN_AUTH = "TokenAuth"; + + private static final String QUERY_ALL_STACKS = "QueryAllStacks"; + + private static final String DELETE_STACK = "DeleteStack"; + + private static final String HEAT_ERROR = "HeatError"; + + private static final String CREATE_STACK = "CreateStack"; + + // Cache Heat Clients statically. Since there is just one MSO user, there is no + // benefit to re-authentication on every request (or across different flows). The + // token will be used until it expires. + // + // The cache key is "tenantId:cloudId" + private static Map <String, HeatCacheEntry> heatClientCache = new HashMap <> (); + + // Fetch cloud configuration each time (may be cached in CloudConfig class) + @Autowired + protected CloudConfig cloudConfig; + + @Autowired + private Environment environment; + + @Autowired + private AuthenticationMethodFactory authenticationMethodFactory; + + @Autowired + private MsoTenantUtilsFactory tenantUtilsFactory; + + private static final MsoLogger LOGGER = MsoLogger.getMsoLogger (MsoLogger.Catalog.RA, MsoHeatUtils.class); + + // Properties names and variables (with default values) + protected String createPollIntervalProp = "ecomp.mso.adapters.po.pollInterval"; + private String deletePollIntervalProp = "ecomp.mso.adapters.po.pollInterval"; + private String deletePollTimeoutProp = "ecomp.mso.adapters.po.pollTimeout"; + + protected static final String createPollIntervalDefault = "15"; + private static final String deletePollIntervalDefault = "15"; + + private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); + + /** + * keep this old method signature here to maintain backwards compatibility. keep others as well. + * this method does not include environment, files, or heatFiles + */ + public StackInfo createStack (String cloudSiteId, + String tenantId, + String stackName, + String heatTemplate, + Map <String, ?> stackInputs, + boolean pollForCompletion, + int timeoutMinutes) throws MsoException { + // Just call the new method with the environment & files variable set to null + return this.createStack (cloudSiteId, + tenantId, + stackName, + heatTemplate, + stackInputs, + pollForCompletion, + timeoutMinutes, + null, + null, + null, + true); + } + + // This method has environment, but not files or heatFiles + public StackInfo createStack (String cloudSiteId, + String tenantId, + String stackName, + String heatTemplate, + Map <String, ?> stackInputs, + boolean pollForCompletion, + int timeoutMinutes, + String environment) throws MsoException { + // Just call the new method with the files/heatFiles variables set to null + return this.createStack (cloudSiteId, + tenantId, + stackName, + heatTemplate, + stackInputs, + pollForCompletion, + timeoutMinutes, + environment, + null, + null, + true); + } + + // This method has environment and files, but not heatFiles. + public StackInfo createStack (String cloudSiteId, + String tenantId, + String stackName, + String heatTemplate, + Map <String, ?> stackInputs, + boolean pollForCompletion, + int timeoutMinutes, + String environment, + Map <String, Object> files) throws MsoException { + return this.createStack (cloudSiteId, + tenantId, + stackName, + heatTemplate, + stackInputs, + pollForCompletion, + timeoutMinutes, + environment, + files, + null, + true); + } + + // This method has environment, files, heatfiles + public StackInfo createStack (String cloudSiteId, + String tenantId, + String stackName, + String heatTemplate, + Map <String, ?> stackInputs, + boolean pollForCompletion, + int timeoutMinutes, + String environment, + Map <String, Object> files, + Map <String, Object> heatFiles) throws MsoException { + return this.createStack (cloudSiteId, + tenantId, + stackName, + heatTemplate, + stackInputs, + pollForCompletion, + timeoutMinutes, + environment, + files, + heatFiles, + true); + } + + /** + * Create a new Stack in the specified cloud location and tenant. The Heat template + * and parameter map are passed in as arguments, along with the cloud access credentials. + * It is expected that parameters have been validated and contain at minimum the required + * parameters for the given template with no extra (undefined) parameters.. + * + * The Stack name supplied by the caller must be unique in the scope of this tenant. + * However, it should also be globally unique, as it will be the identifier for the + * resource going forward in Inventory. This latter is managed by the higher levels + * invoking this function. + * + * The caller may choose to let this function poll Openstack for completion of the + * stack creation, or may handle polling itself via separate calls to query the status. + * In either case, a StackInfo object will be returned containing the current status. + * When polling is enabled, a status of CREATED is expected. When not polling, a + * status of BUILDING is expected. + * + * An error will be thrown if the requested Stack already exists in the specified + * Tenant and Cloud. + * + * For 1510 - add "environment", "files" (nested templates), and "heatFiles" (get_files) as + * parameters for createStack. If environment is non-null, it will be added to the stack. + * The nested templates and get_file entries both end up being added to the "files" on the + * stack. We must combine them before we add them to the stack if they're both non-null. + * + * @param cloudSiteId The cloud (may be a region) in which to create the stack. + * @param tenantId The Openstack ID of the tenant in which to create the Stack + * @param stackName The name of the stack to create + * @param heatTemplate The Heat template + * @param stackInputs A map of key/value inputs + * @param pollForCompletion Indicator that polling should be handled in Java vs. in the client + * @param environment An optional yaml-format string to specify environmental parameters + * @param files a Map<String, Object> that lists the child template IDs (file is the string, object is an int of + * Template id) + * @param heatFiles a Map<String, Object> that lists the get_file entries (fileName, fileBody) + * @param backout Donot delete stack on create Failure - defaulted to True + * @return A StackInfo object + * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception. + */ + + @SuppressWarnings("unchecked") + public StackInfo createStack (String cloudSiteId, + String tenantId, + String stackName, + String heatTemplate, + Map <String, ?> stackInputs, + boolean pollForCompletion, + int timeoutMinutes, + String environment, + Map <String, Object> files, + Map <String, Object> heatFiles, + boolean backout) throws MsoException { + // Create local variables checking to see if we have an environment, nested, get_files + // Could later add some checks to see if it's valid. + boolean haveEnvtVariable = true; + if (environment == null || "".equalsIgnoreCase (environment.trim ())) { + haveEnvtVariable = false; + LOGGER.debug ("createStack called with no environment variable"); + } else { + LOGGER.debug ("createStack called with an environment variable: " + environment); + } + + boolean haveFiles = true; + if (files == null || files.isEmpty ()) { + haveFiles = false; + LOGGER.debug ("createStack called with no files / child template ids"); + } else { + LOGGER.debug ("createStack called with " + files.size () + " files / child template ids"); + } + + boolean haveHeatFiles = true; + if (heatFiles == null || heatFiles.isEmpty ()) { + haveHeatFiles = false; + LOGGER.debug ("createStack called with no heatFiles"); + } else { + LOGGER.debug ("createStack called with " + heatFiles.size () + " heatFiles"); + } + + // Obtain the cloud site information where we will create the stack + CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow( + () -> new MsoCloudSiteNotFound(cloudSiteId)); + LOGGER.debug("Found: " + cloudSite.toString()); + // Get a Heat client. They are cached between calls (keyed by tenantId:cloudId) + // This could throw MsoTenantNotFound or MsoOpenstackException (both propagated) + Heat heatClient = getHeatClient (cloudSite, tenantId); + if (heatClient != null) { + LOGGER.debug("Found: " + heatClient.toString()); + } + + LOGGER.debug ("Ready to Create Stack (" + heatTemplate + ") with input params: " + stackInputs); + + //force entire stackInput object to generic Map<String, Object> for openstack compatibility + ObjectMapper mapper = new ObjectMapper(); + Map<String, Object> normalized = new HashMap<>(); + try { + normalized = mapper.readValue(mapper.writeValueAsString(stackInputs), new TypeReference<HashMap<String,Object>>() {}); + } catch (IOException e1) { + LOGGER.debug("could not map json", e1); + } + + // Build up the stack to create + // Disable auto-rollback, because error reason is lost. Always rollback in the code. + CreateStackParam stack = new CreateStackParam (); + stack.setStackName (stackName); + stack.setTimeoutMinutes (timeoutMinutes); + stack.setParameters (normalized); + stack.setTemplate (heatTemplate); + stack.setDisableRollback (true); + // TJM New for PO Adapter - add envt variable + if (haveEnvtVariable) { + LOGGER.debug ("Found an environment variable - value: " + environment); + stack.setEnvironment (environment); + } + // Now handle nested templates or get_files - have to combine if we have both + // as they're both treated as "files:" on the stack. + if (haveFiles && haveHeatFiles) { + // Let's do this here - not in the bean + LOGGER.debug ("Found files AND heatFiles - combine and add!"); + Map <String, Object> combinedFiles = new HashMap <> (); + for (Entry<String, Object> entry : files.entrySet()) { + combinedFiles.put(entry.getKey(), entry.getValue()); + } + for (Entry<String, Object> entry : heatFiles.entrySet()) { + combinedFiles.put(entry.getKey(), entry.getValue()); + } + stack.setFiles (combinedFiles); + } else { + // Handle if we only have one or neither: + if (haveFiles) { + LOGGER.debug ("Found files - adding to stack"); + stack.setFiles (files); + } + if (haveHeatFiles) { + LOGGER.debug ("Found heatFiles - adding to stack"); + // the setFiles was modified to handle adding the entries + stack.setFiles (heatFiles); + } + } + + // 1802 - attempt to add better formatted printout of request to openstack + try { + Map<String, Object> inputs = new HashMap<>(); + for (Entry<String, ?> entry : stackInputs.entrySet()) { + if (entry.getValue() != null) { + inputs.put(entry.getKey(), entry.getValue()); + } + } + LOGGER.debug(this.printStackRequest(tenantId, heatFiles, files, environment, inputs, stackName, heatTemplate, timeoutMinutes, backout, cloudSiteId)); + } catch (Exception e) { + // that's okay - this is a nice-to-have + LOGGER.debug("(had an issue printing nicely formatted request to debuglog) " + e.getMessage()); + } + + Stack heatStack = null; + try { + // Execute the actual Openstack command to create the Heat stack + OpenStackRequest <Stack> request = heatClient.getStacks ().create (stack); + // Begin X-Auth-User + // Obtain an MSO token for the tenant + CloudIdentity cloudIdentity = cloudConfig.getIdentityService(cloudSite.getIdentityServiceId()); + // cloudIdentity.getMsoId(), cloudIdentity.getMsoPass() + //req + request.header ("X-Auth-User", cloudIdentity.getMsoId ()); + request.header ("X-Auth-Key", CryptoUtils.decryptCloudConfigPassword(cloudIdentity.getMsoPass ())); + LOGGER.debug ("headers added, about to executeAndRecordOpenstackRequest"); + //LOGGER.debug(this.requestToStringBuilder(stack).toString()); + // END - try to fix X-Auth-User + heatStack = executeAndRecordOpenstackRequest (request); + } catch (OpenStackResponseException e) { + // Since this came on the 'Create Stack' command, nothing was changed + // in the cloud. Return the error as an exception. + if (e.getStatus () == 409) { + // Stack already exists. Return a specific error for this case + MsoStackAlreadyExists me = new MsoStackAlreadyExists (stackName, tenantId, cloudSiteId); + me.addContext (CREATE_STACK); + throw me; + } else { + // Convert the OpenStackResponseException to an MsoOpenstackException + LOGGER.debug("ERROR STATUS = " + e.getStatus() + ",\n" + e.getMessage() + "\n" + e.getLocalizedMessage()); + throw heatExceptionToMsoException (e, CREATE_STACK); + } + } catch (OpenStackConnectException e) { + // Error connecting to Openstack instance. Convert to an MsoException + throw heatExceptionToMsoException (e, CREATE_STACK); + } catch (RuntimeException e) { + // Catch-all + throw runtimeExceptionToMsoException (e, CREATE_STACK); + } + + // Subsequent access by the canonical name "<stack name>/<stack-id>". + // Otherwise, simple query by name returns a 302 redirect. + // NOTE: This is specific to the v1 Orchestration API. + String canonicalName = stackName + "/" + heatStack.getId (); + + // If client has requested a final response, poll for stack completion + if (pollForCompletion) { + // Set a time limit on overall polling. + // Use the resource (template) timeout for Openstack (expressed in minutes) + // and add one poll interval to give Openstack a chance to fail on its own.s + + int createPollInterval = Integer.parseInt(this.environment.getProperty(createPollIntervalProp, createPollIntervalDefault)); + int pollTimeout = (timeoutMinutes * 60) + createPollInterval; + // New 1610 - poll on delete if we rollback - use same values for now + int deletePollInterval = createPollInterval; + int deletePollTimeout = pollTimeout; + boolean createTimedOut = false; + StringBuilder stackErrorStatusReason = new StringBuilder(""); + LOGGER.debug("createPollInterval=" + createPollInterval + ", pollTimeout=" + pollTimeout); + + while (true) { + try { + heatStack = queryHeatStack (heatClient, canonicalName); + LOGGER.debug (heatStack.getStackStatus () + " (" + canonicalName + ")"); + try { + LOGGER.debug("Current stack " + this.getOutputsAsStringBuilder(heatStack).toString()); + } catch (Exception e) { + LOGGER.debug("an error occurred trying to print out the current outputs of the stack", e); + } + + if ("CREATE_IN_PROGRESS".equals (heatStack.getStackStatus ())) { + // Stack creation is still running. + // Sleep and try again unless timeout has been reached + if (pollTimeout <= 0) { + // Note that this should not occur, since there is a timeout specified + // in the Openstack call. + LOGGER.error (MessageEnum.RA_CREATE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName, heatStack.getStackStatus (), "", "", MsoLogger.ErrorCode.AvailabilityError, "Create stack timeout"); + createTimedOut = true; + break; + } + + sleep(createPollInterval * 1000L); + + pollTimeout -= createPollInterval; + LOGGER.debug("pollTimeout remaining: " + pollTimeout); + } else { + //save off the status & reason msg before we attempt delete + stackErrorStatusReason.append("Stack error (" + heatStack.getStackStatus() + "): " + heatStack.getStackStatusReason()); + break; + } + } catch (MsoException me) { + // Cannot query the stack status. Something is wrong. + // Try to roll back the stack + if (!backout) + { + LOGGER.warn(MessageEnum.RA_CREATE_STACK_ERR, "Create Stack errored, stack deletion suppressed", "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Exception in Create Stack, stack deletion suppressed"); + } + else + { + try { + LOGGER.debug("Create Stack error - unable to query for stack status - attempting to delete stack: " + canonicalName + " - This will likely fail and/or we won't be able to query to see if delete worked"); + OpenStackRequest <Void> request = heatClient.getStacks ().deleteByName (canonicalName); + executeAndRecordOpenstackRequest (request); + // this may be a waste of time - if we just got an exception trying to query the stack - we'll just + // get another one, n'est-ce pas? + boolean deleted = false; + while (!deleted) { + try { + heatStack = queryHeatStack(heatClient, canonicalName); + if (heatStack != null) { + LOGGER.debug(heatStack.getStackStatus()); + if ("DELETE_IN_PROGRESS".equals(heatStack.getStackStatus())) { + if (deletePollTimeout <= 0) { + LOGGER.error (MessageEnum.RA_CREATE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName, + heatStack.getStackStatus (), "", "", MsoLogger.ErrorCode.AvailabilityError, + "Rollback: DELETE stack timeout"); + break; + } else { + sleep(deletePollInterval * 1000L); + deletePollTimeout -= deletePollInterval; + } + } else if ("DELETE_COMPLETE".equals(heatStack.getStackStatus())){ + LOGGER.debug("DELETE_COMPLETE for " + canonicalName); + deleted = true; + continue; + } else { + //got a status other than DELETE_IN_PROGRESS or DELETE_COMPLETE - so break and evaluate + break; + } + } else { + // assume if we can't find it - it's deleted + LOGGER.debug("heatStack returned null - assume the stack " + canonicalName + " has been deleted"); + deleted = true; + continue; + } + + } catch (Exception e3) { + // Just log this one. We will report the original exception. + LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "Create Stack: Nested exception rolling back stack: " + e3, "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Create Stack: Nested exception rolling back stack on error on query"); + + } + } + } catch (Exception e2) { + // Just log this one. We will report the original exception. + LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "Create Stack: Nested exception rolling back stack: " + e2, "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Create Stack: Nested exception rolling back stack"); + } + } + + // Propagate the original exception from Stack Query. + me.addContext (CREATE_STACK); + throw me; + } + } + + if (!"CREATE_COMPLETE".equals (heatStack.getStackStatus ())) { + LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "Create Stack error: Polling complete with non-success status: " + + heatStack.getStackStatus () + ", " + heatStack.getStackStatusReason (), "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Create Stack error"); + + // Rollback the stack creation, since it is in an indeterminate state. + if (!backout) + { + LOGGER.warn(MessageEnum.RA_CREATE_STACK_ERR, "Create Stack errored, stack deletion suppressed", "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Create Stack error, stack deletion suppressed"); + } + else + { + try { + LOGGER.debug("Create Stack errored - attempting to DELETE stack: " + canonicalName); + LOGGER.debug("deletePollInterval=" + deletePollInterval + ", deletePollTimeout=" + deletePollTimeout); + OpenStackRequest <Void> request = heatClient.getStacks ().deleteByName (canonicalName); + executeAndRecordOpenstackRequest (request); + boolean deleted = false; + while (!deleted) { + try { + heatStack = queryHeatStack(heatClient, canonicalName); + if (heatStack != null) { + LOGGER.debug(heatStack.getStackStatus() + " (" + canonicalName + ")"); + if ("DELETE_IN_PROGRESS".equals(heatStack.getStackStatus())) { + if (deletePollTimeout <= 0) { + LOGGER.error (MessageEnum.RA_CREATE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName, + heatStack.getStackStatus (), "", "", MsoLogger.ErrorCode.AvailabilityError, + "Rollback: DELETE stack timeout"); + break; + } else { + sleep(deletePollInterval * 1000L); + deletePollTimeout -= deletePollInterval; + LOGGER.debug("deletePollTimeout remaining: " + deletePollTimeout); + } + } else if ("DELETE_COMPLETE".equals(heatStack.getStackStatus())){ + LOGGER.debug("DELETE_COMPLETE for " + canonicalName); + deleted = true; + continue; + } else if ("DELETE_FAILED".equals(heatStack.getStackStatus())) { + // Warn about this (?) - but still throw the original exception + LOGGER.warn(MessageEnum.RA_CREATE_STACK_ERR, "Create Stack errored, stack deletion FAILED", "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Create Stack error, stack deletion FAILED"); + LOGGER.debug("Stack deletion FAILED on a rollback of a create - " + canonicalName + ", status=" + heatStack.getStackStatus() + ", reason=" + heatStack.getStackStatusReason()); + break; + } else { + //got a status other than DELETE_IN_PROGRESS or DELETE_COMPLETE - so break and evaluate + break; + } + } else { + // assume if we can't find it - it's deleted + LOGGER.debug("heatStack returned null - assume the stack " + canonicalName + " has been deleted"); + deleted = true; + continue; + } + + } catch (MsoException me2) { + // We got an exception on the delete - don't throw this exception - throw the original - just log. + LOGGER.debug("Exception thrown trying to delete " + canonicalName + " on a create->rollback: " + me2.getContextMessage(), me2); + LOGGER.warn(MessageEnum.RA_CREATE_STACK_ERR, "Create Stack errored, then stack deletion FAILED - exception thrown", "", "", MsoLogger.ErrorCode.BusinessProcesssError, me2.getContextMessage()); + } + + } // end while !deleted + StringBuilder errorContextMessage; + if (createTimedOut) { + errorContextMessage = new StringBuilder("Stack Creation Timeout"); + } else { + errorContextMessage = stackErrorStatusReason; + } + if (deleted) { + errorContextMessage.append(" - stack successfully deleted"); + } else { + errorContextMessage.append(" - encountered an error trying to delete the stack"); + } +// MsoOpenstackException me = new MsoOpenstackException(0, "", stackErrorStatusReason.toString()); + // me.addContext(CREATE_STACK); + // alarmLogger.sendAlarm(HEAT_ERROR, MsoAlarmLogger.CRITICAL, me.getContextMessage()); + // throw me; + } catch (Exception e2) { + // shouldn't happen - but handle + LOGGER.error (MessageEnum.RA_CREATE_STACK_ERR, "Create Stack: Nested exception rolling back stack: " + e2, "", "", MsoLogger.ErrorCode.BusinessProcesssError, "Exception in Create Stack: rolling back stack"); + } + } + MsoOpenstackException me = new MsoOpenstackException(0, "", stackErrorStatusReason.toString()); + me.addContext(CREATE_STACK); + alarmLogger.sendAlarm(HEAT_ERROR, MsoAlarmLogger.CRITICAL, me.getContextMessage()); + throw me; + } + + } else { + // Get initial status, since it will have been null after the create. + heatStack = queryHeatStack (heatClient, canonicalName); + LOGGER.debug (heatStack.getStackStatus ()); + } + + return new StackInfoMapper(heatStack).map(); + } + + /** + * Query for a single stack (by Name) in a tenant. This call will always return a + * StackInfo object. If the stack does not exist, an "empty" StackInfo will be + * returned - containing only the stack name and a status of NOTFOUND. + * + * @param tenantId The Openstack ID of the tenant in which to query + * @param cloudSiteId The cloud identifier (may be a region) in which to query + * @param stackName The name of the stack to query (may be simple or canonical) + * @return A StackInfo object + * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception. + */ + public StackInfo queryStack (String cloudSiteId, String tenantId, String stackName) throws MsoException { + LOGGER.debug ("Query HEAT stack: " + stackName + " in tenant " + tenantId); + + // Obtain the cloud site information where we will create the stack + CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow( + () -> new MsoCloudSiteNotFound(cloudSiteId)); + LOGGER.debug("Found: " + cloudSite.toString()); + + // Get a Heat client. They are cached between calls (keyed by tenantId:cloudId) + Heat heatClient = null; + try { + heatClient = getHeatClient (cloudSite, tenantId); + if (heatClient != null) { + LOGGER.debug("Found: " + heatClient.toString()); + } + } catch (MsoTenantNotFound e) { + // Tenant doesn't exist, so stack doesn't either + LOGGER.debug ("Tenant with id " + tenantId + "not found.", e); + return new StackInfo (stackName, HeatStatus.NOTFOUND); + } catch (MsoException me) { + // Got an Openstack error. Propagate it + LOGGER.error (MessageEnum.RA_CONNECTION_EXCEPTION, "OpenStack", "Openstack Exception on Token request: " + me, "Openstack", "", MsoLogger.ErrorCode.AvailabilityError, "Connection Exception"); + me.addContext ("QueryStack"); + throw me; + } + + // Query the Stack. + // An MsoException will propagate transparently to the caller. + Stack heatStack = queryHeatStack (heatClient, stackName); + + if (heatStack == null) { + // Stack does not exist. Return a StackInfo with status NOTFOUND + return new StackInfo (stackName, HeatStatus.NOTFOUND); + } + + return new StackInfoMapper(heatStack).map(); + } + + /** + * Delete a stack (by Name/ID) in a tenant. If the stack is not found, it will be + * considered a successful deletion. The return value is a StackInfo object which + * contains the current stack status. + * + * The client may choose to let the adapter poll Openstack for completion of the + * stack deletion, or may handle polling itself via separate query calls. In either + * case, a StackInfo object will be returned. When polling is enabled, a final + * status of NOTFOUND is expected. When not polling, a status of DELETING is expected. + * + * There is no rollback from a successful stack deletion. A deletion failure will + * also result in an undefined stack state - the components may or may not have been + * all or partially deleted, so the resulting stack must be considered invalid. + * + * @param tenantId The Openstack ID of the tenant in which to perform the delete + * @param cloudSiteId The cloud identifier (may be a region) from which to delete the stack. + * @param stackName The name/id of the stack to delete. May be simple or canonical + * @param pollForCompletion Indicator that polling should be handled in Java vs. in the client + * @return A StackInfo object + * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception. + * @throws MsoCloudSiteNotFound + */ + public StackInfo deleteStack (String tenantId, + String cloudSiteId, + String stackName, + boolean pollForCompletion) throws MsoException { + // Obtain the cloud site information where we will create the stack + CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow( + () -> new MsoCloudSiteNotFound(cloudSiteId)); + LOGGER.debug("Found: " + cloudSite.toString()); + + // Get a Heat client. They are cached between calls (keyed by tenantId:cloudId) + Heat heatClient = null; + try { + heatClient = getHeatClient (cloudSite, tenantId); + if (heatClient != null) { + LOGGER.debug("Found: " + heatClient.toString()); + } + } catch (MsoTenantNotFound e) { + // Tenant doesn't exist, so stack doesn't either + LOGGER.debug ("Tenant with id " + tenantId + "not found.", e); + return new StackInfo (stackName, HeatStatus.NOTFOUND); + } catch (MsoException me) { + // Got an Openstack error. Propagate it + LOGGER.error (MessageEnum.RA_CONNECTION_EXCEPTION, "Openstack", "Openstack Exception on Token request: " + me, "Openstack", "", MsoLogger.ErrorCode.AvailabilityError, "Connection Exception"); + me.addContext (DELETE_STACK); + throw me; + } + + // OK if stack not found, perform a query first + Stack heatStack = queryHeatStack (heatClient, stackName); + if (heatStack == null || "DELETE_COMPLETE".equals (heatStack.getStackStatus ())) { + // Not found. Return a StackInfo with status NOTFOUND + return new StackInfo (stackName, HeatStatus.NOTFOUND); + } + + // Delete the stack. + + // Use canonical name "<stack name>/<stack-id>" to delete. + // Otherwise, deletion by name returns a 302 redirect. + // NOTE: This is specific to the v1 Orchestration API. + String canonicalName = heatStack.getStackName () + "/" + heatStack.getId (); + + try { + OpenStackRequest <Void> request = null; + if(null != heatClient) { + request = heatClient.getStacks ().deleteByName (canonicalName); + } + else { + LOGGER.debug ("Heat Client is NULL" ); + } + + executeAndRecordOpenstackRequest (request); + } catch (OpenStackResponseException e) { + if (e.getStatus () == 404) { + // Not found. We are OK with this. Return a StackInfo with status NOTFOUND + return new StackInfo (stackName, HeatStatus.NOTFOUND); + } else { + // Convert the OpenStackResponseException to an MsoOpenstackException + throw heatExceptionToMsoException (e, DELETE_STACK); + } + } catch (OpenStackConnectException e) { + // Error connecting to Openstack instance. Convert to an MsoException + throw heatExceptionToMsoException (e, DELETE_STACK); + } catch (RuntimeException e) { + // Catch-all + throw runtimeExceptionToMsoException (e, DELETE_STACK); + } + + // Requery the stack for current status. + // It will probably still exist with "DELETE_IN_PROGRESS" status. + heatStack = queryHeatStack (heatClient, canonicalName); + + if (pollForCompletion) { + // Set a timeout on polling + + int pollInterval = Integer.parseInt(this.environment.getProperty(deletePollIntervalProp, "" + deletePollIntervalDefault)); + int pollTimeout = Integer.parseInt(this.environment.getProperty(deletePollTimeoutProp, "" + deletePollIntervalDefault)); + + // When querying by canonical name, Openstack returns DELETE_COMPLETE status + // instead of "404" (which would result from query by stack name). + while (heatStack != null && !"DELETE_COMPLETE".equals (heatStack.getStackStatus ())) { + LOGGER.debug ("Stack status: " + heatStack.getStackStatus ()); + + if ("DELETE_FAILED".equals (heatStack.getStackStatus ())) { + // Throw a 'special case' of MsoOpenstackException to report the Heat status + String error = "Stack delete error (" + heatStack.getStackStatus () + + "): " + + heatStack.getStackStatusReason (); + MsoOpenstackException me = new MsoOpenstackException (0, "", error); + me.addContext (DELETE_STACK); + + // Alarm this condition, stack deletion failed + alarmLogger.sendAlarm (HEAT_ERROR, MsoAlarmLogger.CRITICAL, me.getContextMessage ()); + + throw me; + } + + if (pollTimeout <= 0) { + LOGGER.error (MessageEnum.RA_DELETE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName, heatStack.getStackStatus (), "", "", MsoLogger.ErrorCode.AvailabilityError, "Delete Stack Timeout"); + + // Throw a 'special case' of MsoOpenstackException to report the Heat status + MsoOpenstackException me = new MsoOpenstackException (0, "", "Stack Deletion Timeout"); + me.addContext (DELETE_STACK); + + // Alarm this condition, stack deletion failed + alarmLogger.sendAlarm (HEAT_ERROR, MsoAlarmLogger.CRITICAL, me.getContextMessage ()); + + throw me; + } + + sleep(pollInterval * 1000L); + + pollTimeout -= pollInterval; + LOGGER.debug("pollTimeout remaining: " + pollTimeout); + + heatStack = queryHeatStack (heatClient, canonicalName); + } + + // The stack is gone when this point is reached + return new StackInfo (stackName, HeatStatus.NOTFOUND); + } + + // Return the current status (if not polling, the delete may still be in progress) + StackInfo stackInfo = new StackInfoMapper(heatStack).map(); + stackInfo.setName (stackName); + + return stackInfo; + } + + /** + * Query for all stacks in a tenant site. This call will return a List of StackInfo + * objects, one for each deployed stack. + * + * Note that this is limited to a single site. To ensure that a tenant is truly + * empty would require looping across all tenant endpoints. + * + * @param tenantId The Openstack ID of the tenant to query + * @param cloudSiteId The cloud identifier (may be a region) in which to query. + * @return A List of StackInfo objects + * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception. + * @throws MsoCloudSiteNotFound + */ + public List <StackInfo> queryAllStacks (String tenantId, String cloudSiteId) throws MsoException { + // Obtain the cloud site information where we will create the stack + CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow( + () -> new MsoCloudSiteNotFound(cloudSiteId)); + // Get a Heat client. They are cached between calls (keyed by tenantId:cloudId) + Heat heatClient = getHeatClient (cloudSite, tenantId); + + try { + OpenStackRequest <Stacks> request = heatClient.getStacks ().list (); + Stacks stacks = executeAndRecordOpenstackRequest (request); + + List <StackInfo> stackList = new ArrayList <> (); + + // Not sure if returns an empty list or null if no stacks exist + if (stacks != null) { + for (Stack stack : stacks) { + stackList.add (new StackInfoMapper(stack).map()); + } + } + + return stackList; + } catch (OpenStackResponseException e) { + if (e.getStatus () == 404) { + // Not sure if this can happen, but return an empty list + LOGGER.debug ("queryAllStacks - stack not found: "); + return new ArrayList <> (); + } else { + // Convert the OpenStackResponseException to an MsoOpenstackException + throw heatExceptionToMsoException (e, QUERY_ALL_STACKS); + } + } catch (OpenStackConnectException e) { + // Error connecting to Openstack instance. Convert to an MsoException + throw heatExceptionToMsoException (e, QUERY_ALL_STACKS); + } catch (RuntimeException e) { + // Catch-all + throw runtimeExceptionToMsoException (e, QUERY_ALL_STACKS); + } + } + + /** + * Validate parameters to be passed to Heat template. This method performs + * three functions: + * 1. Apply default values to parameters which have them defined + * 2. Report any required parameters that are missing. This will generate an + * exception in the caller, since stack create/update operations would fail. + * 3. Report and remove any extraneous parameters. This will allow clients to + * pass supersets of parameters and not get errors. + * + * These functions depend on the HeatTemplate definition from the MSO Catalog DB, + * along with the input parameter Map. The output is an updated parameter map. + * If the parameters are invalid for the template, an IllegalArgumentException + * is thrown. + */ + public Map <String, Object> validateStackParams (Map <String, Object> inputParams, + HeatTemplate heatTemplate) { + // Check that required parameters have been supplied for this template type + StringBuilder missingParams = null; + List <String> paramList = new ArrayList <> (); + + // TODO: Enhance DB to support defaults for Heat Template parameters + + for (HeatTemplateParam parm : heatTemplate.getParameters ()) { + if (parm.isRequired () && !inputParams.containsKey (parm.getParamName ())) { + if (missingParams == null) { + missingParams = new StringBuilder(parm.getParamName()); + } else { + missingParams.append("," + parm.getParamName()); + } + } + paramList.add (parm.getParamName ()); + } + if (missingParams != null) { + // Problem - missing one or more required parameters + String error = "Missing Required inputs for HEAT Template: " + missingParams; + LOGGER.error (MessageEnum.RA_MISSING_PARAM, missingParams + " for HEAT Template", "", "", MsoLogger.ErrorCode.SchemaError, "Missing Required inputs for HEAT Template: " + missingParams); + throw new IllegalArgumentException (error); + } + + // Remove any extraneous parameters (don't throw an error) + Map <String, Object> updatedParams = new HashMap <> (); + List <String> extraParams = new ArrayList <> (); + + for (Entry<String, Object> entry : inputParams.entrySet()) { + if (!paramList.contains(entry.getKey())) { + // This is not a valid parameter for this template + extraParams.add(entry.getKey()); + } else { + updatedParams.put(entry.getKey(), entry.getValue()); + } + } + + if (!extraParams.isEmpty ()) { + LOGGER.warn (MessageEnum.RA_GENERAL_WARNING, "Heat Stack (" + heatTemplate.getTemplateName () + + ") extra input params received: " + + extraParams, "", "", MsoLogger.ErrorCode.DataError, "Heat Stack (" + heatTemplate.getTemplateName () + ") extra input params received: "+ extraParams); + } + + return updatedParams; + } + + // --------------------------------------------------------------- + // PRIVATE FUNCTIONS FOR USE WITHIN THIS CLASS + + /** + * Get a Heat client for the Openstack Identity service. + * This requires a 'member'-level userId + password, which will be retrieved from + * properties based on the specified cloud Id. The tenant in which to operate + * must also be provided. + * <p> + * On successful authentication, the Heat object will be cached for the + * tenantID + cloudId so that it can be reused without reauthenticating with + * Openstack every time. + * + * @return an authenticated Heat object + */ + public Heat getHeatClient (CloudSite cloudSite, String tenantId) throws MsoException { + String cloudId = cloudConfig.getCloudSiteId(cloudSite); + + // Check first in the cache of previously authorized clients + String cacheKey = cloudId + ":" + tenantId; + if (heatClientCache.containsKey (cacheKey)) { + if (!heatClientCache.get (cacheKey).isExpired ()) { + LOGGER.debug ("Using Cached HEAT Client for " + cacheKey); + return heatClientCache.get (cacheKey).getHeatClient (); + } else { + // Token is expired. Remove it from cache. + heatClientCache.remove (cacheKey); + LOGGER.debug ("Expired Cached HEAT Client for " + cacheKey); + } + } + + // Obtain an MSO token for the tenant + CloudIdentity cloudIdentity = cloudConfig.getIdentityService(cloudSite.getIdentityServiceId()); + LOGGER.debug("Found: " + cloudIdentity.toString()); + MsoTenantUtils tenantUtils = tenantUtilsFactory.getTenantUtilsByServerType(cloudIdentity.getIdentityServerType()); + String keystoneUrl = tenantUtils.getKeystoneUrl(cloudId, cloudIdentity); + LOGGER.debug("keystoneUrl=" + keystoneUrl); + Keystone keystoneTenantClient = new Keystone (keystoneUrl); + Access access = null; + try { + Authentication credentials = authenticationMethodFactory.getAuthenticationFor(cloudIdentity); + + OpenStackRequest <Access> request = keystoneTenantClient.tokens () + .authenticate (credentials).withTenantId (tenantId); + + access = executeAndRecordOpenstackRequest (request); + } catch (OpenStackResponseException e) { + if (e.getStatus () == 401) { + // Authentication error. + String error = "Authentication Failure: tenant=" + tenantId + ",cloud=" + cloudIdentity.getId (); + alarmLogger.sendAlarm ("MsoAuthenticationError", MsoAlarmLogger.CRITICAL, error); + throw new MsoAdapterException (error); + } else { + throw keystoneErrorToMsoException (e, TOKEN_AUTH); + } + } catch (OpenStackConnectException e) { + // Connection to Openstack failed + MsoIOException me = new MsoIOException (e.getMessage (), e); + me.addContext (TOKEN_AUTH); + throw me; + } catch (RuntimeException e) { + // Catch-all + throw runtimeExceptionToMsoException (e, TOKEN_AUTH); + } + + // For DCP/LCP, the region should be the cloudId. + String region = cloudSite.getRegionId (); + String heatUrl = null; + try { + // Isolate trying to printout the region IDs + try { + LOGGER.debug("access=" + access.toString()); + for (Access.Service service : access.getServiceCatalog()) { + List<Access.Service.Endpoint> endpoints = service.getEndpoints(); + for (Access.Service.Endpoint endpoint : endpoints) { + LOGGER.debug("AIC returned region=" + endpoint.getRegion()); + } + } + } catch (Exception e) { + LOGGER.debug("Encountered an error trying to printout Access object returned from AIC. " + e.getMessage()); + } + heatUrl = KeystoneUtils.findEndpointURL (access.getServiceCatalog (), "orchestration", region, "public"); + LOGGER.debug("heatUrl=" + heatUrl + ", region=" + region); + } catch (RuntimeException e) { + // This comes back for not found (probably an incorrect region ID) + String error = "AIC did not match an orchestration service for: region=" + region + ",cloud=" + cloudIdentity.getIdentityUrl(); + alarmLogger.sendAlarm ("MsoConfigurationError", MsoAlarmLogger.CRITICAL, error); + throw new MsoAdapterException (error, e); + } + + Heat heatClient = new Heat (heatUrl); + heatClient.token (access.getToken ().getId ()); + + heatClientCache.put (cacheKey, + new HeatCacheEntry (heatUrl, + access.getToken ().getId (), + access.getToken ().getExpires ())); + LOGGER.debug ("Caching HEAT Client for " + cacheKey); + + return heatClient; + } + + /** + * Forcibly expire a HEAT client from the cache. This call is for use by + * the KeystoneClient in case where a tenant is deleted. In that case, + * all cached credentials must be purged so that fresh authentication is + * done if a similarly named tenant is re-created. + * <p> + * Note: This is probably only applicable to dev/test environments where + * the same Tenant Name is repeatedly used for creation/deletion. + * <p> + * + */ + public void expireHeatClient (String tenantId, String cloudId) { + String cacheKey = cloudId + ":" + tenantId; + if (heatClientCache.containsKey (cacheKey)) { + heatClientCache.remove (cacheKey); + LOGGER.debug ("Deleted Cached HEAT Client for " + cacheKey); + } + } + + /* + * Query for a Heat Stack. This function is needed in several places, so + * a common method is useful. This method takes an authenticated Heat Client + * (which internally identifies the cloud & tenant to search), and returns + * a Stack object if found, Null if not found, or an MsoOpenstackException + * if the Openstack API call fails. + * + * The stack name may be a simple name or a canonical name ("{name}/{id}"). + * When simple name is used, Openstack always returns a 302 redirect which + * results in a 2nd request (to the canonical name). Note that query by + * canonical name for a deleted stack returns a Stack object with status + * "DELETE_COMPLETE" while query by simple name for a deleted stack returns + * HTTP 404. + * + * @param heatClient an authenticated Heat client + * + * @param stackName the stack name to query + * + * @return a Stack object that describes the current stack or null if the + * requested stack doesn't exist. + * + * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception + */ + protected Stack queryHeatStack (Heat heatClient, String stackName) throws MsoException { + if (stackName == null) { + return null; + } + try { + OpenStackRequest <Stack> request = heatClient.getStacks ().byName (stackName); + return executeAndRecordOpenstackRequest (request); + } catch (OpenStackResponseException e) { + if (e.getStatus () == 404) { + LOGGER.debug ("queryHeatStack - stack not found: " + stackName); + return null; + } else { + // Convert the OpenStackResponseException to an MsoOpenstackException + throw heatExceptionToMsoException (e, "QueryStack"); + } + } catch (OpenStackConnectException e) { + // Connection to Openstack failed + throw heatExceptionToMsoException (e, "QueryAllStack"); + } + } + + + public Map<String, Object> queryStackForOutputs(String cloudSiteId, + String tenantId, String stackName) throws MsoException { + LOGGER.debug("MsoHeatUtils.queryStackForOutputs)"); + StackInfo heatStack = this.queryStack(cloudSiteId, tenantId, stackName); + if (heatStack == null || heatStack.getStatus() == HeatStatus.NOTFOUND) { + return null; + } + return heatStack.getOutputs(); + } + + public void copyStringOutputsToInputs(Map<String, String> inputs, + Map<String, Object> otherStackOutputs, boolean overWrite) { + if (inputs == null || otherStackOutputs == null) + return; + for (String key : otherStackOutputs.keySet()) { + if (!inputs.containsKey(key)) { + Object obj = otherStackOutputs.get(key); + if (obj instanceof String) { + inputs.put(key, (String) otherStackOutputs.get(key)); + } else if (obj instanceof JsonNode ){ + // This is a bit of mess - but I think it's the least impacting + // let's convert it BACK to a string - then it will get converted back later + try { + String str = this.convertNode((JsonNode) obj); + inputs.put(key, str); + } catch (Exception e) { + LOGGER.debug("DANGER WILL ROBINSON: unable to convert value for JsonNode "+ key, e); + //effect here is this value will not have been copied to the inputs - and therefore will error out downstream + } + } else if (obj instanceof java.util.LinkedHashMap) { + LOGGER.debug("LinkedHashMap - this is showing up as a LinkedHashMap instead of JsonNode"); + try { + String str = JSON_MAPPER.writeValueAsString(obj); + inputs.put(key, str); + } catch (Exception e) { + LOGGER.debug("DANGER WILL ROBINSON: unable to convert value for LinkedHashMap "+ key, e); + } + } else if (obj instanceof Integer) { + try { + String str = "" + obj; + inputs.put(key, str); + } catch (Exception e) { + LOGGER.debug("DANGER WILL ROBINSON: unable to convert value for Integer "+ key, e); + } + } else { + try { + String str = obj.toString(); + inputs.put(key, str); + } catch (Exception e) { + LOGGER.debug("DANGER WILL ROBINSON: unable to convert value for Other "+ key +" (" + e.getMessage() + ")", e); + //effect here is this value will not have been copied to the inputs - and therefore will error out downstream + } + } + } + } + return; + } + public StringBuilder requestToStringBuilder(CreateStackParam stack) { + StringBuilder sb = new StringBuilder(); + sb.append("Stack:\n"); + sb.append("\tStackName: " + stack.getStackName()); + sb.append("\tTemplateUrl: " + stack.getTemplateUrl()); + sb.append("\tTemplate: " + stack.getTemplate()); + sb.append("\tEnvironment: " + stack.getEnvironment()); + sb.append("\tTimeout: " + stack.getTimeoutMinutes()); + sb.append("\tParameters:\n"); + Map<String, Object> params = stack.getParameters(); + if (params == null || params.size() < 1) { + sb.append("\nNONE"); + } else { + for (String key : params.keySet()) { + if (params.get(key) instanceof String) { + sb.append("\n").append(key).append("=").append((String) params.get(key)); + } else if (params.get(key) instanceof JsonNode) { + String jsonStringOut = this.convertNode((JsonNode)params.get(key)); + sb.append("\n").append(key).append("=").append(jsonStringOut); + } else if (params.get(key) instanceof Integer) { + String integerOut = "" + params.get(key); + sb.append("\n").append(key).append("=").append(integerOut); + + } else { + try { + String str = params.get(key).toString(); + sb.append("\n").append(key).append("=").append(str); + } catch (Exception e) { + LOGGER.debug("Exception :",e); + } + } + } + } + return sb; + } + + private String convertNode(final JsonNode node) { + try { + final Object obj = JSON_MAPPER.treeToValue(node, Object.class); + final String json = JSON_MAPPER.writeValueAsString(obj); + return json; + } catch (Exception e) { + LOGGER.debug("Error converting json to string " + e.getMessage(), e); + } + return "[Error converting json to string]"; + } + + + private StringBuilder getOutputsAsStringBuilder(Stack heatStack) { + // This should only be used as a utility to print out the stack outputs + // to the log + StringBuilder sb = new StringBuilder(""); + if (heatStack == null) { + sb.append("(heatStack is null)"); + return sb; + } + List<Output> outputList = heatStack.getOutputs(); + if (outputList == null || outputList.isEmpty()) { + sb.append("(outputs is empty)"); + return sb; + } + Map<String, Object> outputs = new HashMap<>(); + for (Output outputItem : outputList) { + outputs.put(outputItem.getOutputKey(), outputItem.getOutputValue()); + } + int counter = 0; + sb.append("OUTPUTS:\n"); + for (String key : outputs.keySet()) { + sb.append("outputs[").append(counter++).append("]: ").append(key).append("="); + Object obj = outputs.get(key); + if (obj instanceof String) { + sb.append((String) obj).append(" (a string)"); + } else if (obj instanceof JsonNode) { + sb.append(this.convertNode((JsonNode) obj)).append(" (a JsonNode)"); + } else if (obj instanceof java.util.LinkedHashMap) { + try { + String str = JSON_MAPPER.writeValueAsString(obj); + sb.append(str).append(" (a java.util.LinkedHashMap)"); + } catch (Exception e) { + LOGGER.debug("Exception :",e); + sb.append("(a LinkedHashMap value that would not convert nicely)"); + } + } else if (obj instanceof Integer) { + String str = ""; + try { + str = obj.toString() + " (an Integer)\n"; + } catch (Exception e) { + LOGGER.debug("Exception :",e); + str = "(an Integer unable to call .toString() on)"; + } + sb.append(str); + } else if (obj instanceof ArrayList) { + String str = ""; + try { + str = obj.toString() + " (an ArrayList)"; + } catch (Exception e) { + LOGGER.debug("Exception :",e); + str = "(an ArrayList unable to call .toString() on?)"; + } + sb.append(str); + } else if (obj instanceof Boolean) { + String str = ""; + try { + str = obj.toString() + " (a Boolean)"; + } catch (Exception e) { + LOGGER.debug("Exception :",e); + str = "(an Boolean unable to call .toString() on?)"; + } + sb.append(str); + } + else { + String str = ""; + try { + str = obj.toString() + " (unknown Object type)"; + } catch (Exception e) { + LOGGER.debug("Exception :",e); + str = "(a value unable to call .toString() on?)"; + } + sb.append(str); + } + sb.append("\n"); + } + sb.append("[END]"); + return sb; + } + + + public void copyBaseOutputsToInputs(Map<String, Object> inputs, + Map<String, Object> otherStackOutputs, List<String> paramNames, Map<String, String> aliases) { + if (inputs == null || otherStackOutputs == null) + return; + for (String key : otherStackOutputs.keySet()) { + if (paramNames != null) { + if (!paramNames.contains(key) && !aliases.containsKey(key)) { + LOGGER.debug("\tParameter " + key + " is NOT defined to be in the template - do not copy to inputs"); + continue; + } + if (aliases.containsKey(key)) { + LOGGER.debug("Found an alias! Will move " + key + " to " + aliases.get(key)); + Object obj = otherStackOutputs.get(key); + key = aliases.get(key); + otherStackOutputs.put(key, obj); + } + } + if (!inputs.containsKey(key)) { + Object obj = otherStackOutputs.get(key); + LOGGER.debug("\t**Adding " + key + " to inputs (.toString()=" + obj.toString()); + if (obj instanceof String) { + LOGGER.debug("\t\t**A String"); + inputs.put(key, obj); + } else if (obj instanceof Integer) { + LOGGER.debug("\t\t**An Integer"); + inputs.put(key, obj); + } else if (obj instanceof JsonNode) { + LOGGER.debug("\t\t**A JsonNode"); + inputs.put(key, obj); + } else if (obj instanceof Boolean) { + LOGGER.debug("\t\t**A Boolean"); + inputs.put(key, obj); + } else if (obj instanceof java.util.LinkedHashMap) { + LOGGER.debug("\t\t**A java.util.LinkedHashMap **"); + inputs.put(key, obj); + } else if (obj instanceof java.util.ArrayList) { + LOGGER.debug("\t\t**An ArrayList"); + inputs.put(key, obj); + } else { + LOGGER.debug("\t\t**UNKNOWN OBJECT TYPE"); + inputs.put(key, obj); + } + } else { + LOGGER.debug("key=" + key + " is already in the inputs - will not overwrite"); + } + } + return; + } + + public List<String> convertCdlToArrayList(String cdl) { + String cdl2 = cdl.trim(); + String cdl3; + if (cdl2.startsWith("[") && cdl2.endsWith("]")) { + cdl3 = cdl2.substring(1, cdl2.lastIndexOf("]")); + } else { + cdl3 = cdl2; + } + return new ArrayList<>(Arrays.asList(cdl3.split(","))); + } + + /** + * New with 1707 - this method will convert all the String *values* of the inputs + * to their "actual" object type (based on the param type: in the db - which comes from the template): + * (heat variable type) -> java Object type + * string -> String + * number -> Integer + * json -> JsonNode XXX Removed with MSO-1475 / 1802 + * comma_delimited_list -> ArrayList + * boolean -> Boolean + * if any of the conversions should fail, we will default to adding it to the inputs + * as a string - see if Openstack can handle it. + * Also, will remove any params that are extra. + * Any aliases will be converted to their appropriate name (anyone use this feature?) + * @param inputs - the Map<String, String> of the inputs received on the request + * @param template the HeatTemplate object - this is so we can also verify if the param is valid for this template + * @return HashMap<String, Object> of the inputs, cleaned and converted + */ + public Map<String, Object> convertInputMap(Map<String, String> inputs, HeatTemplate template) { + HashMap<String, Object> newInputs = new HashMap<>(); + HashMap<String, HeatTemplateParam> params = new HashMap<>(); + HashMap<String, HeatTemplateParam> paramAliases = new HashMap<>(); + + if (inputs == null) { + LOGGER.debug("convertInputMap - inputs is null - nothing to do here"); + return new HashMap<>(); + } + + LOGGER.debug("convertInputMap in MsoHeatUtils called, with " + inputs.size() + " inputs, and template " + template.getArtifactUuid()); + try { + LOGGER.debug(template.toString()); + Set<HeatTemplateParam> paramSet = template.getParameters(); + LOGGER.debug("paramSet has " + paramSet.size() + " entries"); + } catch (Exception e) { + LOGGER.debug("Exception occurred in convertInputMap:" + e.getMessage(), e); + } + + for (HeatTemplateParam htp : template.getParameters()) { + LOGGER.debug("Adding " + htp.getParamName()); + params.put(htp.getParamName(), htp); + if (htp.getParamAlias() != null && !"".equals(htp.getParamAlias())) { + LOGGER.debug("\tFound ALIAS " + htp.getParamName() + "->" + htp.getParamAlias()); + paramAliases.put(htp.getParamAlias(), htp); + } + } + LOGGER.debug("Now iterate through the inputs..."); + for (String key : inputs.keySet()) { + LOGGER.debug("key=" + key); + boolean alias = false; + String realName = null; + if (!params.containsKey(key)) { + LOGGER.debug(key + " is not a parameter in the template! - check for an alias"); + // add check here for an alias + if (!paramAliases.containsKey(key)) { + LOGGER.debug("The parameter " + key + " is in the inputs, but it's not a parameter for this template - omit"); + continue; + } else { + alias = true; + realName = paramAliases.get(key).getParamName(); + LOGGER.debug("FOUND AN ALIAS! Will use " + realName + " in lieu of give key/alias " + key); + } + } + String type = params.get(key).getParamType(); + if (type == null || "".equals(type)) { + LOGGER.debug("**PARAM_TYPE is null/empty for " + key + ", will default to string"); + type = "string"; + } + LOGGER.debug("Parameter: " + key + " is of type " + type); + if ("string".equalsIgnoreCase(type)) { + // Easiest! + String str = inputs.get(key); + if (alias) + newInputs.put(realName, str); + else + newInputs.put(key, str); + } else if ("number".equalsIgnoreCase(type)) { + String integerString = inputs.get(key); + Integer anInteger = null; + try { + anInteger = Integer.parseInt(integerString); + } catch (Exception e) { + LOGGER.debug("Unable to convert " + integerString + " to an integer!!", e); + anInteger = null; + } + if (anInteger != null) { + if (alias) + newInputs.put(realName, anInteger); + else + newInputs.put(key, anInteger); + } + else { + if (alias) + newInputs.put(realName, integerString); + else + newInputs.put(key, integerString); + } + } else if ("json".equalsIgnoreCase(type)) { + // MSO-1475 - Leave this as a string now + String jsonString = inputs.get(key); + LOGGER.debug("Skipping conversion to jsonNode..."); + if (alias) + newInputs.put(realName, jsonString); + else + newInputs.put(key, jsonString); + //} + } else if ("comma_delimited_list".equalsIgnoreCase(type)) { + String commaSeparated = inputs.get(key); + try { + List<String> anArrayList = this.convertCdlToArrayList(commaSeparated); + if (alias) + newInputs.put(realName, anArrayList); + else + newInputs.put(key, anArrayList); + } catch (Exception e) { + LOGGER.debug("Unable to convert " + commaSeparated + " to an ArrayList!!", e); + if (alias) + newInputs.put(realName, commaSeparated); + else + newInputs.put(key, commaSeparated); + } + } else if ("boolean".equalsIgnoreCase(type)) { + String booleanString = inputs.get(key); + Boolean aBool = Boolean.valueOf(booleanString); + if (alias) + newInputs.put(realName, aBool); + else + newInputs.put(key, aBool); + } else { + // it's null or something undefined - just add it back as a String + String str = inputs.get(key); + if (alias) + newInputs.put(realName, str); + else + newInputs.put(key, str); + } + } + return newInputs; + } + + /* + * This helpful method added for Valet + */ + public String getCloudSiteKeystoneUrl(String cloudSiteId) throws MsoCloudSiteNotFound { + String keystone_url = null; + try { + CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow(() -> new MsoCloudSiteNotFound(cloudSiteId)); + CloudIdentity cloudIdentity = cloudConfig.getIdentityService(cloudSite.getIdentityServiceId()); + keystone_url = cloudIdentity.getIdentityUrl(); + } catch (Exception e) { + throw new MsoCloudSiteNotFound(cloudSiteId); + } + if (keystone_url == null || keystone_url.isEmpty()) { + throw new MsoCloudSiteNotFound(cloudSiteId); + } + return keystone_url; + } + + /* + * Create a string suitable for being dumped to a debug log that creates a + * pseudo-JSON request dumping what's being sent to Openstack API in the create or update request + */ + + private String printStackRequest(String tenantId, + Map<String, Object> heatFiles, + Map<String, Object> nestedTemplates, + String environment, + Map<String, Object> inputs, + String vfModuleName, + String template, + int timeoutMinutes, + boolean backout, + String cloudSiteId) { + StringBuilder sb = new StringBuilder(); + sb.append("CREATE STACK REQUEST (formatted for readability)\n"); + sb.append("tenant=" + tenantId + ", cloud=" + cloudSiteId); + sb.append("{\n"); + sb.append(" \"stack_name\": \"" + vfModuleName + "\",\n"); + sb.append(" \"disable_rollback\": " + backout + ",\n"); + sb.append(" \"timeout_mins\": " + timeoutMinutes + ",\n"); + sb.append(" \"template\": {\n"); + sb.append(template); + sb.append(" },\n"); + sb.append(" \"environment\": {\n"); + if (environment == null) + sb.append("<none>"); + else + sb.append(environment); + sb.append(" },\n"); + sb.append(" \"files\": {\n"); + int filesCounter = 0; + if (heatFiles != null) { + for (String key : heatFiles.keySet()) { + filesCounter++; + if (filesCounter > 1) { + sb.append(",\n"); + } + sb.append(" \"" + key + "\": {\n"); + sb.append(heatFiles.get(key).toString() + "\n }"); + } + } + if (nestedTemplates != null) { + for (String key : nestedTemplates.keySet()) { + filesCounter++; + if (filesCounter > 1) { + sb.append(",\n"); + } + sb.append(" \"" + key + "\": {\n"); + sb.append(nestedTemplates.get(key).toString() + "\n }"); + } + } + sb.append("\n },\n"); + sb.append(" \"parameters\": {\n"); + int paramCounter = 0; + for (String name : inputs.keySet()) { + paramCounter++; + if (paramCounter > 1) { + sb.append(",\n"); + } + Object o = inputs.get(name); + if (o instanceof java.lang.String) { + sb.append(" \"" + name + "\": \"" + inputs.get(name).toString() + "\""); + } else if (o instanceof Integer) { + sb.append(" \"" + name + "\": " + inputs.get(name).toString() ); + } else if (o instanceof ArrayList) { + sb.append(" \"" + name + "\": " + inputs.get(name).toString() ); + } else if (o instanceof Boolean) { + sb.append(" \"" + name + "\": " + inputs.get(name).toString() ); + } else { + sb.append(" \"" + name + "\": " + "\"(there was an issue trying to dump this value...)\"" ); + } + } + sb.append("\n }\n}\n"); + + return sb.toString(); + } + + /******************************************************************************* + * + * Methods (and associated utilities) to implement the VduPlugin interface + * + *******************************************************************************/ + + /** + * VduPlugin interface for instantiate function. + * + * Translate the VduPlugin parameters to the corresponding 'createStack' parameters, + * and then invoke the existing function. + */ + @Override + public VduInstance instantiateVdu ( + CloudInfo cloudInfo, + String instanceName, + Map<String,Object> inputs, + VduModelInfo vduModel, + boolean rollbackOnFailure) + throws VduException + { + String cloudSiteId = cloudInfo.getCloudSiteId(); + String tenantId = cloudInfo.getTenantId(); + + // Translate the VDU ModelInformation structure to that which is needed for + // creating the Heat stack. Loop through the artifacts, looking specifically + // for MAIN_TEMPLATE and ENVIRONMENT. Any other artifact will + // be attached as a FILE. + String heatTemplate = null; + Map<String,Object> nestedTemplates = new HashMap<>(); + Map<String,Object> files = new HashMap<>(); + String heatEnvironment = null; + + for (VduArtifact vduArtifact: vduModel.getArtifacts()) { + if (vduArtifact.getType() == ArtifactType.MAIN_TEMPLATE) { + heatTemplate = new String(vduArtifact.getContent()); + } + else if (vduArtifact.getType() == ArtifactType.NESTED_TEMPLATE) { + nestedTemplates.put(vduArtifact.getName(), new String(vduArtifact.getContent())); + } + else if (vduArtifact.getType() == ArtifactType.ENVIRONMENT) { + heatEnvironment = new String(vduArtifact.getContent()); + } + } + + try { + StackInfo stackInfo = createStack (cloudSiteId, + tenantId, + instanceName, + heatTemplate, + inputs, + true, // poll for completion + vduModel.getTimeoutMinutes(), + heatEnvironment, + nestedTemplates, + files, + rollbackOnFailure); + + // Populate a vduInstance from the StackInfo + return stackInfoToVduInstance(stackInfo); + } + catch (Exception e) { + throw new VduException ("MsoHeatUtils (instantiateVDU): createStack Exception", e); + } + } + + + /** + * VduPlugin interface for query function. + */ + @Override + public VduInstance queryVdu (CloudInfo cloudInfo, String instanceId) + throws VduException + { + String cloudSiteId = cloudInfo.getCloudSiteId(); + String tenantId = cloudInfo.getTenantId(); + + try { + // Query the Cloudify Deployment object and populate a VduInstance + StackInfo stackInfo = queryStack (cloudSiteId, tenantId, instanceId); + + return stackInfoToVduInstance(stackInfo); + } + catch (Exception e) { + throw new VduException ("MsoHeatUtile (queryVdu): queryStack Exception ", e); + } + } + + + /** + * VduPlugin interface for delete function. + */ + @Override + public VduInstance deleteVdu (CloudInfo cloudInfo, String instanceId, int timeoutMinutes) + throws VduException + { + String cloudSiteId = cloudInfo.getCloudSiteId(); + String tenantId = cloudInfo.getTenantId(); + + try { + // Delete the Heat stack + StackInfo stackInfo = deleteStack (tenantId, cloudSiteId, instanceId, true); + + // Populate a VduInstance based on the deleted Cloudify Deployment object + VduInstance vduInstance = stackInfoToVduInstance(stackInfo); + + // Override return state to DELETED (HeatUtils sets to NOTFOUND) + vduInstance.getStatus().setState(VduStateType.DELETED); + + return vduInstance; + } + catch (Exception e) { + throw new VduException ("Delete VDU Exception", e); + } + } + + + /** + * VduPlugin interface for update function. + * + * Update is currently not supported in the MsoHeatUtils implementation of VduPlugin. + * Just return a VduException. + * + */ + @Override + public VduInstance updateVdu ( + CloudInfo cloudInfo, + String instanceId, + Map<String,Object> inputs, + VduModelInfo vduModel, + boolean rollbackOnFailure) + throws VduException + { + throw new VduException ("MsoHeatUtils: updateVdu interface not supported"); + } + + + /* + * Convert the local DeploymentInfo object (Cloudify-specific) to a generic VduInstance object + */ + private VduInstance stackInfoToVduInstance (StackInfo stackInfo) + { + VduInstance vduInstance = new VduInstance(); + + // The full canonical name as the instance UUID + vduInstance.setVduInstanceId(stackInfo.getCanonicalName()); + vduInstance.setVduInstanceName(stackInfo.getName()); + + // Copy inputs and outputs + vduInstance.setInputs(stackInfo.getParameters()); + vduInstance.setOutputs(stackInfo.getOutputs()); + + // Translate the status elements + vduInstance.setStatus(stackStatusToVduStatus (stackInfo)); + + return vduInstance; + } + + private VduStatus stackStatusToVduStatus (StackInfo stackInfo) + { + VduStatus vduStatus = new VduStatus(); + + // Map the status fields to more generic VduStatus. + // There are lots of HeatStatus values, so this is a bit long... + HeatStatus heatStatus = stackInfo.getStatus(); + String statusMessage = stackInfo.getStatusMessage(); + + if (heatStatus == HeatStatus.INIT || heatStatus == HeatStatus.BUILDING) { + vduStatus.setState(VduStateType.INSTANTIATING); + vduStatus.setLastAction((new PluginAction ("create", "in_progress", statusMessage))); + } + else if (heatStatus == HeatStatus.NOTFOUND) { + vduStatus.setState(VduStateType.NOTFOUND); + } + else if (heatStatus == HeatStatus.CREATED) { + vduStatus.setState(VduStateType.INSTANTIATED); + vduStatus.setLastAction((new PluginAction ("create", "complete", statusMessage))); + } + else if (heatStatus == HeatStatus.UPDATED) { + vduStatus.setState(VduStateType.INSTANTIATED); + vduStatus.setLastAction((new PluginAction ("update", "complete", statusMessage))); + } + else if (heatStatus == HeatStatus.UPDATING) { + vduStatus.setState(VduStateType.UPDATING); + vduStatus.setLastAction((new PluginAction ("update", "in_progress", statusMessage))); + } + else if (heatStatus == HeatStatus.DELETING) { + vduStatus.setState(VduStateType.DELETING); + vduStatus.setLastAction((new PluginAction ("delete", "in_progress", statusMessage))); + } + else if (heatStatus == HeatStatus.FAILED) { + vduStatus.setState(VduStateType.FAILED); + vduStatus.setErrorMessage(stackInfo.getStatusMessage()); + } else { + vduStatus.setState(VduStateType.UNKNOWN); + } + + return vduStatus; + } + + private void sleep(long time) { + try { + Thread.sleep(time); + } catch (InterruptedException e) { + LOGGER.debug ("Thread interrupted while sleeping", e); + Thread.currentThread().interrupt(); + } + } + +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoHeatUtilsWithUpdate.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoHeatUtilsWithUpdate.java new file mode 100644 index 0000000000..0b3f9dfe17 --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoHeatUtilsWithUpdate.java @@ -0,0 +1,438 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * Copyright (C) 2017 Huawei Technologies Co., Ltd. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.openstack.utils; + + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.onap.so.cloud.CloudConfig; +import org.onap.so.cloud.CloudSite; +import org.onap.so.logger.MessageEnum; +import org.onap.so.logger.MsoLogger; +import org.onap.so.openstack.beans.StackInfo; +import org.onap.so.openstack.exceptions.MsoCloudSiteNotFound; +import org.onap.so.openstack.exceptions.MsoException; +import org.onap.so.openstack.exceptions.MsoOpenstackException; +import org.onap.so.openstack.exceptions.MsoStackNotFound; +import org.onap.so.openstack.mappers.StackInfoMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.woorea.openstack.base.client.OpenStackBaseException; +import com.woorea.openstack.base.client.OpenStackRequest; +import com.woorea.openstack.heat.Heat; +import com.woorea.openstack.heat.model.Stack; +import com.woorea.openstack.heat.model.Stack.Output; +import com.woorea.openstack.heat.model.UpdateStackParam; + +@Component +public class MsoHeatUtilsWithUpdate extends MsoHeatUtils { + + private static final String UPDATE_STACK = "UpdateStack"; + private static final MsoLogger LOGGER = MsoLogger.getMsoLogger (MsoLogger.Catalog.RA, MsoHeatUtilsWithUpdate.class); + + private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); + + @Autowired + private Environment environment; + /* + * Keep these methods around for backward compatibility + */ + + public StackInfo updateStack (String cloudSiteId, + String tenantId, + String stackName, + String heatTemplate, + Map <String, Object> stackInputs, + boolean pollForCompletion, + int timeoutMinutes) throws MsoException { + // Keeping this method to allow compatibility with no environment or files variable sent. In this case, + // simply return the new method with the environment variable set to null. + return this.updateStack (cloudSiteId, + tenantId, + stackName, + heatTemplate, + stackInputs, + pollForCompletion, + timeoutMinutes, + null, + null, + null); + } + + public StackInfo updateStack (String cloudSiteId, + String tenantId, + String stackName, + String heatTemplate, + Map <String, Object> stackInputs, + boolean pollForCompletion, + int timeoutMinutes, + String environment) throws MsoException { + // Keeping this method to allow compatibility with no environment variable sent. In this case, + // simply return the new method with the files variable set to null. + return this.updateStack (cloudSiteId, + tenantId, + stackName, + heatTemplate, + stackInputs, + pollForCompletion, + timeoutMinutes, + environment, + null, + null); + } + + public StackInfo updateStack (String cloudSiteId, + String tenantId, + String stackName, + String heatTemplate, + Map <String, Object> stackInputs, + boolean pollForCompletion, + int timeoutMinutes, + String environment, + Map <String, Object> files) throws MsoException { + return this.updateStack (cloudSiteId, + tenantId, + stackName, + heatTemplate, + stackInputs, + pollForCompletion, + timeoutMinutes, + environment, + files, + null); + } + + /** + * Update a Stack in the specified cloud location and tenant. The Heat template + * and parameter map are passed in as arguments, along with the cloud access credentials. + * It is expected that parameters have been validated and contain at minimum the required + * parameters for the given template with no extra (undefined) parameters.. + * + * The Stack name supplied by the caller must be unique in the scope of this tenant. + * However, it should also be globally unique, as it will be the identifier for the + * resource going forward in Inventory. This latter is managed by the higher levels + * invoking this function. + * + * The caller may choose to let this function poll Openstack for completion of the + * stack creation, or may handle polling itself via separate calls to query the status. + * In either case, a StackInfo object will be returned containing the current status. + * When polling is enabled, a status of CREATED is expected. When not polling, a + * status of BUILDING is expected. + * + * An error will be thrown if the requested Stack already exists in the specified + * Tenant and Cloud. + * + * @param tenantId The Openstack ID of the tenant in which to create the Stack + * @param cloudSiteId The cloud identifier (may be a region) in which to create the tenant. + * @param stackName The name of the stack to update + * @param heatTemplate The Heat template + * @param stackInputs A map of key/value inputs + * @param pollForCompletion Indicator that polling should be handled in Java vs. in the client + * @param environment An optional yaml-format string to specify environmental parameters + * @param files a Map<String, Object> for listing child template IDs + * @param heatFiles a Map<String, Object> for listing get_file entries (fileName, fileBody) + * @return A StackInfo object + * @throws MsoException Thrown if the Openstack API call returns an exception. + */ + + public StackInfo updateStack (String cloudSiteId, + String tenantId, + String stackName, + String heatTemplate, + Map <String, Object> stackInputs, + boolean pollForCompletion, + int timeoutMinutes, + String environment, + Map <String, Object> files, + Map <String, Object> heatFiles) throws MsoException { + boolean heatEnvtVariable = true; + if (environment == null || "".equalsIgnoreCase (environment.trim ())) { + heatEnvtVariable = false; + } + boolean haveFiles = true; + if (files == null || files.isEmpty ()) { + haveFiles = false; + } + boolean haveHeatFiles = true; + if (heatFiles == null || heatFiles.isEmpty ()) { + haveHeatFiles = false; + } + + // Obtain the cloud site information where we will create the stack + CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow( + () -> new MsoCloudSiteNotFound(cloudSiteId)); + // Get a Heat client. They are cached between calls (keyed by tenantId:cloudId) + // This could throw MsoTenantNotFound or MsoOpenstackException (both propagated) + Heat heatClient = getHeatClient (cloudSite, tenantId); + + // Perform a query first to get the current status + Stack heatStack = queryHeatStack (heatClient, stackName); + if (heatStack == null || "DELETE_COMPLETE".equals (heatStack.getStackStatus ())) { + // Not found. Return a StackInfo with status NOTFOUND + throw new MsoStackNotFound (stackName, tenantId, cloudSiteId); + } + + // Use canonical name "<stack name>/<stack-id>" to update the stack. + // Otherwise, update by name returns a 302 redirect. + // NOTE: This is specific to the v1 Orchestration API. + String canonicalName = heatStack.getStackName () + "/" + heatStack.getId (); + + LOGGER.debug ("Ready to Update Stack (" + canonicalName + ") with input params: " + stackInputs); + //force entire stackInput object to generic Map<String, Object> for openstack compatibility + ObjectMapper mapper = new ObjectMapper(); + Map<String, Object> normalized = new HashMap<>(); + try { + normalized = mapper.readValue(mapper.writeValueAsString(stackInputs), new TypeReference<HashMap<String,Object>>() {}); + } catch (IOException e1) { + LOGGER.debug("could not map json", e1); + } + // Build up the stack update parameters + // Disable auto-rollback, because error reason is lost. Always rollback in the code. + UpdateStackParam stack = new UpdateStackParam (); + stack.setTimeoutMinutes (timeoutMinutes); + stack.setParameters (normalized); + stack.setTemplate (heatTemplate); + stack.setDisableRollback (true); + // TJM add envt to stack + if (heatEnvtVariable) { + stack.setEnvironment (environment); + } + + // Handle nested templates & get_files here. if we have both - must combine + // and then add to stack (both are part of "files:" being added to stack) + if (haveFiles && haveHeatFiles) { + // Let's do this here - not in the bean + LOGGER.debug ("Found files AND heatFiles - combine and add!"); + Map <String, Object> combinedFiles = new HashMap<>(); + for (String keyString : files.keySet ()) { + combinedFiles.put (keyString, files.get (keyString)); + } + for (String keyString : heatFiles.keySet ()) { + combinedFiles.put (keyString, heatFiles.get (keyString)); + } + stack.setFiles (combinedFiles); + } else { + // Handle case where we have one or neither + if (haveFiles) { + stack.setFiles (files); + } + if (haveHeatFiles) { + // setFiles method modified to handle adding a map. + stack.setFiles (heatFiles); + } + } + + try { + // Execute the actual Openstack command to update the Heat stack + OpenStackRequest <Void> request = heatClient.getStacks ().update (canonicalName, stack); + executeAndRecordOpenstackRequest (request); + } catch (OpenStackBaseException e) { + // Since this came on the 'Update Stack' command, nothing was changed + // in the cloud. Rethrow the error as an MSO exception. + throw heatExceptionToMsoException (e, UPDATE_STACK); + } catch (RuntimeException e) { + // Catch-all + throw runtimeExceptionToMsoException (e, UPDATE_STACK); + } + + // If client has requested a final response, poll for stack completion + Stack updateStack = null; + if (pollForCompletion) { + // Set a time limit on overall polling. + // Use the resource (template) timeout for Openstack (expressed in minutes) + // and add one poll interval to give Openstack a chance to fail on its own. + int createPollInterval = Integer.parseInt(this.environment.getProperty(createPollIntervalProp, createPollIntervalDefault)); + int pollTimeout = (timeoutMinutes * 60) + createPollInterval; + + boolean loopAgain = true; + while (loopAgain) { + try { + updateStack = queryHeatStack (heatClient, canonicalName); + LOGGER.debug (updateStack.getStackStatus () + " (" + canonicalName + ")"); + try { + LOGGER.debug("Current stack " + this.getOutputsAsStringBuilderWithUpdate(heatStack).toString()); + } catch (Exception e) { + LOGGER.debug("an error occurred trying to print out the current outputs of the stack", e); + } + + + if ("UPDATE_IN_PROGRESS".equals (updateStack.getStackStatus ())) { + // Stack update is still running. + // Sleep and try again unless timeout has been reached + if (pollTimeout <= 0) { + // Note that this should not occur, since there is a timeout specified + // in the Openstack call. + LOGGER.error (MessageEnum.RA_UPDATE_STACK_TIMEOUT, cloudSiteId, tenantId, stackName, updateStack.getStackStatus(), "", "", MsoLogger.ErrorCode.AvailabilityError, "Update stack timeout"); + loopAgain = false; + } else { + try { + Thread.sleep (createPollInterval * 1000L); + } catch (InterruptedException e) { + // If we are interrupted, we should stop ASAP. + loopAgain = false; + // Set again the interrupted flag + Thread.currentThread().interrupt(); + } + } + pollTimeout -= createPollInterval; + LOGGER.debug("pollTimeout remaining: " + pollTimeout); + } else { + loopAgain = false; + } + } catch (MsoException e) { + // Cannot query the stack. Something is wrong. + + // TODO: No way to roll back the stack at this point. What to do? + e.addContext (UPDATE_STACK); + throw e; + } + } + + if (!"UPDATE_COMPLETE".equals (updateStack.getStackStatus ())) { + LOGGER.error (MessageEnum.RA_UPDATE_STACK_ERR, updateStack.getStackStatus(), updateStack.getStackStatusReason(), "", "", MsoLogger.ErrorCode.DataError, "Update Stack error"); + + // TODO: No way to roll back the stack at this point. What to do? + // Throw a 'special case' of MsoOpenstackException to report the Heat status + MsoOpenstackException me = null; + if ("UPDATE_IN_PROGRESS".equals (updateStack.getStackStatus ())) { + me = new MsoOpenstackException (0, "", "Stack Update Timeout"); + } else { + String error = "Stack error (" + updateStack.getStackStatus () + + "): " + + updateStack.getStackStatusReason (); + me = new MsoOpenstackException (0, "", error); + } + me.addContext (UPDATE_STACK); + throw me; + } + + } else { + // Return the current status. + updateStack = queryHeatStack (heatClient, canonicalName); + if (updateStack != null) { + LOGGER.debug ("UpdateStack, status = " + updateStack.getStackStatus ()); + } else { + LOGGER.debug ("UpdateStack, stack not found"); + } + } + return new StackInfoMapper(updateStack).map(); + } + + private StringBuilder getOutputsAsStringBuilderWithUpdate(Stack heatStack) { + // This should only be used as a utility to print out the stack outputs + // to the log + StringBuilder sb = new StringBuilder(""); + if (heatStack == null) { + sb.append("(heatStack is null)"); + return sb; + } + List<Output> outputList = heatStack.getOutputs(); + if (outputList == null || outputList.isEmpty()) { + sb.append("(outputs is empty)"); + return sb; + } + Map<String, Object> outputs = new HashMap<>(); + for (Output outputItem : outputList) { + outputs.put(outputItem.getOutputKey(), outputItem.getOutputValue()); + } + int counter = 0; + sb.append("OUTPUTS:\n"); + for (String key : outputs.keySet()) { + sb.append("outputs[").append(counter++).append("]: ").append(key).append("="); + Object obj = outputs.get(key); + if (obj instanceof String) { + sb.append((String) obj).append(" (a string)"); + } else if (obj instanceof JsonNode) { + sb.append(this.convertNodeWithUpdate((JsonNode) obj)).append(" (a JsonNode)"); + } else if (obj instanceof java.util.LinkedHashMap) { + try { + String str = JSON_MAPPER.writeValueAsString(obj); + sb.append(str).append(" (a java.util.LinkedHashMap)"); + } catch (Exception e) { + LOGGER.debug("Exception :", e); + sb.append("(a LinkedHashMap value that would not convert nicely)"); + } + } else if (obj instanceof Integer) { + String str = ""; + try { + str = obj.toString() + " (an Integer)\n"; + } catch (Exception e) { + LOGGER.debug("Exception :", e); + str = "(an Integer unable to call .toString() on)"; + } + sb.append(str); + } else if (obj instanceof ArrayList) { + String str = ""; + try { + str = obj.toString() + " (an ArrayList)"; + } catch (Exception e) { + LOGGER.debug("Exception :", e); + str = "(an ArrayList unable to call .toString() on?)"; + } + sb.append(str); + } else if (obj instanceof Boolean) { + String str = ""; + try { + str = obj.toString() + " (a Boolean)"; + } catch (Exception e) { + LOGGER.debug("Exception :", e); + str = "(an Boolean unable to call .toString() on?)"; + } + sb.append(str); + } + else { + String str = ""; + try { + str = obj.toString() + " (unknown Object type)"; + } catch (Exception e) { + LOGGER.debug("Exception :", e); + str = "(a value unable to call .toString() on?)"; + } + sb.append(str); + } + sb.append("\n"); + } + sb.append("[END]"); + return sb; + } + + private String convertNodeWithUpdate(final JsonNode node) { + try { + final Object obj = JSON_MAPPER.treeToValue(node, Object.class); + final String json = JSON_MAPPER.writeValueAsString(obj); + return json; + } catch (Exception e) { + LOGGER.debug("Error converting json to string " + e.getMessage(), e); + } + return "[Error converting json to string]"; + } + +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoKeystoneUtils.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoKeystoneUtils.java new file mode 100644 index 0000000000..d3ec74db8d --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoKeystoneUtils.java @@ -0,0 +1,669 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.openstack.utils; + + +import java.io.Serializable; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.onap.so.cloud.CloudIdentity; +import org.onap.so.cloud.CloudSite; +import org.onap.so.cloud.authentication.AuthenticationMethodFactory; +import org.onap.so.logger.MessageEnum; +import org.onap.so.logger.MsoAlarmLogger; +import org.onap.so.logger.MsoLogger; +import org.onap.so.openstack.beans.MsoTenant; +import org.onap.so.openstack.exceptions.MsoAdapterException; +import org.onap.so.openstack.exceptions.MsoCloudSiteNotFound; +import org.onap.so.openstack.exceptions.MsoException; +import org.onap.so.openstack.exceptions.MsoOpenstackException; +import org.onap.so.openstack.exceptions.MsoTenantAlreadyExists; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.woorea.openstack.base.client.OpenStackBaseException; +import com.woorea.openstack.base.client.OpenStackConnectException; +import com.woorea.openstack.base.client.OpenStackRequest; +import com.woorea.openstack.base.client.OpenStackResponseException; +import com.woorea.openstack.keystone.Keystone; +import com.woorea.openstack.keystone.model.Access; +import com.woorea.openstack.keystone.model.Authentication; +import com.woorea.openstack.keystone.model.Metadata; +import com.woorea.openstack.keystone.model.Role; +import com.woorea.openstack.keystone.model.Roles; +import com.woorea.openstack.keystone.model.Tenant; +import com.woorea.openstack.keystone.model.User; +import com.woorea.openstack.keystone.utils.KeystoneUtils; + +@Component +public class MsoKeystoneUtils extends MsoTenantUtils { + + // Cache the Keystone Clients statically. Since there is just one MSO user, there is no + // benefit to re-authentication on every request (or across different flows). The + // token will be used until it expires. + // + // The cache key is "cloudId" + private static Map <String, KeystoneCacheEntry> adminClientCache = new HashMap<>(); + + private static MsoLogger LOGGER = MsoLogger.getMsoLogger (MsoLogger.Catalog.RA, MsoKeystoneUtils.class); + + @Autowired + private AuthenticationMethodFactory authenticationMethodFactory; + + @Autowired + private MsoHeatUtils msoHeatUtils; + + @Autowired + private MsoNeutronUtils msoNeutronUtils; + + @Autowired + private MsoTenantUtilsFactory tenantUtilsFactory; + /** + * Create a tenant with the specified name in the given cloud. If the tenant already exists, + * an Exception will be thrown. The MSO User will also be added to the "member" list of + * the new tenant to perform subsequent Nova/Heat commands in the tenant. If the MSO User + * association fails, the entire transaction will be rolled back. + * <p> + * For the AIC Cloud (DCP/LCP): it is not clear that cloudId is needed, as all admin + * requests go to the centralized identity service in DCP. However, if some artifact + * must exist in each local LCP instance as well, then it will be needed to access the + * correct region. + * <p> + * + * @param tenantName The tenant name to create + * @param cloudId The cloud identifier (may be a region) in which to create the tenant. + * @return the tenant ID of the newly created tenant + * @throws MsoTenantAlreadyExists Thrown if the requested tenant already exists + * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception + */ + public String createTenant (String tenantName, + String cloudSiteId, + Map <String, String> metadata, + boolean backout) throws MsoException { + // Obtain the cloud site information where we will create the tenant + Optional<CloudSite> cloudSiteOpt = cloudConfig.getCloudSite(cloudSiteId); + if (!cloudSiteOpt.isPresent()) { + LOGGER.error(MessageEnum.RA_CREATE_TENANT_ERR, "MSOCloudSite not found", "", "", MsoLogger.ErrorCode.DataError, "MSOCloudSite not found"); + throw new MsoCloudSiteNotFound (cloudSiteId); + } + Keystone keystoneAdminClient = getKeystoneAdminClient(cloudSiteOpt.get()); + Tenant tenant = null; + try { + // Check if the tenant already exists + tenant = findTenantByName (keystoneAdminClient, tenantName); + + if (tenant != null) { + // Tenant already exists. Throw an exception + LOGGER.error(MessageEnum.RA_TENANT_ALREADY_EXIST, tenantName, cloudSiteId, "", "", MsoLogger.ErrorCode.DataError, "Tenant already exists"); + throw new MsoTenantAlreadyExists (tenantName, cloudSiteId); + } + + // Does not exist, create a new one + tenant = new Tenant (); + tenant.setName (tenantName); + tenant.setDescription ("SDN Tenant (via MSO)"); + tenant.setEnabled (true); + + OpenStackRequest <Tenant> request = keystoneAdminClient.tenants ().create (tenant); + tenant = executeAndRecordOpenstackRequest (request); + } catch (OpenStackBaseException e) { + // Convert Keystone OpenStackResponseException to MsoOpenstackException + throw keystoneErrorToMsoException (e, "CreateTenant"); + } catch (RuntimeException e) { + // Catch-all + throw runtimeExceptionToMsoException (e, "CreateTenant"); + } + + // Add MSO User to the tenant as a member and + // apply tenant metadata if supported by the cloud site + try { + CloudIdentity cloudIdentity = cloudConfig.getIdentityService(cloudSiteOpt.get().getIdentityServiceId()); + + User msoUser = findUserByNameOrId (keystoneAdminClient, cloudIdentity.getMsoId ()); + Role memberRole = findRoleByNameOrId (keystoneAdminClient, cloudIdentity.getMemberRole ()); + + if(msoUser != null && memberRole != null) { + OpenStackRequest <Void> request = keystoneAdminClient.tenants ().addUser (tenant.getId (), + msoUser.getId (), + memberRole.getId ()); + executeAndRecordOpenstackRequest (request); + } + + if (cloudIdentity.hasTenantMetadata () && metadata != null && !metadata.isEmpty ()) { + Metadata tenantMetadata = new Metadata (); + tenantMetadata.setMetadata (metadata); + + OpenStackRequest <Metadata> metaRequest = keystoneAdminClient.tenants () + .createOrUpdateMetadata (tenant.getId (), + tenantMetadata); + executeAndRecordOpenstackRequest (metaRequest); + } + } catch (Exception e) { + // Failed to attach MSO User to the new tenant. Can't operate without access, + // so roll back the tenant. + if (!backout) + { + LOGGER.warn(MessageEnum.RA_CREATE_TENANT_ERR, "Create Tenant errored, Tenant deletion suppressed", "Openstack", "", MsoLogger.ErrorCode.DataError, "Create Tenant error, Tenant deletion suppressed"); + } + else + { + try { + OpenStackRequest <Void> request = keystoneAdminClient.tenants ().delete (tenant.getId ()); + executeAndRecordOpenstackRequest (request); + } catch (Exception e2) { + // Just log this one. We will report the original exception. + LOGGER.error (MessageEnum.RA_CREATE_TENANT_ERR, "Nested exception rolling back tenant", "Openstack", "", MsoLogger.ErrorCode.DataError, "Create Tenant error, Nested exception rolling back tenant", e2); + } + } + + + // Propagate the original exception on user/role/tenant mapping + if (e instanceof OpenStackBaseException) { + // Convert Keystone Exception to MsoOpenstackException + throw keystoneErrorToMsoException ((OpenStackBaseException) e, "CreateTenantUser"); + } else { + MsoAdapterException me = new MsoAdapterException (e.getMessage (), e); + me.addContext ("CreateTenantUser"); + throw me; + } + } + return tenant.getId (); + } + + /** + * Query for a tenant by ID in the given cloud. If the tenant exists, + * return an MsoTenant object. If not, return null. + * <p> + * For the AIC Cloud (DCP/LCP): it is not clear that cloudId is needed, as all admin + * requests go to the centralized identity service in DCP. However, if some artifact + * must exist in each local LCP instance as well, then it will be needed to access the + * correct region. + * <p> + * + * @param tenantId The Openstack ID of the tenant to query + * @param cloudSiteId The cloud identifier (may be a region) in which to query the tenant. + * @return the tenant properties of the queried tenant, or null if not found + * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception + */ + public MsoTenant queryTenant (String tenantId, String cloudSiteId) throws MsoException { + // Obtain the cloud site information where we will query the tenant + CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow( + () -> new MsoCloudSiteNotFound(cloudSiteId)); + + Keystone keystoneAdminClient = getKeystoneAdminClient (cloudSite); + + // Check if the tenant exists and return its Tenant Id + try { + Tenant tenant = findTenantById (keystoneAdminClient, tenantId); + if (tenant == null) { + return null; + } + + Map <String, String> metadata = new HashMap <String, String> (); + if (cloudConfig.getIdentityService(cloudSite.getIdentityServiceId()).hasTenantMetadata ()) { + OpenStackRequest <Metadata> request = keystoneAdminClient.tenants ().showMetadata (tenant.getId ()); + Metadata tenantMetadata = executeAndRecordOpenstackRequest (request); + if (tenantMetadata != null) { + metadata = tenantMetadata.getMetadata (); + } + } + return new MsoTenant (tenant.getId (), tenant.getName (), metadata); + } catch (OpenStackBaseException e) { + // Convert Keystone OpenStackResponseException to MsoOpenstackException + throw keystoneErrorToMsoException (e, "QueryTenant"); + } catch (RuntimeException e) { + // Catch-all + throw runtimeExceptionToMsoException (e, "QueryTenant"); + } + } + + /** + * Query for a tenant with the specified name in the given cloud. If the tenant exists, + * return an MsoTenant object. If not, return null. This query is useful if the client + * knows it has the tenant name, skipping an initial lookup by ID that would always fail. + * <p> + * For the AIC Cloud (DCP/LCP): it is not clear that cloudId is needed, as all admin + * requests go to the centralized identity service in DCP. However, if some artifact + * must exist in each local LCP instance as well, then it will be needed to access the + * correct region. + * <p> + * + * @param tenantName The name of the tenant to query + * @param cloudSiteId The cloud identifier (may be a region) in which to query the tenant. + * @return the tenant properties of the queried tenant, or null if not found + * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception + */ + public MsoTenant queryTenantByName (String tenantName, String cloudSiteId) throws MsoException { + // Obtain the cloud site information where we will query the tenant + CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow( + () -> new MsoCloudSiteNotFound(cloudSiteId)); + Keystone keystoneAdminClient = getKeystoneAdminClient (cloudSite); + + try { + Tenant tenant = findTenantByName (keystoneAdminClient, tenantName); + if (tenant == null) { + return null; + } + + Map <String, String> metadata = new HashMap <String, String> (); + if (cloudConfig.getIdentityService(cloudSite.getIdentityServiceId()).hasTenantMetadata ()) { + OpenStackRequest <Metadata> request = keystoneAdminClient.tenants ().showMetadata (tenant.getId ()); + Metadata tenantMetadata = executeAndRecordOpenstackRequest (request); + if (tenantMetadata != null) { + metadata = tenantMetadata.getMetadata (); + } + } + return new MsoTenant (tenant.getId (), tenant.getName (), metadata); + } catch (OpenStackBaseException e) { + // Convert Keystone OpenStackResponseException to MsoOpenstackException + throw keystoneErrorToMsoException (e, "QueryTenantName"); + } catch (RuntimeException e) { + // Catch-all + throw runtimeExceptionToMsoException (e, "QueryTenantName"); + } + } + + /** + * Delete the specified Tenant (by ID) in the given cloud. This method returns true or + * false, depending on whether the tenant existed and was successfully deleted, or if + * the tenant already did not exist. Both cases are treated as success (no Exceptions). + * <p> + * Note for the AIC Cloud (DCP/LCP): all admin requests go to the centralized identity + * service in DCP. So deleting a tenant from one cloudSiteId will remove it from all + * sites managed by that identity service. + * <p> + * + * @param tenantId The Openstack ID of the tenant to delete + * @param cloudSiteId The cloud identifier from which to delete the tenant. + * @return true if the tenant was deleted, false if the tenant did not exist. + * @throws MsoOpenstackException If the Openstack API call returns an exception. + */ + public boolean deleteTenant (String tenantId, String cloudSiteId) throws MsoException { + // Obtain the cloud site information where we will query the tenant + CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow( + () -> new MsoCloudSiteNotFound(cloudSiteId)); + Keystone keystoneAdminClient = getKeystoneAdminClient (cloudSite); + + try { + // Check that the tenant exists. Also, need the ID to delete + Tenant tenant = findTenantById (keystoneAdminClient, tenantId); + if (tenant == null) { + LOGGER.error(MessageEnum.RA_TENANT_NOT_FOUND, tenantId, cloudSiteId, "", "", MsoLogger.ErrorCode.DataError, "Tenant not found"); + return false; + } + + OpenStackRequest <Void> request = keystoneAdminClient.tenants ().delete (tenant.getId ()); + executeAndRecordOpenstackRequest (request); + LOGGER.debug ("Deleted Tenant " + tenant.getId () + " (" + tenant.getName () + ")"); + + // Clear any cached clients. Not really needed, ID will not be reused. + msoHeatUtils.expireHeatClient (tenant.getId (), cloudSiteId); + msoNeutronUtils.expireNeutronClient (tenant.getId (), cloudSiteId); + } catch (OpenStackBaseException e) { + // Convert Keystone OpenStackResponseException to MsoOpenstackException + throw keystoneErrorToMsoException (e, "Delete Tenant"); + } catch (RuntimeException e) { + // Catch-all + throw runtimeExceptionToMsoException (e, "DeleteTenant"); + } + + return true; + } + + /** + * Delete the specified Tenant (by Name) in the given cloud. This method returns true or + * false, depending on whether the tenant existed and was successfully deleted, or if + * the tenant already did not exist. Both cases are treated as success (no Exceptions). + * <p> + * Note for the AIC Cloud (DCP/LCP): all admin requests go to the centralized identity + * service in DCP. So deleting a tenant from one cloudSiteId will remove it from all + * sites managed by that identity service. + * <p> + * + * @param tenantName The name of the tenant to delete + * @param cloudSiteId The cloud identifier from which to delete the tenant. + * @return true if the tenant was deleted, false if the tenant did not exist. + * @throws MsoOpenstackException If the Openstack API call returns an exception. + */ + public boolean deleteTenantByName (String tenantName, String cloudSiteId) throws MsoException { + // Obtain the cloud site information where we will query the tenant + Optional<CloudSite> cloudSite = cloudConfig.getCloudSite (cloudSiteId); + if (!cloudSite.isPresent()) { + throw new MsoCloudSiteNotFound (cloudSiteId); + } + Keystone keystoneAdminClient = getKeystoneAdminClient (cloudSite.get()); + + try { + // Need the Tenant ID to delete (can't directly delete by name) + Tenant tenant = findTenantByName (keystoneAdminClient, tenantName); + if (tenant == null) { + // OK if tenant already doesn't exist. + LOGGER.error(MessageEnum.RA_TENANT_NOT_FOUND, tenantName, cloudSiteId, "", "", MsoLogger.ErrorCode.DataError, "Tenant not found"); + return false; + } + + // Execute the Delete. It has no return value. + OpenStackRequest <Void> request = keystoneAdminClient.tenants ().delete (tenant.getId ()); + executeAndRecordOpenstackRequest (request); + + LOGGER.debug ("Deleted Tenant " + tenant.getId () + " (" + tenant.getName () + ")"); + + // Clear any cached clients. Not really needed, ID will not be reused. + msoHeatUtils.expireHeatClient (tenant.getId (), cloudSiteId); + msoNeutronUtils.expireNeutronClient (tenant.getId (), cloudSiteId); + } catch (OpenStackBaseException e) { + // Note: It doesn't seem to matter if tenant doesn't exist, no exception is thrown. + // Convert Keystone OpenStackResponseException to MsoOpenstackException + throw keystoneErrorToMsoException (e, "DeleteTenant"); + } catch (RuntimeException e) { + // Catch-all + throw runtimeExceptionToMsoException (e, "DeleteTenant"); + } + + return true; + } + + // ------------------------------------------------------------------- + // PRIVATE UTILITY FUNCTIONS FOR USE WITHIN THIS CLASS + + /* + * Get a Keystone Admin client for the Openstack Identity service. + * This requires an 'admin'-level userId + password along with an 'admin' tenant + * in the target cloud. These values will be retrieved from properties based + * on the specified cloud ID. + * <p> + * On successful authentication, the Keystone object will be cached for the cloudId + * so that it can be reused without going back to Openstack every time. + * + * @param cloudId + * + * @return an authenticated Keystone object + */ + public Keystone getKeystoneAdminClient (CloudSite cloudSite) throws MsoException { + CloudIdentity cloudIdentity = cloudConfig.getIdentityService(cloudSite.getIdentityServiceId()); + + String cloudId = cloudIdentity.getId (); + String adminTenantName = cloudIdentity.getAdminTenant (); + String region = cloudSite.getRegionId (); + + // Check first in the cache of previously authorized clients + KeystoneCacheEntry entry = adminClientCache.get (cloudId); + if (entry != null) { + if (!entry.isExpired ()) { + return entry.getKeystoneClient (); + } else { + // Token is expired. Remove it from cache. + adminClientCache.remove (cloudId); + } + } + MsoTenantUtils tenantUtils = tenantUtilsFactory.getTenantUtilsByServerType(cloudIdentity.getIdentityServerType()); + final String keystoneUrl = tenantUtils.getKeystoneUrl(region, cloudIdentity); + Keystone keystone = new Keystone(keystoneUrl); + + // Must authenticate against the 'admin' tenant to get the services endpoints + Access access = null; + String token = null; + try { + Authentication credentials = authenticationMethodFactory.getAuthenticationFor(cloudIdentity); + OpenStackRequest <Access> request = keystone.tokens () + .authenticate (credentials) + .withTenantName (adminTenantName); + access = executeAndRecordOpenstackRequest (request); + token = access.getToken ().getId (); + } catch (OpenStackResponseException e) { + if (e.getStatus () == 401) { + // Authentication error. Can't access admin tenant - something is mis-configured + String error = "MSO Authentication Failed for " + cloudIdentity.getId (); + alarmLogger.sendAlarm ("MsoAuthenticationError", MsoAlarmLogger.CRITICAL, error); + throw new MsoAdapterException (error); + } else { + throw keystoneErrorToMsoException (e, "TokenAuth"); + } + } catch (OpenStackConnectException e) { + // Connection to Openstack failed + throw keystoneErrorToMsoException (e, "TokenAuth"); + } + + // Get the Identity service URL. Throws runtime exception if not found per region. + String adminUrl = null; + try { + // TODO: FOR TESTING!!!! + adminUrl = KeystoneUtils.findEndpointURL (access.getServiceCatalog (), "identity", region, "public"); + adminUrl = adminUrl.replaceFirst("5000", "35357"); + } catch (RuntimeException e) { + String error = "Identity service not found: region=" + region + ",cloud=" + cloudIdentity.getId (); + alarmLogger.sendAlarm ("MsoConfigurationError", MsoAlarmLogger.CRITICAL, error); + LOGGER.error(MessageEnum.IDENTITY_SERVICE_NOT_FOUND, region, cloudIdentity.getId(), "Openstack", "", MsoLogger.ErrorCode.DataError, "Exception in findEndpointURL"); + throw new MsoAdapterException (error, e); + } + + // A new Keystone object is required for the new URL. Use the auth token from above. + // Note: this doesn't go back to Openstack, it's just a local object. + keystone = new Keystone (adminUrl); + keystone.token (token); + + // Cache to avoid re-authentication for every call. + KeystoneCacheEntry cacheEntry = new KeystoneCacheEntry (adminUrl, token, access.getToken ().getExpires ()); + adminClientCache.put (cloudId, cacheEntry); + + return keystone; + } + + /* + * Find a tenant (or query its existance) by its Name or Id. Check first against the + * ID. If that fails, then try by name. + * + * @param adminClient an authenticated Keystone object + * + * @param tenantName the tenant name or ID to query + * + * @return a Tenant object or null if not found + */ + public Tenant findTenantByNameOrId (Keystone adminClient, String tenantNameOrId) { + if (tenantNameOrId == null) { + return null; + } + + Tenant tenant = findTenantById (adminClient, tenantNameOrId); + if (tenant == null) { + tenant = findTenantByName (adminClient, tenantNameOrId); + } + + return tenant; + } + + /* + * Find a tenant (or query its existance) by its Id. + * + * @param adminClient an authenticated Keystone object + * + * @param tenantName the tenant ID to query + * + * @return a Tenant object or null if not found + */ + private Tenant findTenantById (Keystone adminClient, String tenantId) { + if (tenantId == null) { + return null; + } + + try { + OpenStackRequest <Tenant> request = adminClient.tenants ().show (tenantId); + return executeAndRecordOpenstackRequest (request); + } catch (OpenStackResponseException e) { + if (e.getStatus () == 404) { + return null; + } else { + LOGGER.error(MessageEnum.RA_CONNECTION_EXCEPTION, "Openstack Error, GET Tenant by Id (" + tenantId + "): " + e, "Openstack", "", MsoLogger.ErrorCode.DataError, "Exception in Openstack GET tenant by Id"); + throw e; + } + } + } + + /* + * Find a tenant (or query its existance) by its Name. This method avoids an + * initial lookup by ID when it's known that we have the tenant Name. + * + * @param adminClient an authenticated Keystone object + * + * @param tenantName the tenant name to query + * + * @return a Tenant object or null if not found + */ + public Tenant findTenantByName (Keystone adminClient, String tenantName) { + if (tenantName == null) { + return null; + } + + try { + OpenStackRequest <Tenant> request = adminClient.tenants ().show ("").queryParam ("name", tenantName); + return executeAndRecordOpenstackRequest (request); + } catch (OpenStackResponseException e) { + if (e.getStatus () == 404) { + return null; + } else { + LOGGER.error (MessageEnum.RA_CONNECTION_EXCEPTION, "Openstack Error, GET Tenant By Name (" + tenantName + "): " + e, "Openstack", "", MsoLogger.ErrorCode.DataError, "Exception in Openstack GET Tenant By Name"); + throw e; + } + } + } + + /* + * Look up an Openstack User by Name or Openstack ID. Check the ID first, and if that + * fails, try the Name. + * + * @param adminClient an authenticated Keystone object + * + * @param userName the user name or ID to query + * + * @return a User object or null if not found + */ + private User findUserByNameOrId (Keystone adminClient, String userNameOrId) { + if (userNameOrId == null) { + return null; + } + + try { + OpenStackRequest <User> request = adminClient.users ().show (userNameOrId); + return executeAndRecordOpenstackRequest (request); + } catch (OpenStackResponseException e) { + if (e.getStatus () == 404) { + // Not found by ID. Search for name + return findUserByName (adminClient, userNameOrId); + } else { + LOGGER.error (MessageEnum.RA_CONNECTION_EXCEPTION, "Openstack Error, GET User (" + userNameOrId + "): " + e, "Openstack", "", MsoLogger.ErrorCode.DataError, "Exception in Openstack GET User"); + throw e; + } + } + } + + /* + * Look up an Openstack User by Name. This avoids initial Openstack query by ID + * if we know we have the User Name. + * + * @param adminClient an authenticated Keystone object + * + * @param userName the user name to query + * + * @return a User object or null if not found + */ + public User findUserByName (Keystone adminClient, String userName) { + if (userName == null) { + return null; + } + + try { + OpenStackRequest <User> request = adminClient.users ().show ("").queryParam ("name", userName); + return executeAndRecordOpenstackRequest (request); + } catch (OpenStackResponseException e) { + if (e.getStatus () == 404) { + return null; + } else { + LOGGER.error (MessageEnum.RA_CONNECTION_EXCEPTION, "Openstack Error, GET User By Name (" + userName + "): " + e, "Openstack", "", MsoLogger.ErrorCode.DataError, "Exception in Openstack GET User By Name"); + throw e; + } + } + } + + /* + * Look up an Openstack Role by Name or Id. There is no direct query for Roles, so + * need to retrieve a full list from Openstack and look for a match. By default, + * Openstack should have a "_member_" role for normal VM-level privileges and an + * "admin" role for expanded privileges (e.g. administer tenants, users, and roles). + * <p> + * + * @param adminClient an authenticated Keystone object + * + * @param roleNameOrId the Role name or ID to look up + * + * @return a Role object + */ + private Role findRoleByNameOrId (Keystone adminClient, String roleNameOrId) { + if (roleNameOrId == null) { + return null; + } + + // Search by name or ID. Must search in list + OpenStackRequest <Roles> request = adminClient.roles ().list (); + Roles roles = executeAndRecordOpenstackRequest (request); + + for (Role role : roles) { + if (roleNameOrId.equals (role.getName ()) || roleNameOrId.equals (role.getId ())) { + return role; + } + } + + return null; + } + + private static class KeystoneCacheEntry implements Serializable { + + private static final long serialVersionUID = 1L; + + private String keystoneUrl; + private String token; + private Calendar expires; + + public KeystoneCacheEntry (String url, String token, Calendar expires) { + this.keystoneUrl = url; + this.token = token; + this.expires = expires; + } + + public Keystone getKeystoneClient () { + Keystone keystone = new Keystone (keystoneUrl); + keystone.token (token); + return keystone; + } + + public boolean isExpired () { + // adding arbitrary guard timer of 5 minutes + return expires == null || System.currentTimeMillis() > (expires.getTimeInMillis() - 300000); + } + } + + @Override + public String getKeystoneUrl(String regionId, CloudIdentity cloudIdentity) throws MsoException { + return cloudIdentity.getIdentityUrl(); + } +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoNeutronUtils.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoNeutronUtils.java new file mode 100644 index 0000000000..adeb008ad5 --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoNeutronUtils.java @@ -0,0 +1,550 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.openstack.utils; + + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.onap.so.cloud.CloudConfig; +import org.onap.so.cloud.CloudIdentity; +import org.onap.so.cloud.CloudSite; +import org.onap.so.cloud.authentication.AuthenticationMethodFactory; +import org.onap.so.logger.MessageEnum; +import org.onap.so.logger.MsoAlarmLogger; +import org.onap.so.logger.MsoLogger; +import org.onap.so.openstack.beans.NetworkInfo; +import org.onap.so.openstack.beans.NeutronCacheEntry; +import org.onap.so.openstack.exceptions.MsoAdapterException; +import org.onap.so.openstack.exceptions.MsoCloudSiteNotFound; +import org.onap.so.openstack.exceptions.MsoException; +import org.onap.so.openstack.exceptions.MsoIOException; +import org.onap.so.openstack.exceptions.MsoNetworkAlreadyExists; +import org.onap.so.openstack.exceptions.MsoNetworkNotFound; +import org.onap.so.openstack.exceptions.MsoOpenstackException; +import org.onap.so.openstack.exceptions.MsoTenantNotFound; +import org.onap.so.openstack.mappers.NetworkInfoMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.woorea.openstack.base.client.OpenStackBaseException; +import com.woorea.openstack.base.client.OpenStackConnectException; +import com.woorea.openstack.base.client.OpenStackRequest; +import com.woorea.openstack.base.client.OpenStackResponseException; +import com.woorea.openstack.keystone.Keystone; +import com.woorea.openstack.keystone.model.Access; +import com.woorea.openstack.keystone.model.Authentication; +import com.woorea.openstack.keystone.utils.KeystoneUtils; +import com.woorea.openstack.quantum.Quantum; +import com.woorea.openstack.quantum.model.Network; +import com.woorea.openstack.quantum.model.Networks; +import com.woorea.openstack.quantum.model.Segment; + +@Component +public class MsoNeutronUtils extends MsoCommonUtils +{ + // Cache Neutron Clients statically. Since there is just one MSO user, there is no + // benefit to re-authentication on every request (or across different flows). The + // token will be used until it expires. + // + // The cache key is "tenantId:cloudId" + private static Map<String,NeutronCacheEntry> neutronClientCache = new HashMap<>(); + + // Fetch cloud configuration each time (may be cached in CloudConfig class) + @Autowired + private CloudConfig cloudConfig; + + @Autowired + private AuthenticationMethodFactory authenticationMethodFactory; + + @Autowired + private MsoTenantUtilsFactory tenantUtilsFactory; + + private static MsoLogger LOGGER = MsoLogger.getMsoLogger (MsoLogger.Catalog.RA, MsoNeutronUtils.class); + + public enum NetworkType { + BASIC, PROVIDER, MULTI_PROVIDER + }; + + /** + * Create a network with the specified parameters in the given cloud/tenant. + * + * If a network already exists with the same name, an exception will be thrown. Note that + * this is an MSO-imposed restriction. Openstack does not require uniqueness on network names. + * <p> + * @param cloudSiteId The cloud identifier (may be a region) in which to create the network. + * @param tenantId The tenant in which to create the network + * @param type The type of network to create (Basic, Provider, Multi-Provider) + * @param networkName The network name to create + * @param provider The provider network name (for Provider or Multi-Provider networks) + * @param vlans A list of VLAN segments for the network (for Provider or Multi-Provider networks) + * @return a NetworkInfo object which describes the newly created network + * @throws MsoNetworkAlreadyExists Thrown if a network with the same name already exists + * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception + * @throws MsoCloudSiteNotFound Thrown if the cloudSite is invalid or unknown + */ + public NetworkInfo createNetwork (String cloudSiteId, String tenantId, NetworkType type, String networkName, String provider, List<Integer> vlans) + throws MsoException + { + // Obtain the cloud site information where we will create the stack + CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow( + () -> new MsoCloudSiteNotFound(cloudSiteId)); + + Quantum neutronClient = getNeutronClient (cloudSite, tenantId); + + // Check if a network already exists with this name + // Openstack will allow duplicate name, so require explicit check + Network network = findNetworkByName (neutronClient, networkName); + + if (network != null) { + // Network already exists. Throw an exception + LOGGER.error(MessageEnum.RA_NETWORK_ALREADY_EXIST, networkName, cloudSiteId, tenantId, "Openstack", "", MsoLogger.ErrorCode.DataError, "Network already exists"); + throw new MsoNetworkAlreadyExists (networkName, tenantId, cloudSiteId); + } + + // Does not exist, create a new one + network = new Network(); + network.setName(networkName); + network.setAdminStateUp(true); + + if (type == NetworkType.PROVIDER) { + if (provider != null && vlans != null && vlans.size() > 0) { + network.setProviderPhysicalNetwork (provider); + network.setProviderNetworkType("vlan"); + network.setProviderSegmentationId (vlans.get(0)); + } + } else if (type == NetworkType.MULTI_PROVIDER) { + if (provider != null && vlans != null && vlans.size() > 0) { + List<Segment> segments = new ArrayList<>(vlans.size()); + for (int vlan : vlans) { + Segment segment = new Segment(); + segment.setProviderPhysicalNetwork (provider); + segment.setProviderNetworkType("vlan"); + segment.setProviderSegmentationId (vlan); + + segments.add(segment); + } + network.setSegments(segments); + } + } + + try { + OpenStackRequest<Network> request = neutronClient.networks().create(network); + Network newNetwork = executeAndRecordOpenstackRequest(request); + return new NetworkInfoMapper(newNetwork).map(); + } + catch (OpenStackBaseException e) { + // Convert Neutron exception to an MsoOpenstackException + MsoException me = neutronExceptionToMsoException (e, "CreateNetwork"); + throw me; + } + catch (RuntimeException e) { + // Catch-all + MsoException me = runtimeExceptionToMsoException(e, "CreateNetwork"); + throw me; + } + } + + + /** + * Query for a network with the specified name or ID in the given cloud. If the network exists, + * return an NetworkInfo object. If not, return null. + * <p> + * Whenever possible, the network ID should be used as it is much more efficient. Query by + * name requires retrieval of all networks for the tenant and search for matching name. + * <p> + * @param networkNameOrId The network to query + * @param tenantId The Openstack tenant to look in for the network + * @param cloudSiteId The cloud identifier (may be a region) in which to query the network. + * @return a NetworkInfo object describing the queried network, or null if not found + * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception + * @throws MsoCloudSiteNotFound + */ + public NetworkInfo queryNetwork(String networkNameOrId, String tenantId, String cloudSiteId) throws MsoException + { + LOGGER.debug("In queryNetwork"); + + // Obtain the cloud site information + CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow( + () -> new MsoCloudSiteNotFound(cloudSiteId)); + + Quantum neutronClient = getNeutronClient (cloudSite, tenantId); + + // Check if the network exists and return its info + try { + Network network = findNetworkByNameOrId (neutronClient, networkNameOrId); + if (network == null) { + LOGGER.debug ("Query Network: " + networkNameOrId + " not found in tenant " + tenantId); + return null; + } + return new NetworkInfoMapper(network).map(); + } + catch (OpenStackBaseException e) { + // Convert Neutron exception to an MsoOpenstackException + MsoException me = neutronExceptionToMsoException (e, "QueryNetwork"); + throw me; + } + catch (RuntimeException e) { + // Catch-all + MsoException me = runtimeExceptionToMsoException(e, "QueryNetwork"); + throw me; + } + } + + /** + * Delete the specified Network (by ID) in the given cloud. + * If the network does not exist, success is returned. + * <p> + * @param networkId Openstack ID of the network to delete + * @param tenantId The Openstack tenant. + * @param cloudSiteId The cloud identifier (may be a region) from which to delete the network. + * @return true if the network was deleted, false if the network did not exist + * @throws MsoOpenstackException If the Openstack API call returns an exception, this local + * exception will be thrown. + * @throws MsoCloudSiteNotFound + */ + public boolean deleteNetwork(String networkId, String tenantId, String cloudSiteId) throws MsoException + { + // Obtain the cloud site information where we will create the stack + CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow( + () -> new MsoCloudSiteNotFound(cloudSiteId)); + Quantum neutronClient = getNeutronClient (cloudSite, tenantId); + + try { + // Check that the network exists. + Network network = findNetworkById (neutronClient, networkId); + if (network == null) { + LOGGER.info(MessageEnum.RA_DELETE_NETWORK_EXC, networkId, cloudSiteId, tenantId, "Openstack", ""); + return false; + } + + OpenStackRequest<Void> request = neutronClient.networks().delete(network.getId()); + executeAndRecordOpenstackRequest(request); + + LOGGER.debug ("Deleted Network " + network.getId() + " (" + network.getName() + ")"); + } + catch (OpenStackBaseException e) { + // Convert Neutron exception to an MsoOpenstackException + MsoException me = neutronExceptionToMsoException (e, "Delete Network"); + throw me; + } + catch (RuntimeException e) { + // Catch-all + MsoException me = runtimeExceptionToMsoException(e, "DeleteNetwork"); + throw me; + } + + return true; + } + + + /** + * Update a network with the specified parameters in the given cloud/tenant. + * + * Specifically, this call is intended to update the VLAN segments on a + * multi-provider network. The provider segments will be replaced with the + * supplied list of VLANs. + * <p> + * Note that updating the 'segments' array is not normally supported by Neutron. + * This method relies on a Platform Orchestration extension (using SDN controller + * to manage the virtual networking). + * + * @param cloudSiteId The cloud site ID (may be a region) in which to update the network. + * @param tenantId Openstack ID of the tenant in which to update the network + * @param networkId The unique Openstack ID of the network to be updated + * @param type The network type (Basic, Provider, Multi-Provider) + * @param provider The provider network name. This should not change. + * @param vlans The list of VLAN segments to replace + * @return a NetworkInfo object which describes the updated network + * @throws MsoNetworkNotFound Thrown if the requested network does not exist + * @throws MsoOpenstackException Thrown if the Openstack API call returns an exception + * @throws MsoCloudSiteNotFound + */ + public NetworkInfo updateNetwork (String cloudSiteId, String tenantId, String networkId, NetworkType type, String provider, List<Integer> vlans) + throws MsoException + { + // Obtain the cloud site information where we will create the stack + CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow( + () -> new MsoCloudSiteNotFound(cloudSiteId)); + Quantum neutronClient = getNeutronClient (cloudSite, tenantId); + + // Check that the network exists + Network network = findNetworkById (neutronClient, networkId); + + if (network == null) { + // Network not found. Throw an exception + LOGGER.error(MessageEnum.RA_NETWORK_NOT_FOUND, networkId, cloudSiteId, tenantId, "Openstack", "", MsoLogger.ErrorCode.DataError, "Network not found"); + throw new MsoNetworkNotFound (networkId, tenantId, cloudSiteId); + } + + // Overwrite the properties to be updated + if (type == NetworkType.PROVIDER) { + if (provider != null && vlans != null && vlans.size() > 0) { + network.setProviderPhysicalNetwork (provider); + network.setProviderNetworkType("vlan"); + network.setProviderSegmentationId (vlans.get(0)); + } + } else if (type == NetworkType.MULTI_PROVIDER) { + if (provider != null && vlans != null && vlans.size() > 0) { + List<Segment> segments = new ArrayList<>(vlans.size()); + for (int vlan : vlans) { + Segment segment = new Segment(); + segment.setProviderPhysicalNetwork (provider); + segment.setProviderNetworkType("vlan"); + segment.setProviderSegmentationId (vlan); + + segments.add(segment); + } + network.setSegments(segments); + } + } + + try { + OpenStackRequest<Network> request = neutronClient.networks().update(network); + Network newNetwork = executeAndRecordOpenstackRequest(request); + return new NetworkInfoMapper(newNetwork).map(); + } + catch (OpenStackBaseException e) { + // Convert Neutron exception to an MsoOpenstackException + MsoException me = neutronExceptionToMsoException (e, "UpdateNetwork"); + throw me; + } + catch (RuntimeException e) { + // Catch-all + MsoException me = runtimeExceptionToMsoException(e, "UpdateNetwork"); + throw me; + } + } + + + // ------------------------------------------------------------------- + // PRIVATE UTILITY FUNCTIONS FOR USE WITHIN THIS CLASS + + /** + * Get a Neutron (Quantum) client for the Openstack Network service. + * This requires a 'member'-level userId + password, which will be retrieved from + * properties based on the specified cloud Id. The tenant in which to operate + * must also be provided. + * <p> + * On successful authentication, the Quantum object will be cached for the + * tenantID + cloudId so that it can be reused without reauthenticating with + * Openstack every time. + * + * @param cloudSite - a cloud site definition + * @param tenantId - Openstack tenant ID + * @return an authenticated Quantum object + */ + private Quantum getNeutronClient(CloudSite cloudSite, String tenantId) throws MsoException + { + String cloudId = cloudSite.getId(); + + // Check first in the cache of previously authorized clients + String cacheKey = cloudId + ":" + tenantId; + if (neutronClientCache.containsKey(cacheKey)) { + if (! neutronClientCache.get(cacheKey).isExpired()) { + LOGGER.debug ("Using Cached HEAT Client for " + cacheKey); + NeutronCacheEntry cacheEntry = neutronClientCache.get(cacheKey); + Quantum neutronClient = new Quantum(cacheEntry.getNeutronUrl()); + neutronClient.token(cacheEntry.getToken()); + return neutronClient; + } + else { + // Token is expired. Remove it from cache. + neutronClientCache.remove(cacheKey); + LOGGER.debug ("Expired Cached Neutron Client for " + cacheKey); + } + } + + // Obtain an MSO token for the tenant from the identity service + CloudIdentity cloudIdentity = cloudConfig.getIdentityService(cloudSite.getIdentityServiceId()); + MsoTenantUtils tenantUtils = tenantUtilsFactory.getTenantUtilsByServerType(cloudIdentity.getIdentityServerType()); + final String keystoneUrl = tenantUtils.getKeystoneUrl(cloudId, cloudIdentity); + Keystone keystoneTenantClient = new Keystone(keystoneUrl); + Access access = null; + try { + Authentication credentials = authenticationMethodFactory.getAuthenticationFor(cloudIdentity); + OpenStackRequest<Access> request = keystoneTenantClient.tokens().authenticate(credentials).withTenantId(tenantId); + access = executeAndRecordOpenstackRequest(request); + } + catch (OpenStackResponseException e) { + if (e.getStatus() == 401) { + // Authentication error. + String error = "Authentication Failure: tenant=" + tenantId + ",cloud=" + cloudIdentity.getId(); + alarmLogger .sendAlarm("MsoAuthenticationError", MsoAlarmLogger.CRITICAL, error); + throw new MsoAdapterException(error); + } + else { + MsoException me = keystoneErrorToMsoException(e, "TokenAuth"); + throw me; + } + } + catch (OpenStackConnectException e) { + // Connection to Openstack failed + MsoIOException me = new MsoIOException (e.getMessage(), e); + me.addContext("TokenAuth"); + throw me; + } + catch (RuntimeException e) { + // Catch-all + MsoException me = runtimeExceptionToMsoException(e, "TokenAuth"); + throw me; + } + + String region = cloudSite.getRegionId(); + String neutronUrl = null; + try { + neutronUrl = KeystoneUtils.findEndpointURL(access.getServiceCatalog(), "network", region, "public"); + if (! neutronUrl.endsWith("/")) { + neutronUrl += "/v2.0/"; + } + } catch (RuntimeException e) { + // This comes back for not found (probably an incorrect region ID) + String error = "Network service not found: region=" + region + ",cloud=" + cloudIdentity.getId(); + alarmLogger.sendAlarm("MsoConfigurationError", MsoAlarmLogger.CRITICAL, error); + throw new MsoAdapterException (error, e); + } + + Quantum neutronClient = new Quantum(neutronUrl); + neutronClient.token(access.getToken().getId()); + + neutronClientCache.put(cacheKey, new NeutronCacheEntry(neutronUrl, access.getToken().getId(), access.getToken().getExpires())); + LOGGER.debug ("Caching Neutron Client for " + cacheKey); + + return neutronClient; + } + + /** + * Forcibly expire a Neutron client from the cache. This call is for use by + * the KeystoneClient in case where a tenant is deleted. In that case, + * all cached credentials must be purged so that fresh authentication is + * done on subsequent calls. + * <p> + * @param tenantName + * @param cloudId + */ + public void expireNeutronClient (String tenantId, String cloudId) { + String cacheKey = cloudId + ":" + tenantId; + if (neutronClientCache.containsKey(cacheKey)) { + neutronClientCache.remove(cacheKey); + LOGGER.debug ("Deleted Cached Neutron Client for " + cacheKey); + } + } + + + /* + * Find a tenant (or query its existence) by its Name or Id. Check first against the + * ID. If that fails, then try by name. + * + * @param adminClient an authenticated Keystone object + * @param tenantName the tenant name or ID to query + * @return a Tenant object or null if not found + */ + public Network findNetworkByNameOrId (Quantum neutronClient, String networkNameOrId) + { + if (networkNameOrId == null) { + return null; + } + + Network network = findNetworkById(neutronClient, networkNameOrId); + + if (network == null) { + network = findNetworkByName(neutronClient, networkNameOrId); + } + + return network; + } + + /* + * Find a network (or query its existence) by its Id. + * + * @param neutronClient an authenticated Quantum object + * @param networkId the network ID to query + * @return a Network object or null if not found + */ + private Network findNetworkById (Quantum neutronClient, String networkId) + { + if (networkId == null) { + return null; + } + + try { + OpenStackRequest<Network> request = neutronClient.networks().show(networkId); + Network network = executeAndRecordOpenstackRequest(request); + return network; + } + catch (OpenStackResponseException e) { + if (e.getStatus() == 404) { + return null; + } else { + LOGGER.error (MessageEnum.RA_CONNECTION_EXCEPTION, "OpenStack", "Openstack Error, GET Network By ID (" + networkId + "): " + e, "Openstack", "", MsoLogger.ErrorCode.DataError, "Exception in Openstack"); + throw e; + } + } + } + + /* + * Find a network (or query its existence) by its Name. This method avoids an + * initial lookup by ID when it's known that we have the network Name. + * + * Neutron does not support 'name=*' query parameter for Network query (show). + * The only way to query by name is to retrieve all networks and look for the + * match. While inefficient, this capability will be provided as it is needed + * by MSO, but should be avoided in favor of ID whenever possible. + * + * TODO: + * Network names are not required to be unique, though MSO will attempt to enforce + * uniqueness. This call probably needs to return an error (instead of returning + * the first match). + * + * @param neutronClient an authenticated Quantum object + * @param networkName the network name to query + * @return a Network object or null if not found + */ + public Network findNetworkByName (Quantum neutronClient, String networkName) + { + if (networkName == null) { + return null; + } + + try { + OpenStackRequest<Networks> request = neutronClient.networks().list(); + Networks networks = executeAndRecordOpenstackRequest(request); + for (Network network : networks.getList()) { + if (network.getName().equals(networkName)) { + LOGGER.debug ("Found match on network name: " + networkName); + return network; + } + } + LOGGER.debug ("findNetworkByName - no match found for " + networkName); + return null; + } + catch (OpenStackResponseException e) { + if (e.getStatus() == 404) { + return null; + } else { + LOGGER.error (MessageEnum.RA_CONNECTION_EXCEPTION, "OpenStack", "Openstack Error, GET Network By Name (" + networkName + "): " + e, "OpenStack", "", MsoLogger.ErrorCode.DataError, "Exception in OpenStack"); + throw e; + } + } + } +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoTenantUtils.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoTenantUtils.java new file mode 100644 index 0000000000..28911bc45c --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoTenantUtils.java @@ -0,0 +1,58 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.openstack.utils; + + +import java.util.Map; + +import org.onap.so.cloud.CloudConfig; +import org.onap.so.cloud.CloudIdentity; +import org.onap.so.logger.MsoLogger; +import org.onap.so.openstack.beans.MsoTenant; +import org.onap.so.openstack.exceptions.MsoCloudSiteNotFound; +import org.onap.so.openstack.exceptions.MsoException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public abstract class MsoTenantUtils extends MsoCommonUtils { + + protected static MsoLogger LOGGER = MsoLogger.getMsoLogger (MsoLogger.Catalog.RA, MsoTenantUtils.class); + + @Autowired + protected CloudConfig cloudConfig; + + public abstract String createTenant (String tenantName, String cloudSiteId, Map <String, String> metadata, boolean backout) + throws MsoException; + + public abstract MsoTenant queryTenant (String tenantId, String cloudSiteId) + throws MsoException, MsoCloudSiteNotFound; + + public abstract MsoTenant queryTenantByName (String tenantName, String cloudSiteId) + throws MsoException, MsoCloudSiteNotFound; + + public abstract boolean deleteTenant (String tenantId, String cloudSiteId) + throws MsoException; + + public abstract String getKeystoneUrl (String regionId, CloudIdentity cloudIdentity) + throws MsoException; + +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoTenantUtilsFactory.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoTenantUtilsFactory.java new file mode 100644 index 0000000000..68d0ef2fad --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoTenantUtilsFactory.java @@ -0,0 +1,56 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.openstack.utils; + +import org.onap.so.cloud.CloudConfig; +import org.onap.so.cloud.CloudSite; +import org.onap.so.cloud.ServerType; +import org.onap.so.logger.MsoLogger; +import org.onap.so.openstack.exceptions.MsoCloudSiteNotFound; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class MsoTenantUtilsFactory { + + protected static MsoLogger LOGGER = MsoLogger.getMsoLogger(MsoLogger.Catalog.RA, MsoTenantUtilsFactory.class); + @Autowired + protected CloudConfig cloudConfig; + @Autowired + protected MsoKeystoneUtils keystoneUtils; + + // based on Cloud IdentityServerType returns ORM or KEYSTONE Utils + public MsoTenantUtils getTenantUtils(String cloudSiteId) throws MsoCloudSiteNotFound { + CloudSite cloudSite = cloudConfig.getCloudSite(cloudSiteId).orElseThrow( + () -> new MsoCloudSiteNotFound(cloudSiteId)); + + return getTenantUtilsByServerType(cloudConfig.getIdentityService(cloudSite.getIdentityServiceId()).getIdentityServerType()); + } + + public MsoTenantUtils getTenantUtilsByServerType(ServerType serverType) { + + MsoTenantUtils tenantU = null; + if (ServerType.KEYSTONE.equals(serverType)) { + tenantU = keystoneUtils; + } + return tenantU; + } +} diff --git a/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoYamlEditorWithEnvt.java b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoYamlEditorWithEnvt.java new file mode 100644 index 0000000000..649eb6b07c --- /dev/null +++ b/adapters/mso-adapter-utils/src/main/java/org/onap/so/openstack/utils/MsoYamlEditorWithEnvt.java @@ -0,0 +1,163 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP - SO + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * Copyright (C) 2017 Huawei Technologies Co., Ltd. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.so.openstack.utils; + + + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.onap.so.db.catalog.beans.HeatTemplateParam; +import org.onap.so.logger.MsoLogger; +import org.yaml.snakeyaml.Yaml; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class MsoYamlEditorWithEnvt { + + private static final MsoLogger LOGGER = MsoLogger.getMsoLogger (MsoLogger.Catalog.RA, MsoYamlEditorWithEnvt.class); + + private Map <String, Object> yml; + private Yaml yaml = new Yaml (); + private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); + + public MsoYamlEditorWithEnvt() { + super(); + } + public MsoYamlEditorWithEnvt(byte[] b) { + init(b); + } + + @SuppressWarnings("unchecked") + private synchronized void init (byte[] body) { + InputStream input = new ByteArrayInputStream (body); + yml = (Map <String, Object>) yaml.load (input); + } + + @SuppressWarnings("unchecked") + public synchronized Set <MsoHeatEnvironmentParameter> getParameterListFromEnvt() { + // In an environment entry, the parameters section can only contain the name:value - + // not other attributes. + Set <MsoHeatEnvironmentParameter> paramSet = new HashSet<>(); + Map<String, Object> resourceMap = null; + try { + resourceMap = (Map<String,Object>) yml.get("parameters"); + } catch (Exception e) { + LOGGER.debug("Exception:", e); + return paramSet; + } + if (resourceMap == null) { + return paramSet; + } + + for (Entry<String, Object> stringObjectEntry : resourceMap.entrySet()) { + MsoHeatEnvironmentParameter hep = new MsoHeatEnvironmentParameter(); + Entry<String, Object> pair = stringObjectEntry; + String value; + Object obj = pair.getValue(); + if (obj instanceof String) { + value = yaml.dump(obj); + // but this adds an extra '\n' at the end - which won't hurt - but we don't need it + value = value.substring(0, value.length() - 1); + } else if (obj instanceof LinkedHashMap) { + //Handle that it's json + try { + value = JSON_MAPPER.writeValueAsString(obj); + } catch (Exception e) { + LOGGER.debug("Exception:", e); + value = "_BAD_JSON_MAPPING"; + } + } else { + //this handles integers/longs/floats/etc. + value = String.valueOf(obj); + } + hep.setName((String) pair.getKey()); + hep.setValue(value); + paramSet.add(hep); + } + return paramSet; + } + public synchronized Set <MsoHeatEnvironmentResource> getResourceListFromEnvt() { + try { + Set<MsoHeatEnvironmentResource> resourceList = new HashSet<>(); + @SuppressWarnings("unchecked") + Map<String, Object> resourceMap = (Map<String,Object>) yml.get("resource_registry"); + + for (Entry<String, Object> stringObjectEntry : resourceMap.entrySet()) { + MsoHeatEnvironmentResource her = new MsoHeatEnvironmentResource(); + Entry<String, Object> pair = stringObjectEntry; + her.setName((String) pair.getKey()); + her.setValue((String) pair.getValue()); + resourceList.add(her); + } + return resourceList; + } catch (Exception e) { + LOGGER.debug("Exception:", e); + } + return null; + } + public synchronized Set <HeatTemplateParam> getParameterList () { + Set <HeatTemplateParam> paramSet = new HashSet <> (); + @SuppressWarnings("unchecked") + Map <String, Object> resourceMap = (Map <String, Object>) yml.get ("parameters"); + + for (Entry<String, Object> stringObjectEntry : resourceMap.entrySet()) { + HeatTemplateParam param = new HeatTemplateParam(); + Entry<String, Object> pair = stringObjectEntry; + @SuppressWarnings("unchecked") + Map<String, String> resourceEntry = (Map<String, String>) pair.getValue(); + String value = null; + try { + value = resourceEntry.get("default"); + } catch (ClassCastException cce) { + LOGGER.debug("Exception:", cce); + // This exception only - the value is an integer. For what we're doing + // here - we don't care - so set value to something - and it will + // get marked as not being required - which is correct. + //System.out.println("cce exception!"); + value = "300"; + // okay + } + param.setParamName((String) pair.getKey()); + if (value != null) { + param.setRequired(false); + } else { + param.setRequired(true); + } + value = resourceEntry.get("type"); + param.setParamType(value); + + paramSet.add(param); + + } + return paramSet; + + } + + +} |