/* * Copyright (c) 2012,2013 Big Switch Networks, Inc. * * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/legal/epl-v10.html * * 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. * * This file incorporates work covered by the following copyright and * permission notice: * * 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 org.sdnplatform.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.atomic.AtomicLong; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; 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.OFPhysicalPort.OFPortConfig; import org.openflow.protocol.OFPhysicalPort.OFPortState; import org.openflow.protocol.OFPort; import org.openflow.protocol.OFStatisticsRequest; import org.openflow.protocol.OFType; import org.openflow.protocol.statistics.OFStatistics; import org.openflow.util.HexString; import org.openflow.util.U16; import org.sdnplatform.core.IControllerService.Role; import org.sdnplatform.core.annotations.LogMessageDoc; import org.sdnplatform.core.internal.Controller; import org.sdnplatform.core.internal.OFFeaturesReplyFuture; import org.sdnplatform.core.internal.OFStatisticsFuture; import org.sdnplatform.core.web.serializers.DPIDSerializer; import org.sdnplatform.threadpool.IThreadPoolService; import org.sdnplatform.util.TimedCache; 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 IControllerService controllerProvider; 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 final 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 final ConcurrentHashMap<String, OFPhysicalPort> portsByName; private final Map<Integer,OFStatisticsFuture> statsFutureMap; private final Map<Integer, IOFMessageListener> iofMsgListenersMap; private final Map<Integer,OFFeaturesReplyFuture> featuresFutureMap; private volatile boolean connected; private final TimedCache<Long> timedCache; private final ReentrantReadWriteLock listenerLock; private final ConcurrentMap<Short, AtomicLong> portBroadcastCacheHitMap; 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>>(); } }; // 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, AtomicLong>(); // 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, ListenerContext 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.controllerProvider.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, ListenerContext 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.controllerProvider.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 controllerProvider the controllerProvider to set */ @JsonIgnore public void setControllerProvider( IControllerService controllerProvider) { this.controllerProvider = controllerProvider; } @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) controllerProvider.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 = controllerProvider.getOFMessageFactory().getMessage( OFType.BARRIER_REQUEST); barrierMsg.setXid(getNextTransactionId()); try { List<OFMessage> msglist = new ArrayList<OFMessage>(1); msglist.add(fm); channel.write(msglist); msglist = new ArrayList<OFMessage>(1); msglist.add(barrierMsg); 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)) { 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. */ try { this.write(msglist); } catch (IOException e) { log.error("Error flushing local message buffer: "+e.getMessage(), e); } 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(); } /** * @return The remote SocketAddress of the channel */ @Override @JsonSerialize(using=ToStringSerializer.class) public SocketAddress getInetAddress() { return channel.getRemoteAddress(); } @Override public Future<OFFeaturesReply> querySwitchFeaturesReply() throws IOException { OFMessage request = controllerProvider.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 setControllerProvider(Controller controller) { controllerProvider = controller; } }