/*
* Copyright 2009-2012 Amazon Technologies, Inc.
*
* 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://aws.amazon.com/apache2.0
*
* This file 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 com.amazonaws.ec2.cluster;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.ui.statushandlers.StatusManager;
import com.amazonaws.AmazonClientException;
import com.amazonaws.eclipse.core.AWSClientFactory;
import com.amazonaws.eclipse.core.AwsToolkitCore;
import com.amazonaws.eclipse.ec2.Ec2InstanceLauncher;
import com.amazonaws.eclipse.ec2.Ec2Plugin;
import com.amazonaws.eclipse.ec2.InstanceUtils;
import com.amazonaws.eclipse.ec2.preferences.PreferenceConstants;
import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.model.AssociateAddressRequest;
import com.amazonaws.services.ec2.model.AuthorizeSecurityGroupIngressRequest;
import com.amazonaws.services.ec2.model.DescribeAddressesRequest;
import com.amazonaws.services.ec2.model.DescribeAddressesResult;
import com.amazonaws.services.ec2.model.Instance;
/**
* Models a cluster of hosts running in Amazon EC2. Specific application server
* clusters will need to override this class to provide details on their
* application servers.
*/
public abstract class Cluster {
/** Shared logger */
private static final Logger logger = Logger.getLogger(Cluster.class.getName());
/** Shared factory for creating Amazon EC2 clients */
private final AWSClientFactory clientFactory = AwsToolkitCore.getClientFactory();
/**
* The individual application servers that make up this cluster.
*/
protected List<Ec2Server> applicationServers = new ArrayList<Ec2Server>();
/** The optional proxy for this elastic cluster */
private Ec2WebProxy webProxy;
/**
* A set of EC2 instance IDs representing servers who have had their
* server configuration successfully published since the last time
* the server configuration files were invalidated.
*/
protected Set<String> serversWithUpToDateConfiguration = new HashSet<String>();
/**
* The configuration that defines how a cluster instance runs, including the
* security group in which the AMIs run, the key pair that provides access
* to the instance, etc.
*/
protected final ClusterConfiguration clusterConfiguration;
/**
* A map of the application server AMIs to launch for this cluster mapped by
* Amazon EC2 region name.
*/
private Map<String, String> amisByRegion = new HashMap<String, String>();
/**
* True if this cluster should be running in debug mode, false otherwise.
*/
private boolean debugMode;
/**
* Creates a new cluster with the specified cluster configuration.
*
* @param clusterConfiguration
* The configuration details for this cluster including what
* security group it runs in, what key pair is used to access it,
* etc.
*/
public Cluster(ClusterConfiguration clusterConfiguration) {
this.clusterConfiguration = clusterConfiguration;
}
/**
* Sets whether or not this cluster should be running in debug mode.
*
* @param debugMode
* True if this cluster should be running in debug mode,
* otherwise false.
*/
public void setDebugMode(boolean debugMode) {
this.debugMode = debugMode;
for (Ec2Server server : applicationServers) {
server.setDebugMode(debugMode);
}
}
/**
* Adds a new host to this cluster.
*
* @param instance
* The Amazon EC2 instance to add to this cluster.
*/
public void addHost(Instance instance) {
Ec2Server server = createApplicationServer(instance);
server.setDebugMode(debugMode);
applicationServers.add(server);
}
/**
* Adds the specified host as the proxy for this cluster.
*
* @param instance
* The Amazon EC2 instance that will serve as the proxy for this
* cluster.
*/
public void addProxyHost(Instance instance) {
webProxy = new Ec2WebProxy(instance);
int serverPort = clusterConfiguration.getMainPort();
if (serverPort != -1) {
this.webProxy.setMainPort(serverPort);
}
}
/**
* Publishes the specified resources to this cluster. The default
* implementation simply iterates over the hosts in the cluster and calls
* publish on the individual application servers.
*
* Subclasses can override the default implementation if a specific cluster
* of application servers requires a more involved publish process at the
* cluster level.
*
* @see Ec2Server#publish(File, String) for more details on the data
* provided in the moduleArchive.
*
* @param moduleArchive
* The archive of resources to be deployed.
* @param moduleName
* The name of the module being deployed.
* @throws Exception
* If any problems were encountered while publishing.
*/
public void publish(File moduleArchive, String moduleName) throws Exception {
for (Ec2Server server : applicationServers) {
server.publish(moduleArchive, moduleName);
}
}
/**
* Notifies this cluster that the server configuration files on the cluster
* hosts are out of sync and need to be published the next time
* publishServerConfiguration is called.
*/
public void invalidateServerConfiguration() {
serversWithUpToDateConfiguration.clear();
}
/**
* Returns the number of hosts contained in this cluster.
*
* @return The number of hosts contained in this cluster.
*/
public int size() {
return applicationServers.size();
}
/**
* Initializes this running cluster so that it's ready to be used. The exact
* initialization performed depends on the specifics of the actual
* application servers. The default implementation simply gives each
* application server a chance to initialize themselves.
*
* Subclasses can override this method to perform initialization specific to
* other types of application server clusters.
*
* @throws Exception
* If any problems were encountered while initializing this
* cluster.
*/
public void initialize() throws Exception {
for (Ec2Server server : applicationServers) {
server.initialize();
}
}
/**
* Returns a list of the Amazon EC2 instance IDs for this cluster.
*
* @return A list of the Amazon EC2 instance IDs for this cluster.
*/
public List<String> getInstanceIds() {
List<String> instanceIds = new ArrayList<String>();
if (applicationServers == null) {
return instanceIds;
}
for (Ec2Server server : applicationServers) {
instanceIds.add(server.getInstanceId());
}
return instanceIds;
}
/**
* Stops the application servers in this cluster.
*
* @throws Exception
* If any problems are encountered while stopping the
* application servers.
*/
public void stopApplicationServers() throws Exception {
if (applicationServers != null) {
for (Ec2Server server : applicationServers) {
server.stop();
}
}
if (webProxy != null) {
webProxy.stop();
}
}
/**
* Starts the application servers in this cluster.
*
* @throws Exception
* If any problems are encountered while starting the
* application servers.
*/
public void startApplicationServers() throws Exception {
if (applicationServers != null) {
for (Ec2Server server : applicationServers) {
server.start();
}
}
if (webProxy != null) {
webProxy.start();
}
}
/**
* Starts this cluster, launching any Amazon EC2 instances that need to be
* launched, otherwise just reusing the existing hosts in the cluster if
* they're available. If a proxy is required for this cluster, it will be
* launched (if necessary) and configured here as well.
*
* @param monitor
* The progress monitor to use to report progress of starting
* this cluster.
*
* @throws Exception
* If there are any problems launching the cluster hosts,
* initializing the application server environments, configuring
* the proxy, etc.
*/
public void start(IProgressMonitor monitor) throws Exception {
if (monitor == null)
monitor = new NullProgressMonitor();
if (monitor.isCanceled())
return;
monitor.beginTask("Starting cluster", 30);
// Automatic security group configuration...
configureEc2SecurityGroupPermissions(clusterConfiguration
.getSecurityGroupName());
monitor.worked(10);
// Service container launching...
launchServiceContainerInstances(monitor);
monitor.worked(10);
// Proxy launching...
if (clusterConfiguration.getClusterSize() > 1) {
launchProxyInstance(monitor);
} else {
webProxy = null;
}
monitor.worked(10);
String elasticIp = clusterConfiguration.getElasticIp();
if (elasticIp != null) {
associateElasticIp(elasticIp);
}
monitor.done();
}
/**
* Returns the IP address (as a String) for the head of this cluster. If this
* cluster is fronted by a proxy for load balancing, that address will be
* returned.
*
* @return The IP address (as a String) for the head of this cluster.
*/
public String getIp() {
// if we're associated with an elastic IP, use that...
if (clusterConfiguration.getElasticIp() != null) {
return clusterConfiguration.getElasticIp();
}
// if we're using a proxy to load balance, use that...
if (webProxy != null) {
return webProxy.getIp();
}
// otherwise just use the first instance's public IP
return applicationServers.get(0).getIp();
}
/**
* Returns the IP address for one of the servers in this cluster (not
* including the proxy if one is used for load balancing). Callers that need
* the address of a real server and need to ensure that they aren't going
* through the load balancer should use this method to ensure they get
* direct access to one of the application servers.
*
* @return The IP address for one of the application servers in this
* cluster.
*/
public String getInstanceIp() {
return applicationServers.get(0).getIp();
}
/**
* Publishes any server configuration files that need to be published based
* on callers use of the invalidateServerConfiguration method.
*
* @param serverConfigurationDirectory
* The directory containing the app server specific configuration
* files.
*
* @throws Exception
* If any problems were encountered publishing the server
* configuration files.
*/
public void publishServerConfiguration(File serverConfigurationDirectory) throws Exception {
for (Ec2Server server : applicationServers) {
if (isConfigurationDirty(server.getInstanceId())) {
server.publishServerConfiguration(serverConfigurationDirectory);
setConfigurationClean(server.getInstanceId());
}
}
/*
* TODO: it'd be nice to pass the progress monitor into
* publishServerConfiguration and get a finer granularity on the
* progress being made, but at the same time, we want to try to keep any
* Eclipse specific dependencies out of the cluster management layer as
* much as possible.
*
* We might consider building a simple interface that mirrors what we
* need from IProgressMonitor and then a simple adapter.
*/
// monitor.worked(10 * cluster.size());
if (webProxy != null && isConfigurationDirty(webProxy.getInstanceId())) {
int mainPort = clusterConfiguration.getMainPort();
if (mainPort != -1) {
webProxy.setMainPort(mainPort);
}
try {
webProxy.publishServerConfiguration(null);
webProxy.start();
setConfigurationClean(webProxy.getInstanceId());
} catch (Exception ioe) {
Status status = new Status(Status.ERROR, Ec2Plugin.PLUGIN_ID,
"Unable to publish proxy configuration: "
+ ioe.getMessage(), ioe);
throw new CoreException(status);
}
}
}
/**
* Registers the specified AMI for the specified Amazon EC2 region with this
* cluster. When this cluster needs to launch Amazon EC2 instances, it will
* use the registered AMIs and the configured region to determine which AMI
* to launch.
*
* @param amiId
* The ID of the Amazon EC2 AMI that should be launched for
* application servers in this cluster in the specified Amazon
* EC2 region.
* @param region
* The name of the Amazon EC2 region in which the specified AMI
* exists.
*/
public void registerAmiForRegion(String amiId, String region) {
amisByRegion.put(region, amiId);
}
/**
* Returns the ID of the Amazon EC2 AMI for this cluster in the specified
* region, otherwise null if there is no AMI supported for the specified
* region.
*
* @param region
* The Amazon EC2 region in which the returned AMI exists.
*
* @return The ID of the Amazon EC2 AMI for this cluster in the specified
* region, otherwise null if there is no AMI supported for the
* specified region.
*/
public String getAmiByRegion(String region) {
return amisByRegion.get(region);
}
/**
* Returns the set of Amazon EC2 region names in which this cluster can run.
*
* @return The set of Amazon EC2 region names in which this cluster can run.
*/
public Set<String> getSupportedRegions() {
return amisByRegion.keySet();
}
/**
* Returns a unique ID for the Amazon EC2 resource responsible for load
* balancing in this cluster.
*
* @return A unique ID for the Amazon EC2 resource responsible for load
* balancing in this cluster.
*/
public String getProxyId() {
if (webProxy == null) {
return null;
}
return webProxy.getInstanceId();
}
/*
* Protected Interface
*/
/**
* Subclasses must implement this callback method to create the actual
* application server object for a specified Amazon EC2 instance. The
* application server object is what defines the custom interaction required
* for working with a specific application server.
*
* @param instance
* The Amazon EC2 instance with which the new application server
* is associated.
*
* @return An object extending the Ec2Server abstract class that provides
* the exact logic for working with a specific application server
* (publishing, initializing, etc).
*/
protected abstract Ec2Server createApplicationServer(Instance instance);
/**
* Returns true if the specified instance ID needs to have its configuration
* republished.
*
* @param instanceId
* The ID of the EC2 instance representing the server whose
* configuration files are in question.
*
* @return True if the specified instance ID has not been published since
* the last time the server configuration files were invalidated
* by the invalidateServerConfiguration method,
* otherwise false if the published server configuration files for
* the specified EC2 instance are up to date.
*/
protected boolean isConfigurationDirty(String instanceId) {
return !serversWithUpToDateConfiguration.contains(instanceId);
}
/**
* Notifies this cluster that the specified EC2 instance ID has had its
* server configuration files successfully published and is now up to date.
*
* @param instanceId
* The ID of the EC2 instance whose server configuration has been
* successfully published.
*/
protected void setConfigurationClean(String instanceId) {
serversWithUpToDateConfiguration.add(instanceId);
}
/**
* Returns the Amazon EC2 client to use when working with this cluster.
*
* @return The Amazon EC2 client to use when working with this cluster.
*/
protected AmazonEC2 getEc2Client() {
String clusterEndpoint = clusterConfiguration.getEc2RegionEndpoint();
if (clusterEndpoint != null && clusterEndpoint.length() > 0) {
return clientFactory.getEC2ClientByEndpoint(clusterEndpoint);
}
// We should always have a region/endpoint configured in the cluster,
// but just in case we don't, we'll still return something.
return Ec2Plugin.getDefault().getDefaultEC2Client();
}
/*
* Private Interface
*/
/**
* Configures the security group in which this cluster is running so that
* the cluster can be remotely administered and accessed.
*
* @param securityGroup
* The security group to configure.
*
* @throws CoreException
* If any problems are encountered configuring the specified
* security group.
*/
private void configureEc2SecurityGroupPermissions(String securityGroup)
throws CoreException {
String permissiveNetmask = "0.0.0.0/0";
String strictNetmask = permissiveNetmask;
try {
/*
* We use checkip.amazonaws.com to determine our IP and the most
* restrictive netmask we can use to lock down security group
* permissions, but it only works in the US region.
*/
String region = clusterConfiguration.getEc2RegionName();
region = region.toLowerCase();
} catch (Exception e) {
Status status = new Status(Status.INFO, Ec2Plugin.PLUGIN_ID,
"Unable to lookup netmask from checkip.amazon.com. Defaulting to "
+ strictNetmask);
StatusManager.getManager().handle(status, StatusManager.LOG);
}
// We want locked down permissions for the control/management port (SSH)
authorizeSecurityGroupIngressAndSwallowErrors(securityGroup, "tcp", 22,
22, strictNetmask);
// TODO: we'll eventually need the remote debugging port to be configurable
authorizeSecurityGroupIngressAndSwallowErrors(securityGroup, "tcp",
443, 443, strictNetmask);
// We want more permissive permissions for the main, public port
int mainPort = clusterConfiguration.getMainPort();
if (mainPort != -1) {
authorizeSecurityGroupIngressAndSwallowErrors(securityGroup, "tcp",
mainPort, mainPort, permissiveNetmask);
}
}
/**
* Calls out to EC2 to authorize the specified port range, protocol, netmask
* for the specified security group. If any errors are encountered (ex: the
* requested ingress is already included in the security group permissions),
* they are silently swallowed.
*
* @param securityGroup
* The security group to add permissions to.
* @param protocol
* The protocol for the new permissions.
* @param fromPort
* The starting port of the port range.
* @param toPort
* The ending port of the port range.
* @param netmask
* The netmask associated with the new permissions.
*/
private void authorizeSecurityGroupIngressAndSwallowErrors(
String securityGroup, String protocol, int fromPort, int toPort,
String netmask) {
AmazonEC2 ec2 = getEc2Client();
AuthorizeSecurityGroupIngressRequest request = new AuthorizeSecurityGroupIngressRequest();
request.setGroupName(securityGroup);
request.setFromPort(fromPort);
request.setToPort(toPort);
request.setIpProtocol(protocol);
request.setCidrIp(netmask);
try {
ec2.authorizeSecurityGroupIngress(request);
} catch (AmazonClientException e) {
/*
* We don't worry about these exceptions, since callers specifically
* asked for them to be swallowed
*/
}
}
/**
* Launches any application server instances that need to be launched to
* bring this cluster up to full size.
*
* @param monitor
* The progress monitor to use to report progress.
* @throws CoreException
* If any problems are encountered launching the new Amazon EC2
* instances.
* @throws OperationCanceledException
* If we detect that the user canceled the launch.
*/
private void launchServiceContainerInstances(IProgressMonitor monitor)
throws CoreException, OperationCanceledException {
// Calculate how many hosts we need to bring up based on the total size
// of the cluster and how many hosts we already have up.
int numberOfActiveHosts = size();
int numberOfTotalHosts = clusterConfiguration.getClusterSize();
int numberOfMissingHosts = numberOfTotalHosts - numberOfActiveHosts;
if (numberOfMissingHosts < 0)
numberOfMissingHosts = 0;
/*
* TODO: Add logic to shutdown extra instances if we're running too
* many.
*/
// Launch as many hosts as we need to get our fleet to the right size
List<Instance> instances = new ArrayList<Instance>();
if (numberOfMissingHosts > 0) {
logger.info("Launching " + numberOfMissingHosts
+ " service container instances");
String keyPairName = clusterConfiguration.getKeyPairName();
try {
String region = clusterConfiguration.getEc2RegionName();
String amiId = amisByRegion.get(region);
if (amiId == null) {
Status status = new Status(Status.ERROR, Ec2Plugin.PLUGIN_ID,
"This cluster doesn't have an AMI registered for the '" + region + "' region");
throw new CoreException(status);
}
Ec2InstanceLauncher launcher = new Ec2InstanceLauncher(amiId,
keyPairName);
launcher.setProgressMonitor(monitor);
launcher.setInstanceType(clusterConfiguration
.getEc2InstanceType());
launcher.setEc2RegionEndpoint(clusterConfiguration
.getEc2RegionEndpoint());
launcher.setNumberOfInstances(numberOfMissingHosts);
launcher.setSecurityGroup(clusterConfiguration
.getSecurityGroupName());
instances = launcher.launchAndWait();
// Mark the web proxy configuration as out of date so that it
// gets published next time this cluster is deployed
if (webProxy != null) {
serversWithUpToDateConfiguration.remove(webProxy
.getInstanceId());
}
} catch (OperationCanceledException oce) {
// We want to let OperationCanceledExceptions propagate up so we
// can deal with them at a higher layer.
throw oce;
} catch (Exception e) {
Status status = new Status(Status.ERROR, Ec2Plugin.PLUGIN_ID,
"Unable to start cluster: " + e.getMessage(), e);
throw new CoreException(status);
}
logger.info("Successfully started " + instances.size()
+ " instance(s).");
} else {
logger.info("No missing service container instances need to be launched");
}
// Update the local list of application servers
for (Instance instance : instances) {
addHost(instance);
}
try {
initialize();
} catch (Exception e) {
throw new CoreException(new Status(Status.ERROR, Ec2Plugin.PLUGIN_ID,
"Unable to fully initialize cluster", e));
}
}
/**
* Returns the EC2 instance ID of the instance attached to the specified
* Elastic IP address, or null if the Elastic IP address wasn't found, or
* isn't attached to an instance.
*
* @param elasticIp
* The Elastic IP address to check.
*
* @return The EC2 instance ID of the instance attached to the specified
* Elastic IP address, or null if no instance is attached to this
* address.
*
* @throws AmazonEC2Exception
* If any problems were encountered while looking up the
* specified Elastic IP.
*/
private String lookupAttachedInstance(String elasticIp) throws AmazonClientException {
DescribeAddressesRequest request = new DescribeAddressesRequest().withPublicIps(elasticIp);
DescribeAddressesResult result = getEc2Client().describeAddresses(request);
if (!result.getAddresses().isEmpty()) {
return result.getAddresses().get(0).getInstanceId();
}
return null;
}
/**
* Associates the specified Elastic IP with this cluster.
*
* @param elasticIp
* The Elastic IP to associate with this cluster.
*/
private void associateElasticIp(String elasticIp) {
AmazonEC2 ec2 = getEc2Client();
InstanceUtils instanceUtils = new InstanceUtils(ec2);
try {
String instanceId;
if (webProxy != null) {
logger.info("Associating Elastic IP with proxy...");
instanceId = webProxy.getInstanceId();
} else {
logger.info("Associating Elastic IP with application server...");
instanceId = applicationServers.get(0).getInstanceId();
}
logger.info(" - Elastic IP '" + elasticIp + "' => '" + instanceId + "'");
/*
* Check if the ElasticIP is already associated with the correct
* instance, and if so, we don't need to do anything...
*/
String attachedInstance = this.lookupAttachedInstance(elasticIp);
if (attachedInstance != null && attachedInstance.equals(instanceId)) {
return;
}
String previousDnsName = instanceUtils.lookupInstanceById(instanceId).getPublicDnsName();
AssociateAddressRequest request = new AssociateAddressRequest();
request.setInstanceId(instanceId);
request.setPublicIp(elasticIp);
ec2.associateAddress(request);
/*
* When we associate the Elastic IP the public DNS name of that host
* changes, so we need to refresh our Instance objects in
* order to see the new DNS name. If we don't do that, then we'll
* run into problems (sooner or later) where the old DNS name
* doesn't work anymore. We check periodically to account for the
* fact that Elastic IP changes can take different amounts of time
* to show up.
*/
String currentDnsName;
int pollCount = 0;
do {
if (pollCount++ > 90) {
throw new Exception("Unable to detect that the Elastic IP was correctly associated");
}
try {Thread.sleep(5000);} catch (InterruptedException e) {}
currentDnsName = instanceUtils.lookupInstanceById(instanceId).getPublicDnsName();
} while (currentDnsName.equals(previousDnsName));
if (webProxy != null) {
webProxy.refreshInstance(getEc2Client());
} else {
applicationServers.get(0).refreshInstance(getEc2Client());
}
} catch (Exception e) {
Status status = new Status(Status.WARNING, Ec2Plugin.PLUGIN_ID,
"Unable to associate Elastic IP with cluster: " + e.getMessage(), e);
StatusManager.getManager().handle(status, StatusManager.SHOW | StatusManager.LOG);
}
}
/**
* Launches the load balancing proxy instance if necessary.
*
* @param monitor
* The progress monitor for this method to use to report progress
* and check for a request from the user to cancel this
* operation.
*
* @throws CoreException
* If any problems are encountered that prevent this method from
* launching and configuring the load balancing proxy instance.
* @throws OperationCanceledException
* If this method detects that the user requested to cancel this
* operation while this method is executing.
*/
private void launchProxyInstance(IProgressMonitor monitor)
throws CoreException, OperationCanceledException {
List<String> instanceIds = getInstanceIds();
List<Instance> proxiedInstances;
try {
InstanceUtils instanceUtils = new InstanceUtils(getEc2Client());
proxiedInstances = instanceUtils.lookupInstancesById(instanceIds);
} catch (Exception e) {
Status status = new Status(Status.ERROR, Ec2Plugin.PLUGIN_ID,
"Unable to start proxy: " + e.getMessage(), e);
throw new CoreException(status);
}
if (webProxy == null) {
String keyPairName = clusterConfiguration.getKeyPairName();
List<Instance> proxyInstances = null;
try {
String region = clusterConfiguration.getEc2RegionName();
String amiId = Ec2WebProxy.getAmiIdByRegion(region);
// TODO: availability zone support might be nice
Ec2InstanceLauncher proxyLauncher = new Ec2InstanceLauncher(
amiId, keyPairName);
proxyLauncher.setProgressMonitor(monitor);
proxyLauncher.setEc2RegionEndpoint(clusterConfiguration
.getEc2RegionEndpoint());
proxyLauncher.setInstanceType(clusterConfiguration
.getEc2InstanceType());
proxyLauncher.setSecurityGroup(clusterConfiguration
.getSecurityGroupName());
proxyInstances = proxyLauncher.launchAndWait();
} catch (OperationCanceledException oce) {
// We want to let OperationCanceledExceptions propagate up so we
// can deal with them at a higher layer.
throw oce;
} catch (Exception e) {
Status status = new Status(Status.ERROR, Ec2Plugin.PLUGIN_ID,
"Unable to start proxy: " + e.getMessage(), e);
throw new CoreException(status);
}
webProxy = new Ec2WebProxy(proxyInstances.get(0));
webProxy.setMainPort(clusterConfiguration.getMainPort());
}
webProxy.setProxiedHosts(proxiedInstances);
}
}