/**
* Copyright (C) 2009-2015 Dell, Inc.
* See annotations for authorship information
*
* ====================================================================
* 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.
* ====================================================================
*/
package org.dasein.cloud;
import org.dasein.cloud.admin.AdminServices;
import org.dasein.cloud.ci.CIServices;
import org.dasein.cloud.compute.ComputeServices;
import org.dasein.cloud.dc.DataCenterServices;
import org.dasein.cloud.identity.IdentityServices;
import org.dasein.cloud.network.NetworkServices;
import org.dasein.cloud.platform.PlatformServices;
import org.dasein.cloud.storage.StorageServices;
import org.dasein.cloud.util.NamingConstraints;
import org.dasein.cloud.util.ResourceNamespace;
import org.dasein.util.CalendarWrapper;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
/**
* <p>
* Represents a provider of cloud services. The cloud provider API prescribes a
* number of services which may or may not be implemented for a given cloud. In addition,
* the cloud provider API describes the data center structure of the underlying cloud through
* the concept of regions. Each provider must have at least one region, which in turn has
* at least one zone or data center.
* </p>
* <p>
* This API specifies a number of services that a given cloud provider may implement, but many
* cloud providers will not actually implement all of them. If a given service is not implemented,
* the API call for gaining access to that services should return <code>null</code>. An application
* should therefore test whether a service is <code>null</code> before trying to trigger operations
* in that cloud.
* </p>
* <p>
* When implementing a given service for a particular provider, that provider may not support
* some of the operations of the service. Such methods should throw an
* {@link org.dasein.cloud.OperationNotSupportedException} to flag the lack of support.
* </p>
* @author George Reese @ enstratius (http://www.enstratius.com)
* @version 2014.03 added findUniqueName() based on logic by Stas (issue #134)
* @since 2010.08
*/
public abstract class CloudProvider {
@SuppressWarnings("UnusedDeclaration")
static private @Nonnull String getLastItem(@Nonnull String name) {
int idx = name.lastIndexOf('.');
if (idx < 0) {
return name;
}
else if (idx == (name.length() - 1)) {
return "";
}
return name.substring(idx + 1);
}
static public boolean matchesTags(@Nonnull Map<String, ?> currentValues, @Nonnull String name, @Nonnull String description, @Nullable Map<String, String> valuesToMatch) {
if (valuesToMatch != null && !valuesToMatch.isEmpty()) {
name = name.toLowerCase();
description = description.toLowerCase();
for (Map.Entry<String, String> entry : valuesToMatch.entrySet()) {
String v = (entry.getValue() == null ? null : entry.getValue().toLowerCase());
Object t = currentValues.get(entry.getKey());
if (entry.getKey().equals("Name")) {
if (v != null) {
String n = name.toLowerCase();
if (n.contains(v) || (t != null && t.toString().toLowerCase().contains(v))) {
continue;
}
}
return false;
}
if (entry.getKey().equals("Description")) {
if (v != null) {
String d = description.toLowerCase();
if (d.contains(v) || (t != null && t.toString().toLowerCase().contains(v))) {
continue;
}
}
return false;
}
if (t == null && v == null) {
continue;
}
if (t == null) {
return false;
}
if (v == null) {
return false;
}
if (!t.toString().contains(v)) {
return false;
}
}
}
return true;
}
private CloudProvider computeCloudProvider;
private ProviderContext context;
private CloudProvider storageCloudProvider;
private boolean debug;
private transient int holdCount = 0;
/**
* Base contructor for a cloud provider.
*/
public CloudProvider() { }
/**
* Empties out all credentials and removes any other context information from the cloud provider
* implementation.
*/
public void close() {
int h;
synchronized (this) {
h = holdCount;
}
if (h > 0) {
Thread t = new Thread() {
public void run() {
waitForHold();
}
};
t.setName("Close Hold for " + this);
t.setDaemon(true);
t.start();
}
else {
waitForHold();
}
}
/**
* Called to initialize a cloud provider with an operational context. The operational context
* includes authentication information, the regional context, and any cloud-specific
* context. Prior to initializing itself, this method will close out any existing state.
* @param context the context for services calls using this provider instance
* @deprecated use {@link org.dasein.cloud.ProviderContext#connect()}
*/
@SuppressWarnings("deprecation")
@Deprecated
public final void connect(@Nonnull ProviderContext context) {
connect(context, null);
}
/**
* Called to initialize a cloud provider with an operational context. The operational context
* includes authentication information, the regional context, and any cloud-specific
* context. Prior to initializing itself, this method will close out any existing state.
* @param context the context for services calls using this provider instance
* @param computeProvider the compute context if this is a storage-only cloud (the compute context controls the connection)
* @deprecated use {@link org.dasein.cloud.ProviderContext#connect(CloudProvider)}
*/
@Deprecated
public final void connect(@Nonnull ProviderContext context, @Nullable CloudProvider computeProvider) {
try {
connect(context, computeProvider, null);
context.configureForDeprecatedConnect(this);
}
catch (CloudException e) {
throw new RuntimeException(e); // can't change the signature for backwards compat reasons
}
catch (InternalException e) {
throw new RuntimeException(e); // can't change the signature for backwards compat reasons
}
}
/**
* Establishes a connected state between the Dasein Cloud implementation and the target cloud. This method should
* not be directly triggered by Dasein Cloud clients, but instead within the Dasein Cloud {@link ProviderContext#connect(CloudProvider)}
* method or the backwards compatible {@link #connect(ProviderContext, CloudProvider)} method. Clients should always
* call {@link org.dasein.cloud.ProviderContext#connect()} or {@link ProviderContext#connect(CloudProvider)}.
* @param context the provider context representing the context for the connection
* @param computeProvider if this connection is to a storage cloud being virtually bound to a compute cloud, the connected provider for the associated compute cloud
* @param myCloud if known, the cloud object behind this provider
* @throws CloudException an error occurred communicating with the target cloud
* @throws InternalException an error occurred within Dasein Cloud while setting up the connection state
*/
@SuppressWarnings("deprecation")
protected void connect(@Nonnull ProviderContext context, @Nullable CloudProvider computeProvider, @Nullable Cloud myCloud) throws CloudException, InternalException {
close();
this.context = context;
if (myCloud == null) {
context.setCloud(this);
}
this.computeCloudProvider = computeProvider;
if (computeProvider != null) {
computeProvider.storageCloudProvider = this;
ProviderContext computeContext = computeProvider.getContext();
if (computeContext != null) {
context.setEffectiveAccountNumber(computeContext.getAccountNumber());
}
}
}
/**
* General purpose method for finding a unique name based upon a desired base name. The name resulting from this
* method is guaranteed to be both valid for objects of its type and unique among objects of those type across
* the appropriate namespace. Unless, of course, the result is <code>null</code>. A <code>null</code> value
* means that no permutation of the base name could result in a valid unique name for these kinds of objects
* in this cloud.
* @param baseName the name that the user would ideally desire for an object to be created
* @param constraints the naming constraints that govern the naming of this kind of object
* @param namespace an implementation of an interface responsible for searching efficiently for the availability of a given name
* @return a valid, unique name based on the desired base name or <code>null</code> if no unique permutation was achievable
* @throws CloudException an error occurred interacting with the cloud provider to find the unique name
* @throws InternalException an internal error occurred calculating a unique name
*/
public @Nullable String findUniqueName(@Nonnull String baseName, @Nonnull NamingConstraints constraints, @Nonnull ResourceNamespace namespace) throws CloudException, InternalException {
if (!constraints.isValidName(baseName)) {
baseName = constraints.convertToValidName(baseName, Locale.getDefault());
if (baseName == null) {
return null;
}
}
if (!namespace.hasNamedItem(baseName)) {
return baseName;
}
String name = baseName;
int i = 1;
while (namespace.hasNamedItem(name)) {
name = constraints.incrementName(baseName, i++);
if (name == null) {
return null;
}
}
return name;
}
public abstract @Nullable AdminServices getAdminServices();
/**
* If this is a pure storage implementation that is being paired with a compute implementation,
* the compute implementation holds precedence. This value references the compute provider
* behind this storage provider
* @return the compute provider (if any) behind this storage provider
*/
public final CloudProvider getComputeCloud() {
return computeCloudProvider;
}
/**
* This value will be null unless {@link #connect(ProviderContext)} has been called.
* @return the operational context for this instance of this provider implementation
*/
public final @Nullable ProviderContext getContext() {
return context;
}
/**
* @return an object containing the fields required for connecting Dasein to the cloud provider
*/
public abstract @Nonnull ContextRequirements getContextRequirements();
/**
* This value can be the same as {@link #getProviderName()} if it is not a multi-cloud provider.
* @return the name of the cloud
*/
public abstract @Nonnull String getCloudName();
/**
* Provides access to the data center services that describe the physical structure of the underlying cloud provider.
* @return an implementation of the {@link org.dasein.cloud.dc.DataCenterServices} API
*/
public abstract @Nonnull DataCenterServices getDataCenterServices();
/**
* Provides access to support for complex topologies managed through converged infrastructure as a cloudy environment.
* @return the services representing converged infrastructure, if any
*/
public abstract @Nullable CIServices getCIServices();
public abstract @Nullable ComputeServices getComputeServices();
public abstract @Nullable IdentityServices getIdentityServices();
public abstract @Nullable NetworkServices getNetworkServices();
public abstract @Nullable PlatformServices getPlatformServices();
/**
* @return the name of this cloud provider
*/
public abstract @Nonnull String getProviderName();
/**
* Provides access to the cloud storage services supported by this cloud provider.
* @return an implementation of the {@link org.dasein.cloud.storage.StorageServices} API
*/
@SuppressWarnings("deprecation")
public synchronized @Nullable StorageServices getStorageServices() {
if (storageCloudProvider != null) {
return storageCloudProvider.getStorageServices();
}
ProviderContext computeContext = getContext();
if (computeContext == null) {
return null;
}
String storage = computeContext.getStorage();
if (storage == null) {
return null;
}
try {
CloudProvider p = (CloudProvider) Class.forName(storage).newInstance();
ProviderContext ctx = new ProviderContext();
Properties props = computeContext.getStorageCustomProperties();
ctx.setRegionId(computeContext.getRegionId());
ctx.setCloudName(computeContext.getCloudName());
ctx.setProviderName(computeContext.getProviderName());
ctx.setEndpoint(computeContext.getStorageEndpoint());
ctx.setAccountNumber(computeContext.getStorageAccountNumber());
ctx.setAccessKeys(computeContext.getStoragePublic(), computeContext.getStoragePrivate());
ctx.setX509Cert(computeContext.getStorageX509Cert());
ctx.setX509Key(computeContext.getStorageX509Key());
ctx.setCustomProperties(props == null ? new Properties() : props);
p.connect(ctx, this);
storageCloudProvider = p;
return p.getStorageServices();
}
catch (Throwable t) {
t.printStackTrace();
return null;
}
}
public boolean hasAdminServices() {
return (getAdminServices() != null);
}
public boolean hasCIServices() {
return (getCIServices() != null);
}
public boolean hasComputeServices() {
return (getComputeServices() != null);
}
public boolean hasIdentityServices() {
return (getIdentityServices() != null);
}
public boolean hasNetworkServices() {
return (getNetworkServices() != null);
}
public boolean hasPlatformServices() {
return (getPlatformServices() != null);
}
public boolean hasStorageServices() {
return (getStorageServices() != null);
}
public void hold() {
if (computeCloudProvider != null) {
computeCloudProvider.hold();
}
else {
synchronized (this) {
holdCount++;
}
}
}
public synchronized boolean isConnected() {
return (context != null);
}
public void release() {
if (computeCloudProvider != null) {
computeCloudProvider.release();
}
else {
synchronized (this) {
holdCount--;
notifyAll();
}
}
}
/**
* Tests the validity of the current context and returns the true account identifier for this context.
* In general, this value will make some kind of connection to the cloud provider using the established
* context to verify the credentials are right and then return {@link ProviderContext#getAccountNumber()}.
* In some cases, however, the actual provider account number may differ from the one visible to the
* user. For those scenarios, the return value will return the true account number and not the one the
* user thinks it is. If the connection fails for any reason, this method should return <code>null</code>
* to indicate the failure.
* @return On success, the true account number for the account. On failure, <code>null</code>.
*/
public String testContext() {
return null;
}
private void waitForHold() {
long timeout = System.currentTimeMillis() + (CalendarWrapper.MINUTE * 20L);
while (timeout > System.currentTimeMillis()) {
synchronized (this) {
if (holdCount < 1) {
break;
}
}
try { Thread.sleep(1000L); }
catch (InterruptedException ignore) { /* ignore this */ }
}
if (context != null) {
context.clear();
context = null;
}
if (storageCloudProvider != null) {
storageCloudProvider.close();
storageCloudProvider = null;
}
}
}