/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.brooklyn.core.location.access; import static com.google.common.base.Preconditions.checkNotNull; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import org.apache.brooklyn.api.location.Location; import org.apache.brooklyn.api.mgmt.rebind.RebindContext; import org.apache.brooklyn.api.mgmt.rebind.RebindSupport; import org.apache.brooklyn.api.mgmt.rebind.mementos.LocationMemento; import org.apache.brooklyn.core.location.AbstractLocation; import org.apache.brooklyn.core.mgmt.rebind.BasicLocationRebindSupport; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.exceptions.Exceptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Objects.ToStringHelper; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.net.HostAndPort; /** * * @author aled * * TODO This implementation is not efficient, and currently has a cap of about 50000 rules. * Need to improve the efficiency and scale. * A quick win could be to use a different portReserved counter for each publicIpId, * when calling acquirePublicPort? * * TODO Callers need to be more careful in acquirePublicPort for which ports are actually in use. * If multiple apps sharing the same public-ip (e.g. in the same vcloud-director vOrg) then they * must not allocate the same public port (e.g. ensure they share the same PortForwardManager * by using the same scope in * {@code managementContext.getLocationRegistry().resolve("portForwardManager(scope=global)")}. * However, this still doesn't check if the port is *actually* available. For example, if a * different Brooklyn instance is also deploying there then we can get port conflicts, or if * some ports in that range are already in use (e.g. due to earlier dev/test runs) then this * will not be respected. Callers should probably figure out the port number themselves, but * that also leads to concurrency issues. * * TODO The publicIpId means different things to different callers: * <ul> * <li> In acquirePublicPort() it is (often?) an identifier of the actual public ip. * <li> In later calls to associate(), it is (often?) an identifier for the target machine * such as the jcloudsMachine.getJcloudsId(). * </ul> */ @SuppressWarnings("serial") public class PortForwardManagerImpl extends AbstractLocation implements PortForwardManager { private static final Logger log = LoggerFactory.getLogger(PortForwardManagerImpl.class); protected final Map<String,PortMapping> mappings = new LinkedHashMap<String,PortMapping>(); private final Map<AssociationListener, Predicate<? super AssociationMetadata>> associationListeners = new ConcurrentHashMap<AssociationListener, Predicate<? super AssociationMetadata>>(); @Deprecated protected final Map<String,String> publicIpIdToHostname = new LinkedHashMap<String,String>(); // horrible hack -- see javadoc above private final AtomicInteger portReserved = new AtomicInteger(11000); private final Object mutex = new Object(); public PortForwardManagerImpl() { super(); if (isLegacyConstruction()) { log.warn("Deprecated construction of "+PortForwardManagerImpl.class.getName()+"; instead use location resolver"); } } @Override public void init() { super.init(); Integer portStartingPoint; Object rawPort = getAllConfigBag().getStringKey(PORT_FORWARD_MANAGER_STARTING_PORT.getName()); if (rawPort != null) { portStartingPoint = getConfig(PORT_FORWARD_MANAGER_STARTING_PORT); } else { portStartingPoint = getManagementContext().getConfig().getConfig(PORT_FORWARD_MANAGER_STARTING_PORT); } portReserved.set(portStartingPoint); log.debug(this+" set initial port to "+portStartingPoint); } // TODO Need to use attributes for these so they are persisted (once a location is an entity), // rather than this deprecated approach of custom fields. @Override public RebindSupport<LocationMemento> getRebindSupport() { return new BasicLocationRebindSupport(this) { @Override public LocationMemento getMemento() { Map<String, PortMapping> mappingsCopy; Map<String,String> publicIpIdToHostnameCopy; synchronized (mutex) { mappingsCopy = MutableMap.copyOf(mappings); publicIpIdToHostnameCopy = MutableMap.copyOf(publicIpIdToHostname); } return getMementoWithProperties(MutableMap.<String,Object>of( "mappings", mappingsCopy, "portReserved", portReserved.get(), "publicIpIdToHostname", publicIpIdToHostnameCopy)); } @Override protected void doReconstruct(RebindContext rebindContext, LocationMemento memento) { super.doReconstruct(rebindContext, memento); mappings.putAll( Preconditions.checkNotNull((Map<String, PortMapping>) memento.getCustomField("mappings"), "mappings was not serialized correctly")); portReserved.set( (Integer)memento.getCustomField("portReserved")); publicIpIdToHostname.putAll( Preconditions.checkNotNull((Map<String, String>)memento.getCustomField("publicIpIdToHostname"), "publicIpIdToHostname was not serialized correctly") ); } }; } @Override public int acquirePublicPort(String publicIpId) { int port; synchronized (mutex) { // far too simple -- see javadoc above port = getNextPort(); // TODO When delete deprecated code, stop registering PortMapping until associate() is called PortMapping mapping = new PortMapping(publicIpId, port, null, -1); log.debug(this+" allocating public port "+port+" on "+publicIpId+" (no association info yet)"); mappings.put(makeKey(publicIpId, port), mapping); } onChanged(); return port; } protected int getNextPort() { // far too simple -- see javadoc above return portReserved.getAndIncrement(); } @Override public void associate(String publicIpId, HostAndPort publicEndpoint, Location l, int privatePort) { associateImpl(publicIpId, publicEndpoint, l, privatePort); emitAssociationCreatedEvent(publicIpId, publicEndpoint, l, privatePort); } @Override public void associate(String publicIpId, HostAndPort publicEndpoint, int privatePort) { associateImpl(publicIpId, publicEndpoint, null, privatePort); emitAssociationCreatedEvent(publicIpId, publicEndpoint, null, privatePort); } protected void associateImpl(String publicIpId, HostAndPort publicEndpoint, Location l, int privatePort) { synchronized (mutex) { String publicIp = publicEndpoint.getHostText(); int publicPort = publicEndpoint.getPort(); recordPublicIpHostname(publicIpId, publicIp); PortMapping mapping = new PortMapping(publicIpId, publicEndpoint, l, privatePort); PortMapping oldMapping = getPortMappingWithPublicSide(publicIpId, publicPort); log.debug(this+" associating public "+publicEndpoint+" on "+publicIpId+" with private port "+privatePort+" at "+l+" ("+mapping+")" +(oldMapping == null ? "" : " (overwriting "+oldMapping+" )")); mappings.put(makeKey(publicIpId, publicPort), mapping); } onChanged(); } private void emitAssociationCreatedEvent(String publicIpId, HostAndPort publicEndpoint, Location location, int privatePort) { AssociationMetadata metadata = new AssociationMetadata(publicIpId, publicEndpoint, location, privatePort); for (Map.Entry<AssociationListener, Predicate<? super AssociationMetadata>> entry : associationListeners.entrySet()) { if (entry.getValue().apply(metadata)) { try { entry.getKey().onAssociationCreated(metadata); } catch (Exception e) { Exceptions.propagateIfFatal(e); log.warn("Exception thrown when emitting association creation event " + metadata, e); } } } } @Override public HostAndPort lookup(Location l, int privatePort) { synchronized (mutex) { for (PortMapping m: mappings.values()) { if (l.equals(m.target) && privatePort == m.privatePort) return getPublicHostAndPort(m); } } return null; } @Override public HostAndPort lookup(String publicIpId, int privatePort) { synchronized (mutex) { for (PortMapping m: mappings.values()) { if (publicIpId.equals(m.publicIpId) && privatePort==m.privatePort) return getPublicHostAndPort(m); } } return null; } @Override public boolean forgetPortMapping(String publicIpId, int publicPort) { PortMapping old; synchronized (mutex) { old = mappings.remove(makeKey(publicIpId, publicPort)); if (old != null) { emitAssociationDeletedEvent(associationMetadataFromPortMapping(old)); } log.debug("cleared port mapping for "+publicIpId+":"+publicPort+" - "+old); } if (old != null) onChanged(); return (old != null); } @Override public boolean forgetPortMappings(Location l) { List<PortMapping> result = Lists.newArrayList(); synchronized (mutex) { for (Iterator<PortMapping> iter = mappings.values().iterator(); iter.hasNext();) { PortMapping m = iter.next(); if (l.equals(m.target)) { iter.remove(); result.add(m); emitAssociationDeletedEvent(associationMetadataFromPortMapping(m)); } } } if (log.isDebugEnabled()) log.debug("cleared all port mappings for "+l+" - "+result); if (!result.isEmpty()) { onChanged(); } return !result.isEmpty(); } @Override public boolean forgetPortMappings(String publicIpId) { List<PortMapping> result = Lists.newArrayList(); synchronized (mutex) { for (Iterator<PortMapping> iter = mappings.values().iterator(); iter.hasNext();) { PortMapping m = iter.next(); if (publicIpId.equals(m.publicIpId)) { iter.remove(); result.add(m); emitAssociationDeletedEvent(associationMetadataFromPortMapping(m)); } } } if (log.isDebugEnabled()) log.debug("cleared all port mappings for "+publicIpId+" - "+result); if (!result.isEmpty()) { onChanged(); } return !result.isEmpty(); } private void emitAssociationDeletedEvent(AssociationMetadata metadata) { for (Map.Entry<AssociationListener, Predicate<? super AssociationMetadata>> entry : associationListeners.entrySet()) { if (entry.getValue().apply(metadata)) { try { entry.getKey().onAssociationDeleted(metadata); } catch (Exception e) { Exceptions.propagateIfFatal(e); log.warn("Exception thrown when emitting association creation event " + metadata, e); } } } } @Override protected ToStringHelper string() { int size; synchronized (mutex) { size = mappings.size(); } return super.string().add("scope", getScope()).add("mappingsSize", size); } @Override public String toVerboseString() { String mappingsStr; synchronized (mutex) { mappingsStr = mappings.toString(); } return string().add("mappings", mappingsStr).toString(); } @Override public String getScope() { return checkNotNull(getConfig(SCOPE), "scope"); } @Override public boolean isClient() { return false; } @Override public void addAssociationListener(AssociationListener listener, Predicate<? super AssociationMetadata> filter) { associationListeners.put(listener, filter); } @Override public void removeAssociationListener(AssociationListener listener) { associationListeners.remove(listener); } protected String makeKey(String publicIpId, int publicPort) { return publicIpId+":"+publicPort; } private AssociationMetadata associationMetadataFromPortMapping(PortMapping portMapping) { String publicIpId = portMapping.getPublicEndpoint().getHostText(); HostAndPort publicEndpoint = portMapping.getPublicEndpoint(); Location location = portMapping.getTarget(); int privatePort = portMapping.getPrivatePort(); return new AssociationMetadata(publicIpId, publicEndpoint, location, privatePort); } /////////////////////////////////////////////////////////////////////////////////// // Internal state, for generating memento /////////////////////////////////////////////////////////////////////////////////// public List<PortMapping> getPortMappings() { synchronized (mutex) { return ImmutableList.copyOf(mappings.values()); } } public Map<String, Integer> getPortCounters() { return ImmutableMap.of("global", portReserved.get()); } /////////////////////////////////////////////////////////////////////////////////// // Deprecated /////////////////////////////////////////////////////////////////////////////////// @Override @Deprecated public PortMapping acquirePublicPortExplicit(String publicIpId, int port) { PortMapping mapping = new PortMapping(publicIpId, port, null, -1); log.debug("assigning explicit public port "+port+" at "+publicIpId); PortMapping result; synchronized (mutex) { result = mappings.put(makeKey(publicIpId, port), mapping); } onChanged(); return result; } @Override @Deprecated public boolean forgetPortMapping(PortMapping m) { return forgetPortMapping(m.publicIpId, m.publicPort); } @Override @Deprecated public void recordPublicIpHostname(String publicIpId, String hostnameOrPublicIpAddress) { log.debug("recording public IP "+publicIpId+" associated with "+hostnameOrPublicIpAddress); synchronized (mutex) { String old = publicIpIdToHostname.put(publicIpId, hostnameOrPublicIpAddress); if (old!=null && !old.equals(hostnameOrPublicIpAddress)) log.warn("Changing hostname recorded against public IP "+publicIpId+"; from "+old+" to "+hostnameOrPublicIpAddress); } onChanged(); } @Override @Deprecated public String getPublicIpHostname(String publicIpId) { synchronized (mutex) { return publicIpIdToHostname.get(publicIpId); } } @Override @Deprecated public boolean forgetPublicIpHostname(String publicIpId) { log.debug("forgetting public IP "+publicIpId+" association"); boolean result; synchronized (mutex) { result = (publicIpIdToHostname.remove(publicIpId) != null); } onChanged(); return result; } @Override @Deprecated public int acquirePublicPort(String publicIpId, Location l, int privatePort) { int publicPort; synchronized (mutex) { PortMapping old = getPortMappingWithPrivateSide(l, privatePort); // only works for 1 public IP ID per location (which is the norm) if (old!=null && old.publicIpId.equals(publicIpId)) { log.debug("request to acquire public port at "+publicIpId+" for "+l+":"+privatePort+", reusing old assignment "+old); return old.getPublicPort(); } publicPort = acquirePublicPort(publicIpId); log.debug("request to acquire public port at "+publicIpId+" for "+l+":"+privatePort+", allocating "+publicPort); associateImpl(publicIpId, publicPort, l, privatePort); } onChanged(); return publicPort; } @Override @Deprecated public void associate(String publicIpId, int publicPort, Location l, int privatePort) { synchronized (mutex) { associateImpl(publicIpId, publicPort, l, privatePort); } onChanged(); } protected void associateImpl(String publicIpId, int publicPort, Location l, int privatePort) { synchronized (mutex) { PortMapping mapping = new PortMapping(publicIpId, publicPort, l, privatePort); PortMapping oldMapping = getPortMappingWithPublicSide(publicIpId, publicPort); log.debug("associating public port "+publicPort+" on "+publicIpId+" with private port "+privatePort+" at "+l+" ("+mapping+")" +(oldMapping == null ? "" : " (overwriting "+oldMapping+" )")); mappings.put(makeKey(publicIpId, publicPort), mapping); } } /////////////////////////////////////////////////////////////////////////////////// // Internal only; make protected when deprecated interface method removed /////////////////////////////////////////////////////////////////////////////////// @Override public HostAndPort getPublicHostAndPort(PortMapping m) { if (m.publicEndpoint == null) { String hostname = getPublicIpHostname(m.publicIpId); if (hostname==null) throw new IllegalStateException("No public hostname associated with "+m.publicIpId+" (mapping "+m+")"); return HostAndPort.fromParts(hostname, m.publicPort); } else { return m.publicEndpoint; } } @Override public PortMapping getPortMappingWithPublicSide(String publicIpId, int publicPort) { synchronized (mutex) { return mappings.get(makeKey(publicIpId, publicPort)); } } @Override public Collection<PortMapping> getPortMappingWithPublicIpId(String publicIpId) { List<PortMapping> result = new ArrayList<PortMapping>(); synchronized (mutex) { for (PortMapping m: mappings.values()) if (publicIpId.equals(m.publicIpId)) result.add(m); } return result; } /** returns the subset of port mappings associated with a given location */ @Override public Collection<PortMapping> getLocationPublicIpIds(Location l) { List<PortMapping> result = new ArrayList<PortMapping>(); synchronized (mutex) { for (PortMapping m: mappings.values()) if (l.equals(m.getTarget())) result.add(m); } return result; } @Override public PortMapping getPortMappingWithPrivateSide(Location l, int privatePort) { synchronized (mutex) { for (PortMapping m: mappings.values()) if (l.equals(m.getTarget()) && privatePort==m.privatePort) return m; } return null; } }