/** * 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.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.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import net.floodlightcontroller.core.FloodlightContext; import net.floodlightcontroller.core.IFloodlightProviderService; import net.floodlightcontroller.core.IOFMessageListener; import net.floodlightcontroller.core.IFloodlightProviderService.Role; import net.floodlightcontroller.core.IOFSwitch; import net.floodlightcontroller.core.annotations.LogMessageDoc; import net.floodlightcontroller.core.internal.Controller; import net.floodlightcontroller.core.internal.OFFeaturesReplyFuture; import net.floodlightcontroller.core.internal.OFStatisticsFuture; import net.floodlightcontroller.core.web.serializers.DPIDSerializer; import net.floodlightcontroller.threadpool.IThreadPoolService; import net.floodlightcontroller.util.TimedCache; import org.codehaus.jackson.annotate.JsonIgnore; import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.map.annotate.JsonSerialize; import org.codehaus.jackson.map.ser.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.OFPhysicalPort; import org.openflow.protocol.OFPort; import org.openflow.protocol.OFType; import org.openflow.protocol.OFPhysicalPort.OFPortConfig; import org.openflow.protocol.OFPhysicalPort.OFPortState; import org.openflow.protocol.OFStatisticsRequest; import org.openflow.protocol.statistics.OFStatistics; 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 Logger log = LoggerFactory.getLogger(OFSwitchBase.class); protected ConcurrentMap<Object, Object> attributes; protected IFloodlightProviderService floodlightProvider; protected IThreadPoolService threadPool; protected Date connectedSince; /* Switch features from initial featuresReply */ protected int capabilities; protected int buffers; protected int actions; protected byte tables; protected long datapathId; protected Role role; protected String stringId; /** * Members hidden from subclasses */ private Channel channel; private AtomicInteger transactionIdSource; // Lock to protect modification of the port maps. We only need to // synchronize on modifications. For read operations we are fine since // we rely on ConcurrentMaps which works for our use case. protected Object portLock; // Map port numbers to the appropriate OFPhysicalPort protected ConcurrentHashMap<Short, OFPhysicalPort> portsByNumber; // Map port names to the appropriate OFPhyiscalPort // XXX: The OF spec doesn't specify if port names need to be unique but // according it's always the case in practice. private ConcurrentHashMap<String, OFPhysicalPort> portsByName; private Map<Integer,OFStatisticsFuture> statsFutureMap; private Map<Integer, IOFMessageListener> iofMsgListenersMap; private Map<Integer,OFFeaturesReplyFuture> featuresFutureMap; private volatile boolean connected; private TimedCache<Long> timedCache; private ReentrantReadWriteLock listenerLock; private ConcurrentMap<Short, Long> portBroadcastCacheHitMap; protected 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>>(); } }; // for managing our map sizes protected static int MAX_MACS_PER_SWITCH = 1000; public OFSwitchBase() { this.stringId = null; this.attributes = new ConcurrentHashMap<Object, Object>(); this.connectedSince = new Date(); this.transactionIdSource = new AtomicInteger(); this.portLock = new Object(); this.portsByNumber = new ConcurrentHashMap<Short, OFPhysicalPort>(); this.portsByName = new ConcurrentHashMap<String, OFPhysicalPort>(); this.connected = true; 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.listenerLock = new ReentrantReadWriteLock(); this.portBroadcastCacheHitMap = new ConcurrentHashMap<Short, Long>(); // Defaults properties for an ideal switch this.setAttribute(PROP_FASTWILDCARDS, OFMatch.OFPFW_ALL); this.setAttribute(PROP_SUPPORTS_OFPP_FLOOD, new Boolean(true)); this.setAttribute(PROP_SUPPORTS_OFPP_TABLE, new Boolean(true)); } @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; } @Override public void write(OFMessage m, FloodlightContext bc) 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) throws IOException { 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); } private void write(List<OFMessage> msglist) throws IOException { this.channel.write(msglist); } @Override public void disconnectOutputStream() { channel.close(); } @Override @JsonIgnore public void setFeaturesReply(OFFeaturesReply featuresReply) { synchronized(portLock) { if (stringId == null) { /* ports are updated via port status message, so we * only fill in ports on initial connection. */ for (OFPhysicalPort port : featuresReply.getPorts()) { setPort(port); } } this.datapathId = featuresReply.getDatapathId(); this.capabilities = featuresReply.getCapabilities(); this.buffers = featuresReply.getBuffers(); this.actions = featuresReply.getActions(); this.tables = featuresReply.getTables(); this.stringId = HexString.toHexString(this.datapathId); } } @Override @JsonIgnore public Collection<OFPhysicalPort> getEnabledPorts() { List<OFPhysicalPort> result = new ArrayList<OFPhysicalPort>(); for (OFPhysicalPort port : portsByNumber.values()) { if (portEnabled(port)) { result.add(port); } } return result; } @Override @JsonIgnore public Collection<Short> getEnabledPortNumbers() { List<Short> result = new ArrayList<Short>(); for (OFPhysicalPort port : portsByNumber.values()) { if (portEnabled(port)) { result.add(port.getPortNumber()); } } return result; } @Override public OFPhysicalPort getPort(short portNumber) { return portsByNumber.get(portNumber); } @Override public OFPhysicalPort getPort(String portName) { return portsByName.get(portName); } @Override @JsonIgnore public void setPort(OFPhysicalPort port) { synchronized(portLock) { portsByNumber.put(port.getPortNumber(), port); portsByName.put(port.getName(), port); } } @Override @JsonProperty("ports") public Collection<OFPhysicalPort> getPorts() { return Collections.unmodifiableCollection(portsByNumber.values()); } @Override public void deletePort(short portNumber) { synchronized(portLock) { portsByName.remove(portsByNumber.get(portNumber).getName()); portsByNumber.remove(portNumber); } } @Override public void deletePort(String portName) { synchronized(portLock) { portsByNumber.remove(portsByName.get(portName).getPortNumber()); portsByName.remove(portName); } } @Override public boolean portEnabled(short portNumber) { if (portsByNumber.get(portNumber) == null) return false; return portEnabled(portsByNumber.get(portNumber)); } @Override public boolean portEnabled(String portName) { if (portsByName.get(portName) == null) return false; return portEnabled(portsByName.get(portName)); } @Override public boolean portEnabled(OFPhysicalPort port) { if (port == null) return false; if ((port.getConfig() & OFPortConfig.OFPPC_PORT_DOWN.getValue()) > 0) return false; if ((port.getState() & OFPortState.OFPPS_LINK_DOWN.getValue()) > 0) return false; // Port STP state doesn't work with multiple VLANs, so ignore it for now //if ((port.getState() & OFPortState.OFPPS_STP_MASK.getValue()) == OFPortState.OFPPS_STP_BLOCK.getValue()) // return false; return true; } @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() { return "OFSwitchBase [" + channel.getRemoteAddress() + " 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.channel.write(msglist); return; } @Override public Future<List<OFStatistics>> getStatistics(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.channel.write(msglist); return future; } @Override public void deliverStatisticsReply(OFMessage 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); } } @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; } @JsonIgnore @Override public boolean isConnected() { // No lock needed since we use volatile return connected; } @Override @JsonIgnore public void setConnected(boolean connected) { // No lock needed since we use volatile this.connected = connected; } @Override public Role getHARole() { return role; } @JsonIgnore @Override public void setHARole(Role role, boolean replyReceived) { if (this.role == null && getAttribute(SWITCH_SUPPORTS_NX_ROLE) == null) { // The first role reply we received. Set the attribute // that the switch supports roles setAttribute(SWITCH_SUPPORTS_NX_ROLE, replyReceived); } this.role = role; } @Override @LogMessageDoc(level="ERROR", message="Failed to clear all flows on switch {switch}", explanation="An I/O error occured while trying to clear " + "flows on the switch.", recommendation=LogMessageDoc.CHECK_SWITCH) public void clearAllFlowMods() { // Delete all pre-existing flows 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)); try { List<OFMessage> msglist = new ArrayList<OFMessage>(1); msglist.add(fm); channel.write(msglist); } catch (Exception e) { log.error("Failed to clear all flows on switch " + this, e); } } @Override public boolean updateBroadcastCache(Long entry, Short port) { if (timedCache.update(entry)) { Long count = portBroadcastCacheHitMap.putIfAbsent(port, new Long(1)); if (count != null) { count++; } return true; } else { return false; } } @Override @JsonIgnore public Map<Short, Long> getPortBroadcastHits() { return this.portBroadcastCacheHitMap; } @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)) { try { this.write(msglist); } catch (IOException e) { // TODO: log exception e.printStackTrace(); } 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(); } } /** * Return a read lock that must be held while calling the listeners for * messages from the switch. Holding the read lock prevents the active * switch list from being modified out from under the listeners. * @return */ @Override @JsonIgnore public Lock getListenerReadLock() { return listenerLock.readLock(); } /** * Return a write lock that must be held when the controllers modifies the * list of active switches. This is to ensure that the active switch list * doesn't change out from under the listeners as they are handling a * message from the switch. * @return */ @Override @JsonIgnore public Lock getListenerWriteLock() { return listenerLock.writeLock(); } /** * Get the IP Address for the switch * @return the inet address */ @Override @JsonSerialize(using=ToStringSerializer.class) public SocketAddress getInetAddress() { 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.channel.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 void setFloodlightProvider(Controller controller) { floodlightProvider = controller; } }