/*- * ============LICENSE_START======================================================= * openECOMP : APP-C * ================================================================================ * 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.openecomp.appc.adapter.iaas.impl; import java.net.NoRouteToHostException; import java.net.SocketException; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import com.att.cdp.exceptions.ContextConnectionException; import com.att.cdp.exceptions.ZoneException; import com.att.cdp.openstack.util.ExceptionMapper; import com.att.cdp.pal.util.Time; import com.att.cdp.zones.ContextFactory; import com.att.cdp.zones.spi.AbstractService; import com.att.cdp.zones.spi.RequestState; import com.att.cdp.zones.spi.AbstractService.State; import com.sun.jersey.api.client.ClientHandlerException; import com.woorea.openstack.base.client.OpenStackBaseException; import com.woorea.openstack.base.client.OpenStackClientConnector; import com.woorea.openstack.base.client.OpenStackResponseException; import com.woorea.openstack.base.client.OpenStackSimpleTokenProvider; import com.woorea.openstack.keystone.Keystone; import com.woorea.openstack.keystone.api.TokensResource; import com.woorea.openstack.keystone.model.Access; import com.woorea.openstack.keystone.model.Access.Service; import com.woorea.openstack.keystone.model.Access.Service.Endpoint; import com.woorea.openstack.keystone.model.Authentication; import com.woorea.openstack.keystone.model.Tenant; import com.woorea.openstack.keystone.model.authentication.UsernamePassword; /** * This class is used to capture and cache the service catalog for a specific OpenStack provider. *

* This is needed because the way the servers are represented in the ECOMP product is as their fully qualified URL's. * This is very problematic, because we cant identify their region from the URL, URL's change, and we cant identify the * versions of the service implementations. In otherwords, the URL does not provide us enough information. *

*

* The zone abstraction layer is designed to detect the versions of the services dynamically, and step up or down to * match those reported versions. In order to do that, we need to know before hand what region we are accessing (since * the supported versions may be different by regions). We will need to authenticate to the identity service in order to * do this, plus we have to duplicate the code supporting proxies and trusted hosts that exists in the abstraction * layer, but that cant be helped. *

*

* What we do to circumvent this is connect to the provider using the lowest supported identity api, and read the entire * service catalog into this object. Then, we parse the vm URL to extract the host and port and match that to the * compute services defined in the catalog. When we find a compute service that has the same host name and port, * whatever region that service is supporting is the region for that server. *

*

* While we really only need to do this for compute nodes, there is no telling what other situations may arise where the * full service catalog may be needed. Also, there is very little additional cost (additional RAM) associated with * caching the full service catalog since there is no way to list only a portion of it. *

*/ public class ServiceCatalog { /** * The service name for the compute service endpoint */ public static final String COMPUTE_SERVICE = "compute"; //$NON-NLS-1$ /** * The service name for the identity service endpoint */ public static final String IDENTITY_SERVICE = "identity"; //$NON-NLS-1$ /** * The service name for the compute service endpoint */ public static final String IMAGE_SERVICE = "image"; //$NON-NLS-1$ /** * The service name for the network service endpoint */ public static final String NETWORK_SERVICE = "network"; //$NON-NLS-1$ /** * The service name for the orchestration service endpoint */ public static final String ORCHESTRATION_SERVICE = "orchestration"; //$NON-NLS-1$ /** * The service name for the volume service endpoint */ public static final String VOLUME_SERVICE = "volume"; //$NON-NLS-1$ /** * The service name for the persistent object service endpoint */ public static final String OBJECT_SERVICE = "object-store"; //$NON-NLS-1$ /** * The service name for the metering service endpoint */ public static final String METERING_SERVICE = "metering"; //$NON-NLS-1$ /** * The Openstack Access object that manages the authenticated token and access control */ private Access access; /** * The time (local) that the token expires and we need to re-authenticate */ @SuppressWarnings("unused") private long expiresLocal; /** * The set of all regions that have been defined */ private Set regions; /** * The read/write lock used to protect the cache contents */ private ReadWriteLock rwLock; /** * A map of endpoints for each service organized by service type */ private Map> serviceEndpoints; /** * A map of service types that are published */ private Map serviceTypes; /** * The tenant that we are accessing */ private Tenant tenant; /** * A "token provider" that manages the authentication token that we obtain when logging in */ private OpenStackSimpleTokenProvider tokenProvider; public static final String CLIENT_CONNECTOR_CLASS = "com.woorea.openstack.connector.JerseyConnector"; /** * Create the ServiceCatalog cache and load it from the specified provider * * @param identityURL * The identity service URL to connect to * @param tenantIdentifier * The name or id of the tenant to authenticate with. If the ID is a UUID format (32-character * hexadecimal string), then the authentication is done using the tenant ID, otherwise it is done using * the name. * @param principal * The user id to authenticate to the provider * @param credential * The password to authenticate to the provider * @param properties * Additional properties used to configure the connection, such as proxy and trusted hosts lists * @throws ZoneException * @throws ClassNotFoundException * @throws IllegalAccessException * @throws InstantiationException */ public ServiceCatalog(String identityURL, String tenantIdentifier, String principal, String credential, Properties properties) throws ZoneException { rwLock = new ReentrantReadWriteLock(); serviceTypes = new HashMap<>(); serviceEndpoints = new HashMap<>(); regions = new HashSet<>(); Class connectorClass; OpenStackClientConnector connector; try { connectorClass = Class.forName(CLIENT_CONNECTOR_CLASS); connector = (OpenStackClientConnector) connectorClass.newInstance(); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { e.printStackTrace(); return; } Keystone keystone = new Keystone(identityURL, connector); String proxyHost = properties.getProperty(ContextFactory.PROPERTY_PROXY_HOST); String proxyPort = properties.getProperty(ContextFactory.PROPERTY_PROXY_PORT); String trustedHosts = properties.getProperty(ContextFactory.PROPERTY_TRUSTED_HOSTS, ""); //$NON-NLS-1$ if (proxyHost != null && proxyHost.length() > 0) { keystone.getProperties().setProperty(com.woorea.openstack.common.client.Constants.PROXY_HOST, proxyHost); keystone.getProperties().setProperty(com.woorea.openstack.common.client.Constants.PROXY_PORT, proxyPort); } if (trustedHosts != null) { keystone.getProperties().setProperty(com.woorea.openstack.common.client.Constants.TRUST_HOST_LIST, trustedHosts); } Authentication authentication = new UsernamePassword(principal, credential); TokensResource tokens = keystone.tokens(); TokensResource.Authenticate authenticate = tokens.authenticate(authentication); if (tenantIdentifier.length() == 32 && tenantIdentifier.matches("[0-9a-fA-F]+")) { //$NON-NLS-1$ authenticate = authenticate.withTenantId(tenantIdentifier); } else { authenticate = authenticate.withTenantName(tenantIdentifier); } /* * We have to set up the TrackRequest TLS collection for the ExceptionMapper */ trackRequest(); RequestState.put(RequestState.PROVIDER, "OpenStackProvider"); RequestState.put(RequestState.TENANT, tenantIdentifier); RequestState.put(RequestState.PRINCIPAL, principal); try { access = authenticate.execute(); expiresLocal = getLocalExpiration(access); tenant = access.getToken().getTenant(); tokenProvider = new OpenStackSimpleTokenProvider(access.getToken().getId()); keystone.setTokenProvider(tokenProvider); parseServiceCatalog(access.getServiceCatalog()); } catch (OpenStackBaseException e) { ExceptionMapper.mapException(e); } catch (Exception ex) { throw new ContextConnectionException(ex.getMessage()); } } /** * Returns the list of service endpoints for the published service type * * @param serviceType * The service type to obtain the endpoints for * @return The list of endpoints for the service type, or null if none exist */ public List getEndpoints(String serviceType) { Lock readLock = rwLock.readLock(); readLock.lock(); try { return serviceEndpoints.get(serviceType); } finally { readLock.unlock(); } } /** * Computes the local time when the access token will expire, after which we will need to re-login to access the * provider. * * @param accessKey * The access key used to access the provider * @return The local time the key expires */ private static long getLocalExpiration(Access accessKey) { Date now = Time.getCurrentUTCDate(); if (accessKey != null && accessKey.getToken() != null) { Calendar issued = accessKey.getToken().getIssued_at(); Calendar expires = accessKey.getToken().getExpires(); if (issued != null && expires != null) { long tokenLife = expires.getTimeInMillis() - issued.getTimeInMillis(); return now.getTime() + tokenLife; } } return now.getTime(); } /** * @return The set of all regions that are defined */ public Set getRegions() { Lock readLock = rwLock.readLock(); readLock.lock(); try { return regions; } finally { readLock.unlock(); } } /** * @return A list of service types that are published */ public List getServiceTypes() { Lock readLock = rwLock.readLock(); readLock.lock(); try { ArrayList result = new ArrayList<>(); result.addAll(serviceTypes.keySet()); return result; } finally { readLock.unlock(); } } /** * @return The tenant id */ public String getTenantId() { Lock readLock = rwLock.readLock(); readLock.lock(); try { return tenant.getId(); } finally { readLock.unlock(); } } /** * @return The tenant name */ public String getTenantName() { Lock readLock = rwLock.readLock(); readLock.lock(); try { return tenant.getName(); } finally { readLock.unlock(); } } /** * Returns an indication if the specified service type is published by this provider * * @param serviceType * The service type to check for * @return True if a service of that type is published */ public boolean isServicePublished(String serviceType) { Lock readLock = rwLock.readLock(); readLock.lock(); try { return serviceTypes.containsKey(serviceType); } finally { readLock.unlock(); } } /** * Parses the service catalog and caches the results * * @param services * The list of services published by this provider */ private void parseServiceCatalog(List services) { Lock lock = rwLock.writeLock(); lock.lock(); try { serviceTypes.clear(); serviceEndpoints.clear(); regions.clear(); for (Service service : services) { String type = service.getType(); serviceTypes.put(type, service); List endpoints = service.getEndpoints(); for (Service.Endpoint endpoint : endpoints) { List endpointList = serviceEndpoints.get(type); if (endpointList == null) { endpointList = new ArrayList<>(); serviceEndpoints.put(type, endpointList); } endpointList.add(endpoint); String region = endpoint.getRegion(); if (!regions.contains(region)) { regions.add(region); } } } } finally { lock.unlock(); } } /** * This method is used to provide a diagnostic listing of the service catalog * * @see java.lang.Object#toString() */ @Override public String toString() { StringBuilder builder = new StringBuilder(); Lock lock = rwLock.readLock(); lock.lock(); try { builder.append(String.format("Service Catalog: tenant %s, id[%s], description[%s]\n", tenant.getName(), //$NON-NLS-1$ tenant.getId(), tenant.getDescription())); if (regions != null && !regions.isEmpty()) { builder.append(String.format("%d regions:\n", regions.size())); //$NON-NLS-1$ for (String region : regions) { builder.append("\t" + region + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ } } builder.append(String.format("%d services:\n", serviceEndpoints.size())); //$NON-NLS-1$ for (String serviceType : serviceEndpoints.keySet()) { List endpoints = serviceEndpoints.get(serviceType); Service service = serviceTypes.get(serviceType); builder.append(String.format("\t%s [%s] - %d endpoints\n", service.getType(), service.getName(), //$NON-NLS-1$ endpoints.size())); for (Endpoint endpoint : endpoints) { builder.append(String.format("\t\tRegion [%s], public URL [%s]\n", endpoint.getRegion(), //$NON-NLS-1$ endpoint.getPublicURL())); } } } finally { lock.unlock(); } return builder.toString(); } /** * Initializes the request state for the current requested service. *

* This method is used to track requests made to the various service implementations and to provide additional * information for diagnostic purposes. The RequestState class stores the state in thread-local storage * and is available to all code on that thread. *

*

* This method first obtains the stack trace and scans the stack backward for the call to this method. It then backs * up one more call and assumes that method is the request that we are "tracking". *

* * @param states * A variable argument list of additional state values that the caller wants to add to the request state * thread-local object to track the context. */ protected void trackRequest(State... states) { RequestState.clear(); for (State state : states) { RequestState.put(state.getName(), state.getValue()); } Thread currentThread = Thread.currentThread(); StackTraceElement[] stack = currentThread.getStackTrace(); if (stack != null && stack.length > 0) { int index = 0; StackTraceElement element = null; for (; index < stack.length; index++) { element = stack[index]; if ("trackRequest".equals(element.getMethodName())) { //$NON-NLS-1$ break; } } index++; if (index < stack.length) { element = stack[index]; RequestState.put(RequestState.METHOD, element.getMethodName()); RequestState.put(RequestState.CLASS, element.getClassName()); RequestState.put(RequestState.LINE_NUMBER, Integer.toString(element.getLineNumber())); RequestState.put(RequestState.THREAD, currentThread.getName()); // RequestState.put(RequestState.PROVIDER, context.getProvider().getName()); // RequestState.put(RequestState.TENANT, context.getTenantName()); // RequestState.put(RequestState.PRINCIPAL, context.getPrincipal()); } } } }