/** * Copyright 2012, Big Switch Networks, Inc. * Originally created by David Erickson, Stanford University * * 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 net.floodlightcontroller.core; import java.io.IOException; import java.net.SocketAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReentrantReadWriteLock; import net.floodlightcontroller.core.IFloodlightProviderService.Role; import net.floodlightcontroller.core.annotations.LogMessageDoc; import net.floodlightcontroller.core.annotations.LogMessageDocs; import net.floodlightcontroller.core.internal.Controller; import net.floodlightcontroller.core.internal.OFFeaturesReplyFuture; import net.floodlightcontroller.core.internal.OFStatisticsFuture; import net.floodlightcontroller.core.util.AppCookie; import net.floodlightcontroller.core.web.serializers.DPIDSerializer; import net.floodlightcontroller.debugcounter.IDebugCounter; import net.floodlightcontroller.debugcounter.IDebugCounterService; import net.floodlightcontroller.debugcounter.IDebugCounterService.CounterException; import net.floodlightcontroller.debugcounter.IDebugCounterService.CounterType; import net.floodlightcontroller.debugcounter.NullDebugCounter; import net.floodlightcontroller.devicemanager.SwitchPort; import net.floodlightcontroller.packet.Ethernet; import net.floodlightcontroller.routing.ForwardingBase; import net.floodlightcontroller.threadpool.IThreadPoolService; import net.floodlightcontroller.util.LinkedHashSetWrapper; import net.floodlightcontroller.util.MACAddress; import net.floodlightcontroller.util.OrderedCollection; import net.floodlightcontroller.util.TimedCache; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import org.jboss.netty.channel.Channel; import org.openflow.protocol.OFFeaturesReply; import org.openflow.protocol.OFFlowMod; import org.openflow.protocol.OFMatch; import org.openflow.protocol.OFMessage; import org.openflow.protocol.OFPacketIn; import org.openflow.protocol.OFPortStatus; import org.openflow.protocol.OFPortStatus.OFPortReason; import org.openflow.protocol.OFPort; import org.openflow.protocol.OFStatisticsReply; import org.openflow.protocol.OFStatisticsRequest; import org.openflow.protocol.OFType; import org.openflow.protocol.statistics.OFDescriptionStatistics; import org.openflow.protocol.statistics.OFStatistics; import org.openflow.protocol.statistics.OFStatisticsType; import org.openflow.protocol.statistics.OFTableStatistics; import org.openflow.util.HexString; import org.openflow.util.U16; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This is the internal representation of an openflow switch. */ public abstract class OFSwitchBase implements IOFSwitch { // TODO: should we really do logging in the class or should we throw // exception that can then be handled by callers? protected static final Logger log = LoggerFactory.getLogger(OFSwitchBase.class); protected ConcurrentMap<Object, Object> attributes; protected IFloodlightProviderService floodlightProvider; protected IThreadPoolService threadPool; protected IDebugCounterService debugCounters; // FIXME: Don't use java.util.Date protected volatile Date connectedSince; /* Switch features from initial featuresReply */ protected int capabilities; protected int buffers; protected int actions; protected byte tables; protected long datapathId; protected String stringId; protected short accessFlowPriority; protected short coreFlowPriority; private boolean startDriverHandshakeCalled = false; protected Channel channel; /** * Members hidden from subclasses */ private final AtomicInteger transactionIdSource; private final Map<Integer,OFStatisticsFuture> statsFutureMap; private final Map<Integer, IOFMessageListener> iofMsgListenersMap; private final Map<Integer,OFFeaturesReplyFuture> featuresFutureMap; private volatile boolean connected; private volatile Role role; private final TimedCache<Long> timedCache; private final ConcurrentMap<Short, AtomicLong> portBroadcastCacheHitMap; private final PortManager portManager; // Private members for throttling private boolean writeThrottleEnabled = false; protected boolean packetInThrottleEnabled = false; // used by test private int packetInRateThresholdHigh = Integer.parseInt(System.getProperty("input_threshold", "1000")); private int packetInRateThresholdLow = 1; private int packetInRatePerMacThreshold = 50; private int packetInRatePerPortThreshold = 100; private long messageCount = 0; private long messageCountUniqueOFMatch = 0; private long lastMessageTime; private int currentRate = 0; private TimedCache<OFMatch> ofMatchCache; private TimedCache<Long> macCache; private TimedCache<Long> macBlockedCache; private TimedCache<Short> portCache; private TimedCache<Short> portBlockedCache; private boolean flowTableFull = false; protected OFDescriptionStatistics description; private boolean debugCountersRegistered; @SuppressWarnings("unused") private IDebugCounter ctrSwitch, ctrSwitchPktin, ctrSwitchWrite; private IDebugCounter ctrSwitchPktinDrops, ctrSwitchWriteDrops; private static final String PACKAGE = OFSwitchBase.class.getPackage().getName(); protected final static ThreadLocal<Map<IOFSwitch,List<OFMessage>>> local_msg_buffer = new ThreadLocal<Map<IOFSwitch,List<OFMessage>>>() { @Override protected Map<IOFSwitch,List<OFMessage>> initialValue() { return new HashMap<IOFSwitch,List<OFMessage>>(); } }; public static final int OFSWITCH_APP_ID = 5; static { AppCookie.registerApp(OFSwitchBase.OFSWITCH_APP_ID, "switch"); } public OFSwitchBase() { this.stringId = null; this.attributes = new ConcurrentHashMap<Object, Object>(); this.connectedSince = null; this.transactionIdSource = new AtomicInteger(); this.connected = false; this.statsFutureMap = new ConcurrentHashMap<Integer,OFStatisticsFuture>(); this.featuresFutureMap = new ConcurrentHashMap<Integer,OFFeaturesReplyFuture>(); this.iofMsgListenersMap = new ConcurrentHashMap<Integer,IOFMessageListener>(); this.role = null; this.timedCache = new TimedCache<Long>(100, 5*1000 ); // 5 seconds interval this.portBroadcastCacheHitMap = new ConcurrentHashMap<Short, AtomicLong>(); this.description = new OFDescriptionStatistics(); this.lastMessageTime = System.currentTimeMillis(); this.portManager = new PortManager(); // Defaults properties for an ideal switch this.setAttribute(PROP_FASTWILDCARDS, OFMatch.OFPFW_ALL); this.setAttribute(PROP_SUPPORTS_OFPP_FLOOD, Boolean.valueOf(true)); this.setAttribute(PROP_SUPPORTS_OFPP_TABLE, Boolean.valueOf(true)); if (packetInRateThresholdHigh == 0) { packetInRateThresholdHigh = Integer.MAX_VALUE; } else { packetInRateThresholdLow = packetInRateThresholdHigh / 2; } } /** * Manages the ports of this switch. * * Provides methods to query and update the stored ports. The class ensures * that every port name and port number is unique. When updating ports * the class checks if port number <-> port name mappings have change due * to the update. If a new port P has number and port that are inconsistent * with the previous mapping(s) the class will delete all previous ports * with name or number of the new port and then add the new port. * * Port names are stored as-is but they are compared case-insensitive * * The methods that change the stored ports return a list of * PortChangeEvents that represent the changes that have been applied * to the port list so that IOFSwitchListeners can be notified about the * changes. * * Implementation notes: * - We keep several different representations of the ports to allow for * fast lookups * - Ports are stored in unchangeable lists. When a port is modified new * data structures are allocated. * - We use a read-write-lock for synchronization, so multiple readers are * allowed. */ protected class PortManager { private final ReentrantReadWriteLock lock; private List<ImmutablePort> portList; private List<ImmutablePort> enabledPortList; private List<Short> enabledPortNumbers; private Map<Short,ImmutablePort> portsByNumber; private Map<String,ImmutablePort> portsByName; public PortManager() { this.lock = new ReentrantReadWriteLock(); this.portList = Collections.emptyList(); this.enabledPortList = Collections.emptyList(); this.enabledPortNumbers = Collections.emptyList(); this.portsByName = Collections.emptyMap(); this.portsByNumber = Collections.emptyMap(); } /** * Set the internal data structure storing this switch's port * to the ports specified by newPortsByNumber * * CALLER MUST HOLD WRITELOCK * * @param newPortsByNumber * @throws IllegaalStateException if called without holding the * writelock */ private void updatePortsWithNewPortsByNumber( Map<Short,ImmutablePort> newPortsByNumber) { if (!lock.writeLock().isHeldByCurrentThread()) { throw new IllegalStateException("Method called without " + "holding writeLock"); } Map<String,ImmutablePort> newPortsByName = new HashMap<String, ImmutablePort>(); List<ImmutablePort> newPortList = new ArrayList<ImmutablePort>(); List<ImmutablePort> newEnabledPortList = new ArrayList<ImmutablePort>(); List<Short> newEnabledPortNumbers = new ArrayList<Short>(); for(ImmutablePort p: newPortsByNumber.values()) { newPortList.add(p); newPortsByName.put(p.getName().toLowerCase(), p); if (p.isEnabled()) { newEnabledPortList.add(p); newEnabledPortNumbers.add(p.getPortNumber()); } } portsByName = Collections.unmodifiableMap(newPortsByName); portsByNumber = Collections.unmodifiableMap(newPortsByNumber); enabledPortList = Collections.unmodifiableList(newEnabledPortList); enabledPortNumbers = Collections.unmodifiableList(newEnabledPortNumbers); portList = Collections.unmodifiableList(newPortList); } /** * Handle a OFPortStatus delete message for the given port. * Updates the internal port maps/lists of this switch and returns * the PortChangeEvents caused by the delete. If the given port * exists as it, it will be deleted. If the name<->number for the * given port is inconsistent with the ports stored by this switch * the method will delete all ports with the number or name of the * given port. * * This method will increment error/warn counters and log * * @param delPort the port from the port status message that should * be deleted. * @return ordered collection of port changes applied to this switch */ private OrderedCollection<PortChangeEvent> handlePortStatusDelete(ImmutablePort delPort) { lock.writeLock().lock(); OrderedCollection<PortChangeEvent> events = new LinkedHashSetWrapper<PortChangeEvent>(); try { Map<Short,ImmutablePort> newPortByNumber = new HashMap<Short, ImmutablePort>(portsByNumber); ImmutablePort prevPort = portsByNumber.get(delPort.getPortNumber()); if (prevPort == null) { // so such port. Do we have a port with the name? prevPort = portsByName.get(delPort.getName()); if (prevPort != null) { newPortByNumber.remove(prevPort.getPortNumber()); events.add(new PortChangeEvent(prevPort, PortChangeType.DELETE)); } } else if (prevPort.getName().equals(delPort.getName())) { // port exists with consistent name-number mapping newPortByNumber.remove(delPort.getPortNumber()); events.add(new PortChangeEvent(delPort, PortChangeType.DELETE)); } else { // port with same number exists but its name differs. This // is weird. The best we can do is to delete the existing // port(s) that have delPort's name and number. newPortByNumber.remove(delPort.getPortNumber()); events.add(new PortChangeEvent(prevPort, PortChangeType.DELETE)); // is there another port that has delPort's name? prevPort = portsByName.get(delPort.getName().toLowerCase()); if (prevPort != null) { newPortByNumber.remove(prevPort.getPortNumber()); events.add(new PortChangeEvent(prevPort, PortChangeType.DELETE)); } } updatePortsWithNewPortsByNumber(newPortByNumber); return events; } finally { lock.writeLock().unlock(); } } /** * Handle a OFPortStatus message, update the internal data structures * that store ports and return the list of OFChangeEvents. * * This method will increment error/warn counters and log * * @param ps * @return */ public OrderedCollection<PortChangeEvent> handlePortStatusMessage(OFPortStatus ps) { if (ps == null) { throw new NullPointerException("OFPortStatus message must " + "not be null"); } lock.writeLock().lock(); try { ImmutablePort port = ImmutablePort.fromOFPhysicalPort(ps.getDesc()); OFPortReason reason = OFPortReason.fromReasonCode(ps.getReason()); if (reason == null) { throw new IllegalArgumentException("Unknown PortStatus " + "reason code " + ps.getReason()); } if (log.isDebugEnabled()) { log.debug("Handling OFPortStatus: {} for {}", reason, port.toBriefString()); } if (reason == OFPortReason.OFPPR_DELETE) return handlePortStatusDelete(port); // We handle ADD and MODIFY the same way. Since OpenFlow // doesn't specify what uniquely identifies a port the // notion of ADD vs. MODIFY can also be hazy. So we just // compare the new port to the existing ones. Map<Short,ImmutablePort> newPortByNumber = new HashMap<Short, ImmutablePort>(portsByNumber); OrderedCollection<PortChangeEvent> events = getSinglePortChanges(port); for (PortChangeEvent e: events) { switch(e.type) { case DELETE: newPortByNumber.remove(e.port.getPortNumber()); break; case ADD: if (reason != OFPortReason.OFPPR_ADD) { // weird case } // fall through case DOWN: case OTHER_UPDATE: case UP: // update or add the port in the map newPortByNumber.put(e.port.getPortNumber(), e.port); break; } } updatePortsWithNewPortsByNumber(newPortByNumber); return events; } finally { lock.writeLock().unlock(); } } /** * Given a new or modified port newPort, returns the list of * PortChangeEvents to "transform" the current ports stored by * this switch to include / represent the new port. The ports stored * by this switch are <b>NOT</b> updated. * * This method acquires the readlock and is thread-safe by itself. * Most callers will need to acquire the write lock before calling * this method though (if the caller wants to update the ports stored * by this switch) * * @param newPort the new or modified port. * @return the list of changes */ public OrderedCollection<PortChangeEvent> getSinglePortChanges(ImmutablePort newPort) { lock.readLock().lock(); try { OrderedCollection<PortChangeEvent> events = new LinkedHashSetWrapper<PortChangeEvent>(); // Check if we have a port by the same number in our // old map. ImmutablePort prevPort = portsByNumber.get(newPort.getPortNumber()); if (newPort.equals(prevPort)) { // nothing has changed return events; } if (prevPort != null && prevPort.getName().equals(newPort.getName())) { // A simple modify of a exiting port // A previous port with this number exists and it's name // also matches the new port. Find the differences if (prevPort.isEnabled() && !newPort.isEnabled()) { events.add(new PortChangeEvent(newPort, PortChangeType.DOWN)); } else if (!prevPort.isEnabled() && newPort.isEnabled()) { events.add(new PortChangeEvent(newPort, PortChangeType.UP)); } else { events.add(new PortChangeEvent(newPort, PortChangeType.OTHER_UPDATE)); } return events; } if (prevPort != null) { // There exists a previous port with the same port // number but the port name is different (otherwise we would // never have gotten here) // Remove the port. Name-number mapping(s) have changed events.add(new PortChangeEvent(prevPort, PortChangeType.DELETE)); } // We now need to check if there exists a previous port sharing // the same name as the new/updated port. prevPort = portsByName.get(newPort.getName().toLowerCase()); if (prevPort != null) { // There exists a previous port with the same port // name but the port number is different (otherwise we // never have gotten here). // Remove the port. Name-number mapping(s) have changed events.add(new PortChangeEvent(prevPort, PortChangeType.DELETE)); } // We always need to add the new port. Either no previous port // existed or we just deleted previous ports with inconsistent // name-number mappings events.add(new PortChangeEvent(newPort, PortChangeType.ADD)); return events; } finally { lock.readLock().unlock(); } } /** * Compare the current ports of this switch to the newPorts list and * return the changes that would be applied to transfort the current * ports to the new ports. No internal data structures are updated * see {@link #compareAndUpdatePorts(List, boolean)} * * @param newPorts the list of new ports * @return The list of differences between the current ports and * newPortList */ public OrderedCollection<PortChangeEvent> comparePorts(Collection<ImmutablePort> newPorts) { return compareAndUpdatePorts(newPorts, false); } /** * Compare the current ports of this switch to the newPorts list and * return the changes that would be applied to transform the current * ports to the new ports. No internal data structures are updated * see {@link #compareAndUpdatePorts(List, boolean)} * * @param newPorts the list of new ports * @return The list of differences between the current ports and * newPortList */ public OrderedCollection<PortChangeEvent> updatePorts(Collection<ImmutablePort> newPorts) { return compareAndUpdatePorts(newPorts, true); } /** * Compare the current ports stored in this switch instance with the * new port list given and return the differences in the form of * PortChangeEvents. If the doUpdate flag is true, newPortList will * replace the current list of this switch (and update the port maps) * * Implementation note: * Since this method can optionally modify the current ports and * since it's not possible to upgrade a read-lock to a write-lock * we need to hold the write-lock for the entire operation. If this * becomes a problem and if compares() are common we can consider * splitting in two methods but this requires lots of code duplication * * @param newPorts the list of new ports. * @param doUpdate If true the newPortList will replace the current * port list for this switch. If false this switch will not be changed. * @return The list of differences between the current ports and * newPorts * @throws NullPointerException if newPortsList is null * @throws IllegalArgumentException if either port names or port numbers * are duplicated in the newPortsList. */ private OrderedCollection<PortChangeEvent> compareAndUpdatePorts( Collection<ImmutablePort> newPorts, boolean doUpdate) { if (newPorts == null) { throw new NullPointerException("newPortsList must not be null"); } lock.writeLock().lock(); try { OrderedCollection<PortChangeEvent> events = new LinkedHashSetWrapper<PortChangeEvent>(); Map<Short,ImmutablePort> newPortsByNumber = new HashMap<Short, ImmutablePort>(); Map<String,ImmutablePort> newPortsByName = new HashMap<String, ImmutablePort>(); List<ImmutablePort> newEnabledPortList = new ArrayList<ImmutablePort>(); List<Short> newEnabledPortNumbers = new ArrayList<Short>(); List<ImmutablePort> newPortsList = new ArrayList<ImmutablePort>(newPorts); for (ImmutablePort p: newPortsList) { if (p == null) { throw new NullPointerException("portList must not " + "contain null values"); } // Add the port to the new maps and lists and check // that every port is unique ImmutablePort duplicatePort; duplicatePort = newPortsByNumber.put(p.getPortNumber(), p); if (duplicatePort != null) { String msg = String.format("Cannot have two ports " + "with the same number: %s <-> %s", p.toBriefString(), duplicatePort.toBriefString()); throw new IllegalArgumentException(msg); } duplicatePort = newPortsByName.put(p.getName().toLowerCase(), p); if (duplicatePort != null) { String msg = String.format("Cannot have two ports " + "with the same name: %s <-> %s", p.toBriefString(), duplicatePort.toBriefString()); throw new IllegalArgumentException(msg); } if (p.isEnabled()) { newEnabledPortList.add(p); newEnabledPortNumbers.add(p.getPortNumber()); } // get changes events.addAll(getSinglePortChanges(p)); } // find deleted ports // We need to do this after looping through all the new ports // to we can handle changed name<->number mappings correctly // We could pull it into the loop of we address this but // it's probably not worth it for (ImmutablePort oldPort: this.portList) { if (!newPortsByNumber.containsKey(oldPort.getPortNumber())) { PortChangeEvent ev = new PortChangeEvent(oldPort, PortChangeType.DELETE); events.add(ev); } } if (doUpdate) { portsByName = Collections.unmodifiableMap(newPortsByName); portsByNumber = Collections.unmodifiableMap(newPortsByNumber); enabledPortList = Collections.unmodifiableList(newEnabledPortList); enabledPortNumbers = Collections.unmodifiableList(newEnabledPortNumbers); portList = Collections.unmodifiableList(newPortsList); } return events; } finally { lock.writeLock().unlock(); } } public ImmutablePort getPort(String name) { if (name == null) { throw new NullPointerException("Port name must not be null"); } lock.readLock().lock(); try { return portsByName.get(name.toLowerCase()); } finally { lock.readLock().unlock(); } } public ImmutablePort getPort(Short portNumber) { lock.readLock().lock(); try { return portsByNumber.get(portNumber); } finally { lock.readLock().unlock(); } } public List<ImmutablePort> getPorts() { lock.readLock().lock(); try { return portList; } finally { lock.readLock().unlock(); } } public List<ImmutablePort> getEnabledPorts() { lock.readLock().lock(); try { return enabledPortList; } finally { lock.readLock().unlock(); } } public List<Short> getEnabledPortNumbers() { lock.readLock().lock(); try { return enabledPortNumbers; } finally { lock.readLock().unlock(); } } } @Override public boolean attributeEquals(String name, Object other) { Object attr = this.attributes.get(name); if (attr == null) return false; return attr.equals(other); } @Override public Object getAttribute(String name) { // returns null if key doesn't exist return this.attributes.get(name); } @Override public void setAttribute(String name, Object value) { this.attributes.put(name, value); return; } @Override public Object removeAttribute(String name) { return this.attributes.remove(name); } @Override public boolean hasAttribute(String name) { return this.attributes.containsKey(name); } @Override @JsonIgnore public void setChannel(Channel channel) { this.channel = channel; } // For driver subclass to set throttling protected void enableWriteThrottle(boolean enable) { this.writeThrottleEnabled = enable; } @Override public boolean isWriteThrottleEnabled() { return this.writeThrottleEnabled; } @Override @LogMessageDocs({ @LogMessageDoc(level="WARN", message="Drop throttled OF message to switch {switch}", explanation="The controller is sending more messages" + "than the switch can handle. Some messages are dropped" + "to prevent switch outage", recommendation=LogMessageDoc.REPORT_CONTROLLER_BUG) }) public void writeThrottled(OFMessage m, FloodlightContext bc) throws IOException { if (channel == null || !isConnected()) return; /** * By default, channel uses an unbounded send queue. Enable throttling * prevents the queue from growing big. * * channel.isWritable() returns true when queue length is less than * high water mark (64 kbytes). Once exceeded, isWritable() becomes * false after queue length drops below low water mark (32 kbytes). */ if (!writeThrottleEnabled || channel.isWritable()) { write(m, bc); } else { // Let logback duplicate filtering take care of excessive logs ctrSwitchWriteDrops.updateCounterNoFlush(); log.warn("Drop throttled OF message to switch {}", this); } } @Override public void writeThrottled(List<OFMessage> msglist, FloodlightContext bc) throws IOException { if (!writeThrottleEnabled || channel.isWritable()) { write(msglist, bc); } else { // Let logback duplicate filtering take care of excessive logs ctrSwitchWriteDrops.updateCounterNoFlush(msglist.size()); log.warn("Drop throttled OF messages to switch {}", this); } } @Override public void write(OFMessage m, FloodlightContext bc) { if (channel == null || !isConnected()) return; //throws IOException { Map<IOFSwitch,List<OFMessage>> msg_buffer_map = local_msg_buffer.get(); List<OFMessage> msg_buffer = msg_buffer_map.get(this); if (msg_buffer == null) { msg_buffer = new ArrayList<OFMessage>(); msg_buffer_map.put(this, msg_buffer); } this.floodlightProvider.handleOutgoingMessage(this, m, bc); msg_buffer.add(m); if ((msg_buffer.size() >= Controller.BATCH_MAX_SIZE) || ((m.getType() != OFType.PACKET_OUT) && (m.getType() != OFType.FLOW_MOD))) { this.write(msg_buffer); msg_buffer.clear(); } } @Override @LogMessageDoc(level="WARN", message="Sending OF message that modifies switch " + "state while in the slave role: {switch}", explanation="An application has sent a message to a switch " + "that is not valid when the switch is in a slave role", recommendation=LogMessageDoc.REPORT_CONTROLLER_BUG) public void write(List<OFMessage> msglist, FloodlightContext bc) { if (channel == null || !isConnected()) return; for (OFMessage m : msglist) { if (role == Role.SLAVE) { switch (m.getType()) { case PACKET_OUT: case FLOW_MOD: case PORT_MOD: log.warn("Sending OF message that modifies switch " + "state while in the slave role: {}", m.getType().name()); break; default: break; } } this.floodlightProvider.handleOutgoingMessage(this, m, bc); } this.write(msglist); } /** * Not callable by writers, but allow IOFSwitch implementation to override * @param msglist * @throws IOException */ protected void write(List<OFMessage> msglist) { if (channel == null || !isConnected()) return; this.channel.write(msglist); } @Override public void disconnectOutputStream() { if (channel == null) return; channel.close(); } @Override @JsonIgnore public void setFeaturesReply(OFFeaturesReply featuresReply) { if (stringId == null) { /* ports are updated via port status message, so we * only fill in ports on initial connection. */ List<ImmutablePort> immutablePorts = ImmutablePort .immutablePortListOf(featuresReply.getPorts()); portManager.updatePorts(immutablePorts); } this.datapathId = featuresReply.getDatapathId(); this.stringId = HexString.toHexString(featuresReply.getDatapathId()); this.capabilities = featuresReply.getCapabilities(); this.buffers = featuresReply.getBuffers(); this.actions = featuresReply.getActions(); this.tables = featuresReply.getTables(); } @Override @JsonIgnore public Collection<ImmutablePort> getEnabledPorts() { return portManager.getEnabledPorts(); } @Override @JsonIgnore public Collection<Short> getEnabledPortNumbers() { return portManager.getEnabledPortNumbers(); } @Override public ImmutablePort getPort(short portNumber) { return portManager.getPort(portNumber); } @Override public ImmutablePort getPort(String portName) { return portManager.getPort(portName); } @Override @JsonIgnore public OrderedCollection<PortChangeEvent> processOFPortStatus(OFPortStatus ps) { return portManager.handlePortStatusMessage(ps); } @Override @JsonProperty("ports") public Collection<ImmutablePort> getPorts() { return portManager.getPorts(); } @Override public OrderedCollection<PortChangeEvent> comparePorts(Collection<ImmutablePort> ports) { return portManager.comparePorts(ports); } @Override @JsonIgnore public OrderedCollection<PortChangeEvent> setPorts(Collection<ImmutablePort> ports) { return portManager.updatePorts(ports); } @Override public boolean portEnabled(short portNumber) { ImmutablePort p = portManager.getPort(portNumber); if (p == null) return false; return p.isEnabled(); } @Override public boolean portEnabled(String portName) { ImmutablePort p = portManager.getPort(portName); if (p == null) return false; return p.isEnabled(); } @Override @JsonSerialize(using=DPIDSerializer.class) @JsonProperty("dpid") public long getId() { if (this.stringId == null) throw new RuntimeException("Features reply has not yet been set"); return this.datapathId; } @JsonIgnore @Override public String getStringId() { return stringId; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { String channelString = (channel != null) ? channel.getRemoteAddress().toString() : "?"; return "OFSwitchBase [" + channelString + " DPID[" + ((stringId != null) ? stringId : "?") + "]]"; } @Override public ConcurrentMap<Object, Object> getAttributes() { return this.attributes; } @Override public Date getConnectedSince() { return connectedSince; } @JsonIgnore @Override public int getNextTransactionId() { return this.transactionIdSource.incrementAndGet(); } @Override public void sendStatsQuery(OFStatisticsRequest request, int xid, IOFMessageListener caller) throws IOException { request.setXid(xid); this.iofMsgListenersMap.put(xid, caller); List<OFMessage> msglist = new ArrayList<OFMessage>(1); msglist.add(request); this.write(msglist); return; } @Override public Future<List<OFStatistics>> queryStatistics(OFStatisticsRequest request) throws IOException { request.setXid(getNextTransactionId()); OFStatisticsFuture future = new OFStatisticsFuture(threadPool, this, request.getXid()); this.statsFutureMap.put(request.getXid(), future); List<OFMessage> msglist = new ArrayList<OFMessage>(1); msglist.add(request); this.write(msglist); return future; } @Override public void deliverStatisticsReply(OFStatisticsReply reply) { checkForTableStats(reply); OFStatisticsFuture future = this.statsFutureMap.get(reply.getXid()); if (future != null) { future.deliverFuture(this, reply); // The future will ultimately unregister itself and call // cancelStatisticsReply return; } /* Transaction id was not found in statsFutureMap.check the other map */ IOFMessageListener caller = this.iofMsgListenersMap.get(reply.getXid()); if (caller != null) { caller.receive(this, reply, null); } } @LogMessageDocs({ @LogMessageDoc(level="INFO", message="Switch {switch} flow table is full", explanation="The switch flow table at least 98% full, " + "this requires attention if using reactive flow setup"), @LogMessageDoc(level="INFO", message="Switch {switch} flow table capacity back to normal", explanation="The switch flow table is less than 90% full") }) private void checkForTableStats(OFStatisticsReply statReply) { if (statReply.getStatisticType() != OFStatisticsType.TABLE) { return; } List<? extends OFStatistics> stats = statReply.getStatistics(); // Assume a single table only OFStatistics stat = stats.get(0); if (stat instanceof OFTableStatistics) { OFTableStatistics tableStat = (OFTableStatistics) stat; int activeCount = tableStat.getActiveCount(); int maxEntry = tableStat.getMaximumEntries(); log.debug("Switch {} active entries {} max entries {}", new Object[] { this.stringId, activeCount, maxEntry}); int percentFull = activeCount * 100 / maxEntry; if (flowTableFull && percentFull < 90) { log.info("Switch {} flow table capacity is back to normal", toString()); floodlightProvider.addSwitchEvent(this.datapathId, "SWITCH_FLOW_TABLE_NORMAL < 90% full", false); } else if (percentFull >= 98) { log.info("Switch {} flow table is almost full", toString()); floodlightProvider.addSwitchEvent(this.datapathId, "SWITCH_FLOW_TABLE_ALMOST_FULL >= 98% full", false); } } } @Override public void cancelStatisticsReply(int transactionId) { if (null == this.statsFutureMap.remove(transactionId)) { this.iofMsgListenersMap.remove(transactionId); } } @Override public void cancelAllStatisticsReplies() { /* we don't need to be synchronized here. Even if another thread * modifies the map while we're cleaning up the future will eventuall * timeout */ for (OFStatisticsFuture f : statsFutureMap.values()) { f.cancel(true); } statsFutureMap.clear(); iofMsgListenersMap.clear(); } /** * @param floodlightProvider the floodlightProvider to set */ @JsonIgnore public void setFloodlightProvider( IFloodlightProviderService floodlightProvider) { this.floodlightProvider = floodlightProvider; } @Override @JsonIgnore public void setThreadPoolService(IThreadPoolService tp) { this.threadPool = tp; } @Override @JsonIgnore public void setDebugCounterService(IDebugCounterService debugCounters) throws CounterException { this.debugCounters = debugCounters; registerOverloadCounters(); } @JsonIgnore @Override public boolean isConnected() { // no lock needed since we use volatile return connected; } @JsonIgnore @Override public boolean isActive() { // no lock needed since we use volatile return isConnected() && this.role == Role.MASTER; } @Override @JsonIgnore public void setConnected(boolean connected) { // No lock needed since we use volatile if (connected && this.connectedSince == null) this.connectedSince = new Date(); else if (!connected) this.connectedSince = null; this.connected = connected; } @Override public Role getHARole() { return role; } @JsonIgnore @Override public void setHARole(Role role) { this.role = role; } @LogMessageDoc(level="INFO", message="Switch {switch} flow cleared", explanation="The switch flow table has been cleared, " + "this normally happens on switch connection") @Override public void clearAllFlowMods() { if (channel == null || !isConnected()) return; // Delete all pre-existing flows log.info("Clearing all flows on switch {}", this); OFMatch match = new OFMatch().setWildcards(OFMatch.OFPFW_ALL); OFMessage fm = ((OFFlowMod) floodlightProvider.getOFMessageFactory() .getMessage(OFType.FLOW_MOD)) .setMatch(match) .setCommand(OFFlowMod.OFPFC_DELETE) .setOutPort(OFPort.OFPP_NONE) .setLength(U16.t(OFFlowMod.MINIMUM_LENGTH)); fm.setXid(getNextTransactionId()); OFMessage barrierMsg = floodlightProvider.getOFMessageFactory().getMessage( OFType.BARRIER_REQUEST); barrierMsg.setXid(getNextTransactionId()); List<OFMessage> msglist = new ArrayList<OFMessage>(2); msglist.add(fm); msglist.add(barrierMsg); channel.write(msglist); } @Override public boolean updateBroadcastCache(Long entry, Short port) { if (timedCache.update(entry)) { AtomicLong count = portBroadcastCacheHitMap.get(port); if(count == null) { AtomicLong newCount = new AtomicLong(0); AtomicLong retrieved; if((retrieved = portBroadcastCacheHitMap.putIfAbsent(port, newCount)) == null ) { count = newCount; } else { count = retrieved; } } count.incrementAndGet(); return true; } else { return false; } } @Override @JsonIgnore public Map<Short, Long> getPortBroadcastHits() { Map<Short, Long> res = new HashMap<Short, Long>(); for (Map.Entry<Short, AtomicLong> entry : portBroadcastCacheHitMap.entrySet()) { res.put(entry.getKey(), entry.getValue().get()); } return res; } @Override public void flush() { Map<IOFSwitch,List<OFMessage>> msg_buffer_map = local_msg_buffer.get(); List<OFMessage> msglist = msg_buffer_map.get(this); if ((msglist != null) && (msglist.size() > 0)) { /* ============================ BIG CAVEAT =============================== * This code currently works, but relies on undocumented behavior of * netty. * * The method org.jboss.netty.channel.Channel.write(Object) * (invoked from this.write(List<OFMessage> msg) is currently * documented to be <emph>asynchronous</emph>. If the method /were/ truely * asynchronous, this would break our code (because we are clearing the * msglist right after calling write. * * For now, Netty actually invokes the conversion pipeline before doing * anything asynchronous, so we are safe. But we should probably change * that behavior. */ this.write(msglist); msglist.clear(); } } public static void flush_all() { Map<IOFSwitch,List<OFMessage>> msg_buffer_map = local_msg_buffer.get(); for (IOFSwitch sw : msg_buffer_map.keySet()) { sw.flush(); } } /** * Get the IP Address for the switch * @return the inet address */ @Override @JsonSerialize(using=ToStringSerializer.class) public SocketAddress getInetAddress() { if (channel == null) return null; return channel.getRemoteAddress(); } @Override public Future<OFFeaturesReply> querySwitchFeaturesReply() throws IOException { OFMessage request = floodlightProvider.getOFMessageFactory(). getMessage(OFType.FEATURES_REQUEST); request.setXid(getNextTransactionId()); OFFeaturesReplyFuture future = new OFFeaturesReplyFuture(threadPool, this, request.getXid()); this.featuresFutureMap.put(request.getXid(), future); List<OFMessage> msglist = new ArrayList<OFMessage>(1); msglist.add(request); this.write(msglist); return future; } @Override public void deliverOFFeaturesReply(OFMessage reply) { OFFeaturesReplyFuture future = this.featuresFutureMap.get(reply.getXid()); if (future != null) { future.deliverFuture(this, reply); // The future will ultimately unregister itself and call // cancelFeaturesReply return; } log.error("Switch {}: received unexpected featureReply", this); } @Override public void cancelFeaturesReply(int transactionId) { this.featuresFutureMap.remove(transactionId); } @Override public int getBuffers() { return buffers; } @Override public int getActions() { return actions; } @Override public int getCapabilities() { return capabilities; } @Override public byte getTables() { return tables; } @Override public OFDescriptionStatistics getDescriptionStatistics() { return new OFDescriptionStatistics(description); } @Override public void setFloodlightProvider(Controller controller) { floodlightProvider = controller; } /** * For switch drivers to set thresholds, all rates in per second * @param pktInHigh - above this start throttling * @param pktInLow - below this stop throttling * @param pktInPerMac - block host if unique pktIn rate reaches this * @param pktInPerPort - block port if unique pktIn rate reaches this */ @JsonIgnore protected void setInputThrottleThresholds(int pktInHigh, int pktInLow, int pktInPerMac, int pktInPerPort) { packetInRateThresholdHigh = pktInHigh; packetInRateThresholdLow = pktInLow; packetInRatePerMacThreshold = pktInPerMac; packetInRatePerPortThreshold = pktInPerPort; } /** * Return if switch has exceeded the high threshold of packet in rate. * @return */ @Override public boolean isOverloaded() { return packetInThrottleEnabled; } /** * Determine if this message should be dropped. * * We compute the current rate by taking a timestamp every 100 messages. * Could change to a more complex scheme if more accuracy is needed. * * Enable throttling if the rate goes above packetInRateThresholdHigh * Disable throttling when the rate drops below packetInRateThresholdLow * * While throttling is enabled, we do the following: * - Remove duplicate packetIn's mapped to the same OFMatch * - After filtering, if packetIn rate per host (mac) is above * packetInRatePerMacThreshold, push a flow mod to block mac on port * - After filtering, if packetIn rate per port is above * packetInRatePerPortThreshold, push a flow mod to block port * - Allow blocking flow mods have a hard timeout and expires automatically * * TODO: keep a history of all events related in input throttling * * @param ofm * @return */ @Override public boolean inputThrottled(OFMessage ofm) { if (ofm.getType() != OFType.PACKET_IN) { return false; } ctrSwitchPktin.updateCounterNoFlush(); // Compute current packet in rate messageCount++; if (messageCount % 1000 == 0) { long now = System.currentTimeMillis(); if (now != lastMessageTime) { currentRate = (int) (1000000 / (now - lastMessageTime)); lastMessageTime = now; } else { currentRate = Integer.MAX_VALUE; } } if (!packetInThrottleEnabled) { if (currentRate <= packetInRateThresholdHigh) { return false; // most common case } enablePacketInThrottle(); } else if (currentRate < packetInRateThresholdLow) { disablePacketInThrottle(); return false; } // Now we are in the slow path where we need to do filtering // First filter based on OFMatch OFPacketIn pin = (OFPacketIn)ofm; OFMatch match = new OFMatch(); match.loadFromPacket(pin.getPacketData(), pin.getInPort()); if (ofMatchCache.update(match)) { ctrSwitchPktinDrops.updateCounterNoFlush(); return true; } // We have packet in with a distinct flow, check per mac rate messageCountUniqueOFMatch++; if ((messageCountUniqueOFMatch % packetInRatePerMacThreshold) == 1) { checkPerSourceMacRate(pin); } // Check per port rate if ((messageCountUniqueOFMatch % packetInRatePerPortThreshold) == 1) { checkPerPortRate(pin); } return false; } /** * We rely on the fact that packet in processing is single threaded * per packet-in, so no locking is necessary. */ private void disablePacketInThrottle() { ofMatchCache = null; macCache = null; macBlockedCache = null; portCache = null; portBlockedCache = null; packetInThrottleEnabled = false; floodlightProvider.addSwitchEvent(this.datapathId, "SWITCH_OVERLOAD_THROTTLE_DISABLED ==>" + "Pktin rate " + currentRate + "/s", false); log.info("Packet in rate is {}, disable throttling on {}", currentRate, this); } private void enablePacketInThrottle() { ofMatchCache = new TimedCache<OFMatch>(2048, 5000); // 5 second interval macCache = new TimedCache<Long>(64, 1000 ); // remember last second macBlockedCache = new TimedCache<Long>(256, 5000 ); // 5 second interval portCache = new TimedCache<Short>(16, 1000 ); // rememeber last second portBlockedCache = new TimedCache<Short>(64, 5000 ); // 5 second interval packetInThrottleEnabled = true; messageCountUniqueOFMatch = 0; floodlightProvider.addSwitchEvent(this.datapathId, "SWITCH_OVERLOAD_THROTTLE_ENABLED ==>" + "Pktin rate " + currentRate + "/s", false); log.info("Packet in rate is {}, enable throttling on {}", currentRate, this); } private void registerOverloadCounters() throws CounterException { if (debugCountersRegistered) { return; } if (debugCounters == null) { log.error("Debug Counter Service not found"); debugCounters = new NullDebugCounter(); debugCountersRegistered = true; } // every level of the hierarchical counter has to be registered // even if they are not used ctrSwitch = debugCounters.registerCounter( PACKAGE , stringId, "Counter for this switch", CounterType.ALWAYS_COUNT); ctrSwitchPktin = debugCounters.registerCounter( PACKAGE, stringId + "/pktin", "Packet in counter for this switch", CounterType.ALWAYS_COUNT); ctrSwitchWrite = debugCounters.registerCounter( PACKAGE, stringId + "/write", "Write counter for this switch", CounterType.ALWAYS_COUNT); ctrSwitchPktinDrops = debugCounters.registerCounter( PACKAGE, stringId + "/pktin/drops", "Packet in throttle drop count", CounterType.ALWAYS_COUNT, IDebugCounterService.CTR_MDATA_WARN); ctrSwitchWriteDrops = debugCounters.registerCounter( PACKAGE, stringId + "/write/drops", "Switch write throttle drop count", CounterType.ALWAYS_COUNT, IDebugCounterService.CTR_MDATA_WARN); } /** * Check if we have sampled this mac in the last second. * Since we check every packetInRatePerMacThreshold packets, * the presence of the mac in the macCache means the rate is * above the threshold in a statistical sense. * * Take care not to block topology probing packets. Also don't * push blocking flow mod if we have already done so within the * last 5 seconds. * * @param pin * @return */ private void checkPerSourceMacRate(OFPacketIn pin) { byte[] data = pin.getPacketData(); byte[] mac = Arrays.copyOfRange(data, 6, 12); MACAddress srcMac = MACAddress.valueOf(mac); short ethType = (short) (((data[12] & 0xff) << 8) + (data[13] & 0xff)); if (ethType != Ethernet.TYPE_LLDP && ethType != Ethernet.TYPE_BSN && macCache.update(srcMac.toLong())) { // Check if we already pushed a flow in the last 5 seconds if (macBlockedCache.update(srcMac.toLong())) { return; } // write out drop flow per srcMac int port = pin.getInPort(); SwitchPort swPort = new SwitchPort(getId(), port); ForwardingBase.blockHost(floodlightProvider, swPort, srcMac.toLong(), (short) 5, AppCookie.makeCookie(OFSWITCH_APP_ID, 0)); floodlightProvider.addSwitchEvent(this.datapathId, "SWITCH_PORT_BLOCKED_TEMPORARILY " + "OFPort " + port + " mac " + srcMac, false); log.info("Excessive packet in from {} on {}, block host for 5 sec", srcMac.toString(), swPort); } } /** * Works in a similar way as checkPerSourceMacRate(). * * TODO Don't block ports with links? * * @param pin * @return */ private void checkPerPortRate(OFPacketIn pin) { Short port = pin.getInPort(); if (portCache.update(port)) { // Check if we already pushed a flow in the last 5 seconds if (portBlockedCache.update(port)) { return; } // write out drop flow per port SwitchPort swPort = new SwitchPort(getId(), port); ForwardingBase.blockHost(floodlightProvider, swPort, -1L, (short) 5, AppCookie.makeCookie(OFSWITCH_APP_ID, 1)); floodlightProvider.addSwitchEvent(this.datapathId, "SWITCH_PORT_BLOCKED_TEMPORARILY " + "OFPort " + port, false); log.info("Excessive packet in from {}, block port for 5 sec", swPort); } } @Override @JsonIgnore @LogMessageDoc(level="WARN", message="Switch {switch} flow table is full", explanation="The controller received flow table full " + "message from the switch, could be caused by increased " + "traffic pattern", recommendation=LogMessageDoc.REPORT_CONTROLLER_BUG) public void setTableFull(boolean isFull) { if (isFull && !flowTableFull) { floodlightProvider.addSwitchEvent(this.datapathId, "SWITCH_FLOW_TABLE_FULL " + "Table full error from switch", false); log.warn("Switch {} flow table is full", stringId); } flowTableFull = isFull; } @Override public short getAccessFlowPriority() { return accessFlowPriority; } @Override public short getCoreFlowPriority() { return coreFlowPriority; } @Override public void setAccessFlowPriority(short accessFlowPriority) { this.accessFlowPriority = accessFlowPriority; } @Override public void setCoreFlowPriority(short coreFlowPriority) { this.coreFlowPriority = coreFlowPriority; } @Override public void startDriverHandshake() { if (startDriverHandshakeCalled) throw new SwitchDriverSubHandshakeAlreadyStarted(); startDriverHandshakeCalled = true; } @Override public boolean isDriverHandshakeComplete() { if (!startDriverHandshakeCalled) throw new SwitchDriverSubHandshakeNotStarted(); return true; } @Override public void processDriverHandshakeMessage(OFMessage m) { if (startDriverHandshakeCalled) throw new SwitchDriverSubHandshakeCompleted(m); else throw new SwitchDriverSubHandshakeNotStarted(); } }