/*-
* ============LICENSE_START=======================================================
* ONAP - SO
* ================================================================================
* Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
* ================================================================================
* Modifications Copyright (c) 2019 Samsung
* ================================================================================
* Modifications Copyright (c) 2020 Nokia
* ================================================================================
* 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.HashMap;
import java.util.Map;
import java.util.Optional;
import org.onap.so.db.catalog.beans.CloudIdentity;
import org.onap.so.db.catalog.beans.CloudSite;
import org.onap.logging.filter.base.ErrorCode;
import org.onap.so.logger.MessageEnum;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 {
public static final String DELETE_TENANT = "Delete Tenant";
private static final Logger LOGGER = LoggerFactory.getLogger(MsoKeystoneUtils.class);
/**
* 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.
*
* 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.
*
*
* @param tenantName The tenant name to create
* @param cloudSiteId 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 metadata, boolean backout)
throws MsoException {
// Obtain the cloud site information where we will create the tenant
Optional cloudSiteOpt = cloudConfig.getCloudSite(cloudSiteId);
if (!cloudSiteOpt.isPresent()) {
LOGGER.error("{} MSOCloudSite {} not found {} ", MessageEnum.RA_CREATE_TENANT_ERR, cloudSiteId,
ErrorCode.DataError.getValue());
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("{} Tenant name {} already exists on Cloud site id {}, {}",
MessageEnum.RA_TENANT_ALREADY_EXIST, tenantName, cloudSiteId, ErrorCode.DataError.getValue());
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 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 = cloudSiteOpt.get().getIdentityService();
User msoUser = findUserByNameOrId(keystoneAdminClient, cloudIdentity.getMsoId());
Role memberRole = findRoleByNameOrId(keystoneAdminClient, cloudIdentity.getMemberRole());
if (msoUser != null && memberRole != null) {
OpenStackRequest request =
keystoneAdminClient.tenants().addUser(tenant.getId(), msoUser.getId(), memberRole.getId());
executeAndRecordOpenstackRequest(request);
}
if (cloudIdentity.getTenantMetadata() && metadata != null && !metadata.isEmpty()) {
Metadata tenantMetadata = new Metadata();
tenantMetadata.setMetadata(metadata);
OpenStackRequest 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("{} Create Tenant errored, Tenant deletion suppressed {} ",
MessageEnum.RA_CREATE_TENANT_ERR, ErrorCode.DataError.getValue());
} else {
try {
OpenStackRequest request = keystoneAdminClient.tenants().delete(tenant.getId());
executeAndRecordOpenstackRequest(request);
} catch (Exception e2) {
// Just log this one. We will report the original exception.
LOGGER.error("{} Nested exception rolling back tenant {} ", MessageEnum.RA_CREATE_TENANT_ERR,
ErrorCode.DataError.getValue(), 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.
*
* 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.
*
*
* @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 metadata = new HashMap<>();
if (cloudSite.getIdentityService().getTenantMetadata()) {
OpenStackRequest 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.
*
* 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.
*
*
* @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 metadata = new HashMap<>();
if (cloudSite.getIdentityService().getTenantMetadata()) {
OpenStackRequest 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).
*
* 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.
*
*
* @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("{} Tenant id {} not found on cloud site id {}, {}", MessageEnum.RA_TENANT_NOT_FOUND,
tenantId, cloudSiteId, ErrorCode.DataError.getValue());
return false;
}
OpenStackRequest request = keystoneAdminClient.tenants().delete(tenant.getId());
executeAndRecordOpenstackRequest(request);
LOGGER.debug("Deleted Tenant {} ({})", tenant.getId(), tenant.getName());
} catch (OpenStackBaseException e) {
// Convert Keystone OpenStackResponseException to MsoOpenstackException
throw keystoneErrorToMsoException(e, DELETE_TENANT);
} catch (RuntimeException e) {
// Catch-all
throw runtimeExceptionToMsoException(e, DELETE_TENANT);
}
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).
*
* 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.
*
*
* @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 = 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("{} Tenant {} not found on Cloud site id {}, {}", MessageEnum.RA_TENANT_NOT_FOUND,
tenantName, cloudSiteId, ErrorCode.DataError.getValue());
return false;
}
// Execute the Delete. It has no return value.
OpenStackRequest request = keystoneAdminClient.tenants().delete(tenant.getId());
executeAndRecordOpenstackRequest(request);
LOGGER.debug("Deleted Tenant {} ({})", tenant.getId(), tenant.getName());
} 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, DELETE_TENANT);
} catch (RuntimeException e) {
// Catch-all
throw runtimeExceptionToMsoException(e, DELETE_TENANT);
}
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. 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 = cloudSite.getIdentityService();
String adminTenantName = cloudIdentity.getAdminTenant();
String region = cloudSite.getRegionId();
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 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();
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 {
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();
LOGGER.error("{} Region: {} Cloud identity {} {} Exception in findEndpointURL ",
MessageEnum.IDENTITY_SERVICE_NOT_FOUND, region, cloudIdentity.getId(),
ErrorCode.DataError.getValue(), e);
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);
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 request = adminClient.tenants().show(tenantId);
return executeAndRecordOpenstackRequest(request);
} catch (OpenStackResponseException e) {
if (e.getStatus() == 404) {
return null;
} else {
LOGGER.error("{} {} Openstack Error, GET Tenant by Id ({}): ", MessageEnum.RA_CONNECTION_EXCEPTION,
ErrorCode.DataError.getValue(), tenantId, e);
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 request = adminClient.tenants().show("").queryParam("name", tenantName);
return executeAndRecordOpenstackRequest(request);
} catch (OpenStackResponseException e) {
if (e.getStatus() == 404) {
return null;
} else {
LOGGER.error("{} {} Openstack Error, GET Tenant By Name ({}) ", MessageEnum.RA_CONNECTION_EXCEPTION,
ErrorCode.DataError.getValue(), tenantName, e);
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 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("{} {} Openstack Error, GET User ({}) ", MessageEnum.RA_CONNECTION_EXCEPTION,
ErrorCode.DataError.getValue(), userNameOrId, e);
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 request = adminClient.users().show("").queryParam("name", userName);
return executeAndRecordOpenstackRequest(request);
} catch (OpenStackResponseException e) {
if (e.getStatus() == 404) {
return null;
} else {
LOGGER.error("{} {} Openstack Error, GET User By Name ({}): ", MessageEnum.RA_CONNECTION_EXCEPTION,
ErrorCode.DataError.getValue(), userName, e);
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).
*
* @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 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;
}
@Override
public String getKeystoneUrl(String regionId, CloudIdentity cloudIdentity) throws MsoException {
return cloudIdentity.getIdentityUrl();
}
}