/*************************************************************************
* Copyright 2009-2014 Eucalyptus Systems, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
* Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
* CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
* additional information or have any questions.
************************************************************************/
package com.eucalyptus.loadbalancing;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.PersistenceContext;
import javax.persistence.PostLoad;
import javax.persistence.PrePersist;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import com.eucalyptus.configurable.ConfigurableClass;
import com.eucalyptus.configurable.ConfigurableField;
import com.eucalyptus.configurable.ConfigurableFieldType;
import com.eucalyptus.configurable.ConfigurableProperty;
import com.eucalyptus.configurable.ConfigurablePropertyException;
import com.eucalyptus.configurable.PropertyChangeListener;
import com.eucalyptus.entities.AbstractPersistent;
import com.eucalyptus.entities.Entities;
import com.eucalyptus.entities.TransactionResource;
import com.eucalyptus.loadbalancing.LoadBalancer.LoadBalancerCoreView;
import com.eucalyptus.loadbalancing.LoadBalancerPolicyDescription.LoadBalancerPolicyDescriptionCoreView;
import com.eucalyptus.loadbalancing.LoadBalancerPolicyDescription.LoadBalancerPolicyDescriptionCoreViewTransform;
import com.eucalyptus.loadbalancing.common.msgs.Listener;
import com.eucalyptus.util.Exceptions;
import com.eucalyptus.util.TypeMapper;
import com.google.common.base.Function;
import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
import com.google.common.collect.ContiguousSet;
import com.google.common.collect.DiscreteDomain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Range;
import com.google.common.collect.Sets;
import com.google.common.collect.Lists;
/**
* @author Sang-Min Park
*
*/
@SuppressWarnings("deprecation")
@ConfigurableClass(root = "services.loadbalancing", description = "Parameters controlling loadbalancing")
@Entity
@PersistenceContext( name = "eucalyptus_loadbalancing" )
@Table( name = "metadata_listener" )
public class LoadBalancerListener extends AbstractPersistent
{
private static Logger LOG = Logger.getLogger( LoadBalancerListener.class );
public static class ELBPortRestrictionChangeListener implements PropertyChangeListener {
@Override
public void fireChange( ConfigurableProperty t, Object newValue ) throws ConfigurablePropertyException {
try {
if ( newValue instanceof String ) {
final Set<Integer> range = PortRangeMapper.apply((String)newValue);
}
} catch ( final Exception e ) {
throw new ConfigurablePropertyException("Malformed port: value should be [port(, port)] or [port-port]");
}
}
}
private static Function<String, Set<Integer>> PortRangeMapper = new Function<String, Set<Integer>>(){
@SuppressWarnings("deprecation")
@Override
@Nullable
public Set<Integer> apply(@Nullable String input) {
try{
if(input.contains("-")){
if(StringUtils.countMatches(input, "-") != 1)
throw new Exception("malformed range");
final String[] tokens = input.split("-");
if(tokens.length!=2){
throw new Exception("invalid range");
}
final int beginPort = Integer.parseInt(tokens[0]);
final int endPort = Integer.parseInt(tokens[1]);
if(beginPort < 1 || endPort > 65535 || beginPort > endPort)
throw new Exception("invald range");
return ContiguousSet.create(Range.closed(beginPort, endPort), DiscreteDomain.integers( ));
}else if(input.contains(",")){
final String[] tokens = input.split(",");
if(tokens.length != StringUtils.countMatches(input, ",")+1)
throw new Exception("malformed list");
final Set<Integer> ports = Sets.newHashSet();
for(final String token : tokens){
final int portNum = Integer.parseInt(token);
if(token.isEmpty()|| portNum < 1 || portNum > 65535)
throw new Exception("invald port number");
ports.add(portNum);
}
return ports;
}else{
final int portNum = Integer.parseInt(input);
if(input.isEmpty()|| portNum < 1 || portNum > 65535)
throw new Exception("invald port number");
return Sets.newHashSet(portNum);
}
}catch(final Exception ex){
throw Exceptions.toUndeclared(ex);
}
}
};
private final static String DEFAULT_PORT_RESTRICTION = "22";
@ConfigurableField( displayName = "loadbalancer_restricted_ports",
description = "The ports restricted for use as a loadbalancer port. Format should be port(, port) or [port-port]",
initial = DEFAULT_PORT_RESTRICTION,
readonly = false,
type = ConfigurableFieldType.KEYVALUE,
changeListener = ELBPortRestrictionChangeListener.class
)
public static String RESTRICTED_PORTS = DEFAULT_PORT_RESTRICTION;
public enum PROTOCOL{
HTTP, HTTPS, TCP, SSL, NONE
}
@Transient
private static final long serialVersionUID = 1L;
@Transient
private LoadBalancerListenerRelationView view = null;
@PostLoad
private void onLoad(){
if(this.view==null)
this.view = new LoadBalancerListenerRelationView(this);
}
private LoadBalancerListener(){}
public static LoadBalancerListener named(final LoadBalancer lb, int lbPort){
LoadBalancerListener newInstance = new LoadBalancerListener();
newInstance.loadbalancer = lb;
newInstance.loadbalancerPort = lbPort;
newInstance.uniqueName = newInstance.createUniqueName();
return newInstance;
}
private LoadBalancerListener(Builder builder){
this.loadbalancer = builder.lb;
this.instancePort = builder.instancePort;
this.instanceProtocol = builder.instanceProtocol;
this.loadbalancerPort = builder.loadbalancerPort;
this.protocol = builder.protocol;
this.sslCertificateArn = builder.sslCertificateArn;
}
public void setSSLCertificateId(final String certArn){
this.sslCertificateArn = certArn;
}
public static class Builder{
public Builder(LoadBalancer lb, int instancePort, int loadbalancerPort, PROTOCOL protocol){
this.lb = lb;
this.instancePort = instancePort;
this.loadbalancerPort = loadbalancerPort;
this.protocol = protocol.name().toLowerCase();
}
public Builder instanceProtocol(PROTOCOL protocol){
this.instanceProtocol= protocol.name().toLowerCase();
return this;
}
public Builder withSSLCerntificate(String arn){
this.sslCertificateArn = arn;
return this;
}
public LoadBalancerListener build(){
return new LoadBalancerListener(this);
}
private LoadBalancer lb = null;
private Integer instancePort = null;
private String instanceProtocol = null;
private Integer loadbalancerPort = null;
private String protocol = null;
private String sslCertificateArn = null;
}
@ManyToOne
@JoinColumn( name = "metadata_loadbalancer_fk" )
private LoadBalancer loadbalancer = null;
@Column(name="instance_port", nullable=false)
private Integer instancePort = null;
@Column(name="instance_protocol", nullable=true)
private String instanceProtocol = null;
@Column(name="loadbalancer_port", nullable=false)
private Integer loadbalancerPort = null;
@Column(name="protocol", nullable=false)
private String protocol = null;
@Column(name="certificate_id", nullable=true)
private String sslCertificateArn = null;
@Column(name="unique_name", nullable=false, unique=true)
private String uniqueName = null;
@ManyToMany( fetch = FetchType.LAZY, cascade = CascadeType.REMOVE )
@JoinTable( name = "metadata_policy_has_listeners", joinColumns = { @JoinColumn( name = "metadata_listener_fk" ) }, inverseJoinColumns = @JoinColumn( name = "metadata_policy_fk" ) )
private List<LoadBalancerPolicyDescription> policies = null;
public int getInstancePort(){
return this.instancePort;
}
public PROTOCOL getInstanceProtocol(){
if(this.instanceProtocol==null)
return PROTOCOL.NONE;
return PROTOCOL.valueOf(this.instanceProtocol.toUpperCase());
}
public int getLoadbalancerPort(){
return this.loadbalancerPort;
}
public PROTOCOL getProtocol(){
return PROTOCOL.valueOf(this.protocol.toUpperCase());
}
public String getCertificateId(){
return this.sslCertificateArn;
}
public void addPolicy(final LoadBalancerPolicyDescription policy){
if(this.policies==null){
this.policies = Lists.newArrayList();
}
if(!this.policies.contains(policy))
this.policies.add(policy);
}
public void removePolicy(final String policyName){
if(this.policies==null || policyName==null)
return;
LoadBalancerPolicyDescription toDelete = null;
for(final LoadBalancerPolicyDescription pol : this.policies){
if(policyName.equals(pol.getPolicyName()))
toDelete = pol;
}
if(toDelete!=null)
this.policies.remove(toDelete);
}
public void removePolicy(final LoadBalancerPolicyDescription policy){
if(this.policies==null || policy==null)
return;
this.policies.remove(policy);
}
public void resetPolicies(){
if(this.policies == null)
return;
this.policies.clear();
}
public List<LoadBalancerPolicyDescriptionCoreView> getPolicies(){
return this.view.getPolicies();
}
public static boolean protocolSupported(Listener listener){
try{
final PROTOCOL protocol = PROTOCOL.valueOf(listener.getProtocol().toUpperCase());
if(PROTOCOL.HTTP.equals(protocol) || PROTOCOL.TCP.equals(protocol) || PROTOCOL.HTTPS.equals(protocol) || PROTOCOL.SSL.equals(protocol))
return true;
else
return false;
}catch(Exception e){
return false;
}
}
public static boolean acceptable(Listener listener){
try{
if(! (listener.getInstancePort() > 0 &&
listener.getLoadBalancerPort() > 0 &&
!Strings.isNullOrEmpty(listener.getProtocol())))
return false;
PROTOCOL protocol = PROTOCOL.valueOf(listener.getProtocol().toUpperCase());
if(!Strings.isNullOrEmpty(listener.getInstanceProtocol()))
protocol = PROTOCOL.valueOf(listener.getInstanceProtocol().toUpperCase());
return true;
}catch(Exception e){
return false;
}
}
public static boolean validRange(Listener listener){
try{
if(! (listener.getInstancePort() > 0 &&
listener.getLoadBalancerPort() > 0 &&
!Strings.isNullOrEmpty(listener.getProtocol())))
return false;
int lbPort =listener.getLoadBalancerPort();
int instancePort = listener.getInstancePort();
if (! (lbPort >= 1 && lbPort <= 65535))
return false;
if (! (instancePort >= 1 && instancePort <= 65535))
return false;
return true;
}catch(Exception e){
return false;
}
}
// port 22: used as sshd by servo instances
public static boolean portAvailable(Listener listener){
try{
if(! (listener.getInstancePort() > 0 &&
listener.getLoadBalancerPort() > 0 &&
!Strings.isNullOrEmpty(listener.getProtocol())))
return false;
int lbPort =listener.getLoadBalancerPort();
int instancePort = listener.getInstancePort();
return ! PortRangeMapper.apply(RESTRICTED_PORTS).contains(lbPort);
}catch(Exception e){
return false;
}
}
@PrePersist
private void generateOnCommit( ) {
if(this.uniqueName==null)
this.uniqueName = createUniqueName( );
}
protected String createUniqueName( ) {
return String.format("listener-%s-%s-%s", this.loadbalancer.getOwnerAccountNumber(), this.loadbalancer.getDisplayName(), this.loadbalancerPort);
}
@Override
public String toString(){
return String.format("Listener for %s: %nProtocol=%s, Port=%d, InstancePort=%d, InstanceProtocol=%s, CertId=%s",
this.loadbalancer.getDisplayName(), this.protocol, this.loadbalancerPort, this.instancePort, this.instanceProtocol, this.sslCertificateArn);
}
public static class LoadBalancerListenerCoreView {
private LoadBalancerListener listener = null;
LoadBalancerListenerCoreView(LoadBalancerListener listener){
this.listener = listener;
}
public int getInstancePort(){
return this.listener.getInstancePort();
}
public PROTOCOL getInstanceProtocol(){
return this.listener.getInstanceProtocol();
}
public int getLoadbalancerPort(){
return this.listener.getLoadbalancerPort();
}
public PROTOCOL getProtocol(){
return this.listener.getProtocol();
}
public String getCertificateId(){
return this.listener.getCertificateId();
}
}
@TypeMapper
public enum LoadBalancerListenerCoreViewTransform implements Function<LoadBalancerListener, LoadBalancerListenerCoreView> {
INSTANCE;
@Override
@Nullable
public LoadBalancerListenerCoreView apply(
@Nullable LoadBalancerListener arg0) {
return new LoadBalancerListenerCoreView(arg0);
}
}
public enum LoadBalancerListenerEntityTransform implements Function<LoadBalancerListenerCoreView, LoadBalancerListener> {
INSTANCE;
@Override
@Nullable
public LoadBalancerListener apply(
@Nullable LoadBalancerListenerCoreView arg0) {
try ( final TransactionResource db = Entities.transactionFor( LoadBalancerListener.class ) ) {
return Entities.uniqueResult(arg0.listener);
}catch(final Exception ex){
throw Exceptions.toUndeclared(ex);
}
}
}
public static class LoadBalancerListenerRelationView {
private LoadBalancer loadbalancer = null;
private ImmutableList<LoadBalancerPolicyDescriptionCoreView> policies = null;
private LoadBalancerListenerRelationView(final LoadBalancerListener listener){
if(listener.loadbalancer!=null) {
Entities.initialize( listener.loadbalancer );
this.loadbalancer = listener.loadbalancer;
}
if(listener.policies!=null){
this.policies = ImmutableList.copyOf(Collections2.transform(listener.policies,
LoadBalancerPolicyDescriptionCoreViewTransform.INSTANCE));
}
}
public LoadBalancerCoreView getLoadBalancer(){
return this.loadbalancer.getCoreView( );
}
public ImmutableList<LoadBalancerPolicyDescriptionCoreView> getPolicies(){
return this.policies;
}
}
}