/** * 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 java.io.Serializable; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Random; import javax.annotation.Nonnull; import javax.annotation.Nullable; /** * <p> * The contextual information necessary for making calls to a cloud provider. Each provider requires * a provider context in order to connect to the target cloud account and perform operations within * the cloud. This context includes the account number, operational region, and any number of * authentication keys. * </p> * @author George Reese * @version 2014.03 refactored for discoverability of configuration values and better model enforcement (issue #123) * @since 2010.08 */ public class ProviderContext extends ProviderContextCompat implements Serializable { static private final Random random = new Random(); static public class Value<T> { public String name; public T value; public Value(@Nonnull String name, @Nonnull T value) { this.name = name; this.value = value; } public byte[][] getKeypair() { return (byte[][])value; } public Float getFloat() { if( value instanceof Float ) { return (Float)value; } else if( value instanceof Number ) { return ((Number)value).floatValue(); } else if( value instanceof String ) { return Float.parseFloat((String) value); } else throw new ClassCastException("Not a float: " + value); } public Integer getInt() { if( value instanceof Integer ) { return (Integer)value; } else if( value instanceof Number ) { return ((Number)value).intValue(); } else if( value instanceof String ) { return Integer.parseInt((String) value); } else throw new ClassCastException("Not an integer: " + value); } public byte[] getPassword() { if( value instanceof String ) { try { return ((String)value).getBytes("utf-8"); } catch( UnsupportedEncodingException ignore ) { return (byte[])value; } } return (byte[])value; } public String getText() { if( value instanceof String ) { return (String)value; } else { return value.toString(); } } static public @Nonnull Value<?> parseValue(@Nonnull ContextRequirements.Field field, @Nonnull String ... fromStrings) throws UnsupportedEncodingException { switch( field.type ) { case KEYPAIR: if( fromStrings.length != 2 ) { throw new IndexOutOfBoundsException("Should have exactly 2 strings for a keypair value"); } if( fromStrings[0] == null || fromStrings[1] == null ) { throw new RuntimeException("Keypair values can not be null"); } byte[][] bytes = new byte[2][]; bytes[0] = fromStrings[0].getBytes("utf-8"); bytes[1] = fromStrings[1].getBytes("utf-8"); return new Value<byte[][]>(field.name, bytes); case TEXT: case TOKEN: return new Value<String>(field.name, fromStrings[0]); case INTEGER: return new Value<Integer>(field.name, Integer.parseInt(fromStrings[0])); case FLOAT: return new Value<Float>(field.name, Float.parseFloat(fromStrings[0])); case PASSWORD: return new Value<byte[]>(field.name, fromStrings[0].getBytes("utf-8")); default: throw new RuntimeException("Unsupported type: " + field.type); } } } /** * Helper method for clearing out credentials with random data. * @param keys a list of credentials to fill with random data */ static public void clear(byte[] ... keys) { if( keys != null ) { for( byte[] key : keys ) { if( key != null ) { random.nextBytes(key); } } } } /** * Constructs a provider context from configuration values provided by a client. The preferred mechanism to access * this constructor is via {@link Cloud#createContext(String, String, org.dasein.cloud.ProviderContext.Value...)}. * @param cloud the cloud configuration object to build against * @param accountNumber the account number within the cloud to use for the connection context * @param regionId the ID of the region in which the client will be operating * @param configurationValues one or more configuration values that match the configuration requirements for the cloud * @return an instance of a provider context ready to connect to the target cloud using the specified configuration values */ static ProviderContext getContext(@Nonnull Cloud cloud, @Nonnull String accountNumber, @Nullable String regionId, @Nonnull Value<?> ... configurationValues) { ProviderContext ctx = new ProviderContext(cloud, accountNumber, regionId); Properties p = new Properties(); ctx.configurationValues = new HashMap<String,Object>(); for( Value<?> v : configurationValues ) { ctx.configurationValues.put(v.name, v.value); if( v.value instanceof String ) { p.setProperty(v.name, (String)v.value); } } //noinspection deprecation ctx.setCustomProperties(p); return ctx; } /** * @return a pseudo-random number using the context random number generator (not a secure random) */ @SuppressWarnings("UnusedDeclaration") public static Random getRandom() { return random; } private String accountNumber; private Cloud cloud; private Map<String,Object> configurationValues; private String effectiveAccountNumber; private String regionId; private RequestTrackingStrategy strategy; /** * Constructs a provider context from the provided values * @param cloud the cloud configuration object to build against * @param accountNumber the account number within the cloud to use for the connection context * @param regionId the ID of the region in which the client will be operating */ private ProviderContext(@Nonnull Cloud cloud, @Nonnull String accountNumber, @Nullable String regionId) { this.cloud = cloud; this.accountNumber = accountNumber; this.regionId = regionId; } /** * @return the account number that identifies the account to the cloud provider independent of context */ public @Nonnull String getAccountNumber() { return accountNumber; } @SuppressWarnings("deprecation") void configureForDeprecatedConnect(@Nonnull CloudProvider p) { if( configurationValues == null ) { configurationValues = new HashMap<String, Object>(); ContextRequirements r = p.getContextRequirements(); ContextRequirements.Field access = r.getCompatAccessKeys(); if( access != null ) { byte[] key = getAccessPublic(); if( key != null ) { configurationValues.put(access.name, new byte[][] { key, getAccessPrivate() }); } } ContextRequirements.Field x509 = r.getCompatAccessX509(); if( x509 != null ) { byte[] cert = getX509Cert(); if( cert != null ) { configurationValues.put(x509.name, new byte[][] { cert, getX509Key() }); } } Properties props = getCustomProperties(); for( ContextRequirements.Field field : r.getConfigurableValues() ) { if( (access == null || !access.name.equals(field.name)) && (x509 == null || !x509.name.equals(field.name)) ) { if( props.containsKey(field.name) ) { configurationValues.put(field.name, props.getProperty(field.name)); } } } } } /** * Connects to the cloud associated with this connection context. The result will be a connected implementation * of the {@link CloudProvider} abstract class specific to the cloud in question. * @return a connected cloud provider instance * @throws CloudException an error occurred with any handshake that might have been necessary to perform a connection (generally not needed) * @throws InternalException an error occurred loading the {@link org.dasein.cloud.CloudProvider} implementation */ public @Nonnull CloudProvider connect() throws CloudException, InternalException { return connect(null); } /** * Connects to the cloud associated with this connection context. The result will be a connected implementation * of the {@link CloudProvider} abstract class specific to the cloud in question. This variation exists specifically * for clients trying to use a compute provider together with a storage provider to create an apparently unified cloud. * The approach is to first connect to the compute provider, and then connect to the storage provider using this method. * @param computeProvider the compute provider with which the connected storage provider will be associated * @return a connected cloud provider instance * @throws CloudException an error occurred with any handshake that might have been necessary to perform a connection (generally not needed) * @throws InternalException an error occurred loading the {@link org.dasein.cloud.CloudProvider} implementation */ public @Nonnull CloudProvider connect(@Nullable CloudProvider computeProvider) throws CloudException, InternalException { try { ProviderContext computeContext = null; if( computeProvider != null ) { computeContext = computeProvider.getContext(); if( computeContext == null ) { throw new InternalException("The compute provider has not yet connected to the compute cloud"); } } CloudProvider p = cloud.buildProvider(); p.connect(this, computeProvider, cloud); if( computeContext != null ) { effectiveAccountNumber = computeContext.getAccountNumber(); } return p; } catch( InstantiationException e ) { throw new InternalException(e); } catch( IllegalAccessException e ) { throw new InternalException(e); } } /** * Creates a copy of this context with some parameters replaced by new values. * * @param havingRegionId the region to set into copied context. * @return a new provider context. * @throws InternalException an error occurred loading the {@link org.dasein.cloud.CloudProvider} implementation */ public @Nonnull ProviderContext copy( @Nonnull String havingRegionId ) throws InternalException { try { CloudProvider provider = this.getCloud().buildProvider(); List<ContextRequirements.Field> fields = provider.getContextRequirements().getConfigurableValues(); List<Value<Object>> values = new ArrayList<Value<Object>>(); for( ContextRequirements.Field f : fields ) { Object value = this.getConfigurationValue(f); if( value != null ) { values.add(new Value<Object>(f.name, value)); } } return this.getCloud().createContext(getAccountNumber(), havingRegionId, values.toArray(new Value[values.size()])); } catch( IllegalAccessException e ) { throw new InternalException(e); } catch( InstantiationException e ) { throw new InternalException(e); } } /** * @return the cloud for which this context is configured */ public @Nonnull Cloud getCloud() { return cloud; } /** * Looks through the values provided for configuring this context and returns the named value, if set * @param field the name of the field whose configuration value is sought * @return the value matching the named field or <code>null</code> if no value is set */ public @Nullable Object getConfigurationValue(@Nonnull String field) { return configurationValues.get(field); } /** * Looks through the values provided for configuring this context and returns the matching value, if set * @param field the field from the context requirements whose configuration value is sought * @return the value matching the specified field or <code>null</code> if no value is set */ public @Nullable Object getConfigurationValue(@Nonnull ContextRequirements.Field field) { return getConfigurationValue(field.name); } /** * The effective account number under which this context operates. It is used for defining ownership of * storage assets so they align with compute assets while the {@link #getAccountNumber()} for this context * is used for interaction with the cloud. This value has meaning only in the scenario in which separate compute * and storage clouds are being "glued together" to create a single virtual cloud within Dasein Cloud. Because they * are separate clouds, they will have different account numbers used in connecting to them. But to make the * storage assets look like they are owned by the compute cloud, the effective account number of the storage * cloud {@link org.dasein.cloud.ProviderContext} is set to the real account number of the compute cloud. * Ownership of storage assets is then set to the effective account number even though in reality their ownership * in the target cloud is the account number. * @return the effective account number for this context */ public @Nonnull String getEffectiveAccountNumber() { if( effectiveAccountNumber == null ) { return getAccountNumber(); } return effectiveAccountNumber; } /** * @return the cloud's unique identified for the region in which this context is operating */ public @Nullable String getRegionId() { return regionId; } /** * Sets a strategy for tracking client requests to Dasein. Management of the request ID occurs outside of Dasein so that * the client can determine the scope of exactly what it is tracking. * @param strategy an object that describes the way in which the ID should be used (sent as a header, logged to file etc). * @return the ProviderContext with the tracking strategy set */ public @Nonnull ProviderContext withRequestTracking(@Nonnull RequestTrackingStrategy strategy){ this.strategy = strategy; return this; } /** * @return the strategy object currently being used for tracking requests */ public @Nullable RequestTrackingStrategy getRequestTrackingStrategy(){ return this.strategy; } /******************************** DEPRECATED METHODS ********************************/ /** * Constructs a new, empty provider context for managing the provider context information. * @deprecated use {@link ProviderContext#getContext(Cloud, String, String, Value...)} */ @Deprecated public ProviderContext() { } /** * Constructs a new provider context for the specified account number in the specified region. * @param accountNumber the account number for the account in the cloud * @param inRegionId the region to be referenced by this provider context * @deprecated use {@link ProviderContext#getContext(Cloud, String, String, Value...)} */ @Deprecated public ProviderContext(@Nonnull String accountNumber, @Nonnull String inRegionId) { this.accountNumber = accountNumber; regionId = inRegionId; } @Override @Deprecated public void setAccountNumber(@Nonnull String accountNumber) { if( this.accountNumber == null ) { this.accountNumber = accountNumber; } else { throw new RuntimeException("Cannot double-set the account number. Tried " + accountNumber + ", was already " + this.accountNumber); } } @SuppressWarnings("deprecation") @Deprecated public void setCloud(@Nonnull CloudProvider provider) throws InternalException { String endpoint = getEndpoint(); if( endpoint == null ) { throw new InternalException("The context was not properly configured"); } cloud = Cloud.getInstance(endpoint); if( cloud == null ) { String pname = getProviderName(); String cname = getCloudName(); cloud = Cloud.register(pname == null ? provider.getProviderName() : pname, cname == null ? provider.getCloudName() : cname, endpoint, provider.getClass()); } } /** * Sets the effective account number for the provider context for when this context represents a storage * cloud being merged into a compute cloud. * @param effectiveAccountNumber the account number of the compute cloud associated with this storage context * @deprecated use {@link Cloud#createContext(String, String, org.dasein.cloud.ProviderContext.Value...)} (will automatically set effective account number when connected) */ @Deprecated void setEffectiveAccountNumber(@Nonnull String effectiveAccountNumber) { if( this.effectiveAccountNumber == null ) { this.effectiveAccountNumber = effectiveAccountNumber; } else { throw new RuntimeException("Cannot double-set the effective account number. Tried " + effectiveAccountNumber + ", was already " + this.effectiveAccountNumber); } } @Override @Deprecated public void setRegionId(@Nullable String regionId) { if( this.regionId == null ) { this.regionId = regionId; } else { throw new RuntimeException("Cannot double-set the region ID. Tried " + regionId + ", was already " + this.regionId); } } }