/*
* Copyright 2008-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.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import com.amazonaws.services.ec2.model.Instance;
import com.amazonaws.eclipse.ec2.RemoteCommandUtils;
/**
* Models a proxy that sits in front of servers and load balances requests
* between them.
*/
public class Ec2WebProxy extends Ec2Server {
/** The ID for our US-East proxy AMI */
private static final String US_EAST_1_AMI_ID = "ami-d1ca2db8";
/** The ID for our EU-West proxy AMI */
private static final String EU_WEST_1_AMI_ID = "ami-10163e64";
/** The ID for our US-West proxy AMI */
private static final String US_WEST_1_AMI_ID = "ami-3b09587e";
/** The ID for our AP-Southeast proxy AMI */
private static final String AP_SOUTHEAST_1_AMI_ID = "ami-0df38c5f";
/** The EC2 instances behind this proxy */
private List<Instance> proxiedInstances;
/** Shared utilities for executing remote commands */
private static final RemoteCommandUtils remoteCommandUtils = new RemoteCommandUtils();
/** Shared logger */
private static final Logger logger = Logger.getLogger(Ec2WebProxy.class.getName());
/**
* The port on which this proxy should listen for requests, and on which the
* servers behind this proxy are listening for requests.
*/
private int serverPort = DEFAULT_PORT;
/**
* The default port for this proxy to listen for requests.
*/
private static final int DEFAULT_PORT = 80;
/**
* Returns the ID of the AMI to use when starting this server in EC2, based
* on the specified region. If an AMI isn't available for the specified
* region, this method will throw an exception.
*
* @param region
* The name of the EC2 region (ex: 'us-east-1') in which the
* instance will be launched.
*
* @return The ID of the AMI to use when starting this server in EC2, based
* on the specified region.
*
* @throws Exception
* If there is no AMI registered for the specified region.
*/
public static String getAmiIdByRegion(String region) throws Exception {
if (region.equalsIgnoreCase("us-east-1")) {
return US_EAST_1_AMI_ID;
} else if (region.equalsIgnoreCase("eu-west-1")) {
return EU_WEST_1_AMI_ID;
} else if (region.equalsIgnoreCase("us-west-1")) {
return US_WEST_1_AMI_ID;
} else if (region.equalsIgnoreCase("ap-southeast-1")) {
return AP_SOUTHEAST_1_AMI_ID;
}
throw new Exception("Unsupported region: '" + region + "'");
}
/**
* Creates a new EC2 web proxy running on the specified instance.
*
* @param instance
* A started instance running the EC2 web proxy AMI.
*/
public Ec2WebProxy(Instance instance) {
super(instance);
}
/**
* Sets the main server port for this proxy and the servers behind it. This
* is the port on which this proxy will listen for requests, and the port on
* which the servers behind it are listening for requests. It is not
* currently possible to have the proxy listening on one port and have the
* servers behind it listening on a different port.
*
* @param serverPort
* The port on which this proxy should listen for requests, and
* on which the servers behind this proxy are listening for
* requests.
*/
public void setMainPort(int serverPort) {
this.serverPort = serverPort;
}
/**
* Connects to the proxy and starts the proxy software.
*
* @throws IOException
* If any problems are encountered connecting to the proxy and
* starting the proxy software.
*/
public void startProxy() throws IOException {
String newHaproxyLocation = "/env/haproxy/haproxy";
String startCommand = newHaproxyLocation + " -D -f /etc/haproxy.cfg -p /var/run/haproxy.pid -sf $(</var/run/haproxy.pid)";
remoteCommandUtils.executeRemoteCommand(startCommand, instance);
}
/**
* Connects to the proxy and publishes the proxy configuration.
*
* @throws IOException
* If any problems were encountered publishing the
* configuration.
*/
@Override
public void publishServerConfiguration(File unused) throws Exception {
HaproxyConfigurationListenSection section
= new HaproxyConfigurationListenSection("proxy", serverPort);
for (Instance instance : proxiedInstances) {
section.addServer(instance.getPrivateDnsName() + ":" + serverPort);
}
String proxyConfiguration
= getGlobalSection().toConfigString()
+ getDefaultsSection().toConfigString()
+ section.toConfigString();
logger.fine("Publishing proxy configuration:\n" + proxyConfiguration);
File f = File.createTempFile("haproxyConfig", ".cfg");
FileWriter writer = new FileWriter(f);
try {
writer.write(proxyConfiguration);
} finally {
writer.close();
}
String remoteFile = "/tmp/" + f.getName();
remoteCommandUtils.copyRemoteFile(f.getAbsolutePath(), remoteFile, instance);
String remoteCommand = "cp " + remoteFile + " /etc/haproxy.cfg";
remoteCommandUtils.executeRemoteCommand(remoteCommand, instance);
}
/**
* Sets the instances that this proxy load balances between.
*
* @param instances
* The instances that this proxy will load balance between.
*/
public void setProxiedHosts(List<Instance> instances) {
this.proxiedInstances = instances;
}
/**
* Returns an HaproxyConfigurationSection object already set up with the
* defaults for the "global" section.
*
* @return An HaproxyConfigurationSection object already set up with the
* defaults for the "global" section.
*/
private HaproxyConfigurationSection getGlobalSection() {
HaproxyConfigurationSection section = new HaproxyConfigurationSection("global");
section.addProperty("log 127.0.0.1", "local0");
section.addProperty("log 127.0.0.1", "local1 notice");
section.addProperty("maxconn", "4096");
section.addProperty("user", "nobody");
section.addProperty("group", "nobody");
return section;
}
/**
* Returns an HaproxyConfigurationSection object already set up with the
* defaults for the "defaults" section.
*
* @return An HaproxyConfigurationSection object already set up with the
* defaults for the "defaults" section.
*/
private HaproxyConfigurationSection getDefaultsSection() {
HaproxyConfigurationSection section = new HaproxyConfigurationSection("defaults");
section.addProperty("log", "global");
section.addProperty("mode", "http");
section.addProperty("option", "httplog");
section.addProperty("option", "dontlognull");
section.addProperty("retries", "3");
section.addProperty("redispatch", "");
section.addProperty("maxconn", "2000");
section.addProperty("contimeout", "5000");
section.addProperty("clitimeout", "50000");
section.addProperty("srvtimeout", "50000");
return section;
}
/**
* Models the HAProxy configuration data for a "listen" section.
*
* @author Jason Fulghum <fulghum@amazon.com>
*/
private class HaproxyConfigurationListenSection extends HaproxyConfigurationSection {
private int serverCount = 1;
public HaproxyConfigurationListenSection(String sectionName, int port) {
super("listen " + sectionName);
this.addProperty("bind", ":" + port);
this.addProperty("balance", "roundrobin");
}
public void addServer(String server) {
this.addProperty("server s" + serverCount++, server);
}
}
/**
* Models an HAProxy configuration section.
*
* @author Jason Fulghum <fulghum@amazon.com>
*/
private class HaproxyConfigurationSection {
private final String sectionName;
private final List<String[]> properties = new ArrayList<String[]>();
/**
* Creates a new object with the specified section name.
*
* @param sectionName
* The name of this section that all properties will be
* listed under.
*/
public HaproxyConfigurationSection(String sectionName) {
this.sectionName = sectionName;
}
/**
* Adds a property to this section. Note that multiple different
* properties can have the same key.
*
* @param key
* The name of the property being set.
* @param value
* The value of the property being set.
*/
public void addProperty(String key, String value) {
properties.add(new String[] {key, value});
}
/**
* Returns a string representation of this section, designed for use in
* an haproxy configuration file.
*
* @return A string representation of this section, designed for use in
* an haproxy configuration file.
*/
public String toConfigString() {
StringBuilder builder = new StringBuilder();
builder.append(sectionName + "\n");
for (String[] pair : properties) {
String key = pair[0];
String value = pair[1];
builder.append("\t" + key + " \t" + value + "\n");
}
builder.append("\n");
return builder.toString();
}
}
public void setInstance(Instance newInstance) {
this.instance = newInstance;
}
@Override
public void start() throws Exception {
startProxy();
}
@Override
public void stop() throws Exception {
// no-op since it's not needed yet
}
@Override
public void publish(File archiveFile, String moduleName) throws Exception {
// no-op since there's no content
}
}