blob: 005708d135b1b9f04e9539a58952e6a4c3318711 [file] [log] [blame]
/**
* Copyright 2011, 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.
**/
/**
* Floodlight
* A BSD licensed, Java based OpenFlow controller
*
* Floodlight is a Java based OpenFlow controller originally written by David Erickson at Stanford
* University. It is available under the BSD license.
*
* For documentation, forums, issue tracking and more visit:
*
* http://www.openflowhub.org/display/Floodlight/Floodlight+Home
**/
package net.floodlightcontroller.learningswitch;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import net.floodlightcontroller.core.FloodlightContext;
import net.floodlightcontroller.core.IFloodlightProviderService;
import net.floodlightcontroller.core.IOFMessageListener;
import net.floodlightcontroller.core.IOFSwitch;
import net.floodlightcontroller.core.module.FloodlightModuleContext;
import net.floodlightcontroller.core.module.FloodlightModuleException;
import net.floodlightcontroller.core.module.IFloodlightModule;
import net.floodlightcontroller.core.module.IFloodlightService;
import net.floodlightcontroller.core.types.MacVlanPair;
import net.floodlightcontroller.counter.ICounterStoreService;
import net.floodlightcontroller.packet.Ethernet;
import net.floodlightcontroller.restserver.IRestApiService;
import org.openflow.protocol.OFError;
import org.openflow.protocol.OFFlowMod;
import org.openflow.protocol.OFFlowRemoved;
import org.openflow.protocol.OFMatch;
import org.openflow.protocol.OFMessage;
import org.openflow.protocol.OFPacketIn;
import org.openflow.protocol.OFPacketOut;
import org.openflow.protocol.OFPort;
import org.openflow.protocol.OFType;
import org.openflow.protocol.action.OFAction;
import org.openflow.protocol.action.OFActionOutput;
import org.openflow.util.HexString;
import org.openflow.util.LRULinkedHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LearningSwitch
implements IFloodlightModule, ILearningSwitchService, IOFMessageListener {
protected static Logger log = LoggerFactory.getLogger(LearningSwitch.class);
// Module dependencies
protected IFloodlightProviderService floodlightProvider;
protected ICounterStoreService counterStore;
protected IRestApiService restApi;
// Stores the learned state for each switch
protected Map<IOFSwitch, Map<MacVlanPair,Short>> macVlanToSwitchPortMap;
// flow-mod - for use in the cookie
public static final int LEARNING_SWITCH_APP_ID = 1;
// LOOK! This should probably go in some class that encapsulates
// the app cookie management
public static final int APP_ID_BITS = 12;
public static final int APP_ID_SHIFT = (64 - APP_ID_BITS);
public static final long LEARNING_SWITCH_COOKIE = (long) (LEARNING_SWITCH_APP_ID & ((1 << APP_ID_BITS) - 1)) << APP_ID_SHIFT;
// more flow-mod defaults
protected static final short IDLE_TIMEOUT_DEFAULT = 5;
protected static final short HARD_TIMEOUT_DEFAULT = 0;
protected static final short PRIORITY_DEFAULT = 100;
// for managing our map sizes
protected static final int MAX_MACS_PER_SWITCH = 1000;
// normally, setup reverse flow as well. Disable only for using cbench for comparison with NOX etc.
protected static final boolean LEARNING_SWITCH_REVERSE_FLOW = true;
/**
* @param floodlightProvider the floodlightProvider to set
*/
public void setFloodlightProvider(IFloodlightProviderService floodlightProvider) {
this.floodlightProvider = floodlightProvider;
}
@Override
public String getName() {
return "learningswitch";
}
/**
* Adds a host to the MAC/VLAN->SwitchPort mapping
* @param sw The switch to add the mapping to
* @param mac The MAC address of the host to add
* @param vlan The VLAN that the host is on
* @param portVal The switchport that the host is on
*/
protected void addToPortMap(IOFSwitch sw, long mac, short vlan, short portVal) {
Map<MacVlanPair,Short> swMap = macVlanToSwitchPortMap.get(sw);
if (vlan == (short) 0xffff) {
// OFMatch.loadFromPacket sets VLAN ID to 0xffff if the packet contains no VLAN tag;
// for our purposes that is equivalent to the default VLAN ID 0
vlan = 0;
}
if (swMap == null) {
// May be accessed by REST API so we need to make it thread safe
swMap = Collections.synchronizedMap(new LRULinkedHashMap<MacVlanPair,Short>(MAX_MACS_PER_SWITCH));
macVlanToSwitchPortMap.put(sw, swMap);
}
swMap.put(new MacVlanPair(mac, vlan), portVal);
}
/**
* Removes a host from the MAC/VLAN->SwitchPort mapping
* @param sw The switch to remove the mapping from
* @param mac The MAC address of the host to remove
* @param vlan The VLAN that the host is on
*/
protected void removeFromPortMap(IOFSwitch sw, long mac, short vlan) {
if (vlan == (short) 0xffff) {
vlan = 0;
}
Map<MacVlanPair,Short> swMap = macVlanToSwitchPortMap.get(sw);
if (swMap != null)
swMap.remove(new MacVlanPair(mac, vlan));
}
/**
* Get the port that a MAC/VLAN pair is associated with
* @param sw The switch to get the mapping from
* @param mac The MAC address to get
* @param vlan The VLAN number to get
* @return The port the host is on
*/
public Short getFromPortMap(IOFSwitch sw, long mac, short vlan) {
if (vlan == (short) 0xffff) {
vlan = 0;
}
Map<MacVlanPair,Short> swMap = macVlanToSwitchPortMap.get(sw);
if (swMap != null)
return swMap.get(new MacVlanPair(mac, vlan));
// if none found
return null;
}
/**
* Clears the MAC/VLAN -> SwitchPort map for all switches
*/
public void clearLearnedTable() {
macVlanToSwitchPortMap.clear();
}
/**
* Clears the MAC/VLAN -> SwitchPort map for a single switch
* @param sw The switch to clear the mapping for
*/
public void clearLearnedTable(IOFSwitch sw) {
Map<MacVlanPair, Short> swMap = macVlanToSwitchPortMap.get(sw);
if (swMap != null)
swMap.clear();
}
@Override
public synchronized Map<IOFSwitch, Map<MacVlanPair,Short>> getTable() {
return macVlanToSwitchPortMap;
}
/**
* Writes a OFFlowMod to a switch.
* @param sw The switch tow rite the flowmod to.
* @param command The FlowMod actions (add, delete, etc).
* @param bufferId The buffer ID if the switch has buffered the packet.
* @param match The OFMatch structure to write.
* @param outPort The switch port to output it to.
*/
private void writeFlowMod(IOFSwitch sw, short command, int bufferId,
OFMatch match, short outPort) {
// from openflow 1.0 spec - need to set these on a struct ofp_flow_mod:
// struct ofp_flow_mod {
// struct ofp_header header;
// struct ofp_match match; /* Fields to match */
// uint64_t cookie; /* Opaque controller-issued identifier. */
//
// /* Flow actions. */
// uint16_t command; /* One of OFPFC_*. */
// uint16_t idle_timeout; /* Idle time before discarding (seconds). */
// uint16_t hard_timeout; /* Max time before discarding (seconds). */
// uint16_t priority; /* Priority level of flow entry. */
// uint32_t buffer_id; /* Buffered packet to apply to (or -1).
// Not meaningful for OFPFC_DELETE*. */
// uint16_t out_port; /* For OFPFC_DELETE* commands, require
// matching entries to include this as an
// output port. A value of OFPP_NONE
// indicates no restriction. */
// uint16_t flags; /* One of OFPFF_*. */
// struct ofp_action_header actions[0]; /* The action length is inferred
// from the length field in the
// header. */
// };
OFFlowMod flowMod = (OFFlowMod) floodlightProvider.getOFMessageFactory().getMessage(OFType.FLOW_MOD);
flowMod.setMatch(match);
flowMod.setCookie(LearningSwitch.LEARNING_SWITCH_COOKIE);
flowMod.setCommand(command);
flowMod.setIdleTimeout(LearningSwitch.IDLE_TIMEOUT_DEFAULT);
flowMod.setHardTimeout(LearningSwitch.HARD_TIMEOUT_DEFAULT);
flowMod.setPriority(LearningSwitch.PRIORITY_DEFAULT);
flowMod.setBufferId(bufferId);
flowMod.setOutPort((command == OFFlowMod.OFPFC_DELETE) ? outPort : OFPort.OFPP_NONE.getValue());
flowMod.setFlags((command == OFFlowMod.OFPFC_DELETE) ? 0 : (short) (1 << 0)); // OFPFF_SEND_FLOW_REM
// set the ofp_action_header/out actions:
// from the openflow 1.0 spec: need to set these on a struct ofp_action_output:
// uint16_t type; /* OFPAT_OUTPUT. */
// uint16_t len; /* Length is 8. */
// uint16_t port; /* Output port. */
// uint16_t max_len; /* Max length to send to controller. */
// type/len are set because it is OFActionOutput,
// and port, max_len are arguments to this constructor
flowMod.setActions(Arrays.asList((OFAction) new OFActionOutput(outPort, (short) 0xffff)));
flowMod.setLength((short) (OFFlowMod.MINIMUM_LENGTH + OFActionOutput.MINIMUM_LENGTH));
if (log.isTraceEnabled()) {
log.trace("{} {} flow mod {}",
new Object[]{ sw, (command == OFFlowMod.OFPFC_DELETE) ? "deleting" : "adding", flowMod });
}
counterStore.updatePktOutFMCounterStore(sw, flowMod);
// and write it out
try {
sw.write(flowMod, null);
} catch (IOException e) {
log.error("Failed to write {} to switch {}", new Object[]{ flowMod, sw }, e);
}
}
/**
* Writes an OFPacketOut message to a switch.
* @param sw The switch to write the PacketOut to.
* @param packetInMessage The corresponding PacketIn.
* @param egressPort The switchport to output the PacketOut.
*/
private void writePacketOutForPacketIn(IOFSwitch sw,
OFPacketIn packetInMessage,
short egressPort) {
// from openflow 1.0 spec - need to set these on a struct ofp_packet_out:
// uint32_t buffer_id; /* ID assigned by datapath (-1 if none). */
// uint16_t in_port; /* Packet's input port (OFPP_NONE if none). */
// uint16_t actions_len; /* Size of action array in bytes. */
// struct ofp_action_header actions[0]; /* Actions. */
/* uint8_t data[0]; */ /* Packet data. The length is inferred
from the length field in the header.
(Only meaningful if buffer_id == -1.) */
OFPacketOut packetOutMessage = (OFPacketOut) floodlightProvider.getOFMessageFactory().getMessage(OFType.PACKET_OUT);
short packetOutLength = (short)OFPacketOut.MINIMUM_LENGTH; // starting length
// Set buffer_id, in_port, actions_len
packetOutMessage.setBufferId(packetInMessage.getBufferId());
packetOutMessage.setInPort(packetInMessage.getInPort());
packetOutMessage.setActionsLength((short)OFActionOutput.MINIMUM_LENGTH);
packetOutLength += OFActionOutput.MINIMUM_LENGTH;
// set actions
List<OFAction> actions = new ArrayList<OFAction>(1);
actions.add(new OFActionOutput(egressPort, (short) 0));
packetOutMessage.setActions(actions);
// set data - only if buffer_id == -1
if (packetInMessage.getBufferId() == OFPacketOut.BUFFER_ID_NONE) {
byte[] packetData = packetInMessage.getPacketData();
packetOutMessage.setPacketData(packetData);
packetOutLength += (short)packetData.length;
}
// finally, set the total length
packetOutMessage.setLength(packetOutLength);
// and write it out
try {
counterStore.updatePktOutFMCounterStore(sw, packetOutMessage);
sw.write(packetOutMessage, null);
} catch (IOException e) {
log.error("Failed to write {} to switch {}: {}", new Object[]{ packetOutMessage, sw, e });
}
}
/**
* Processes a OFPacketIn message. If the switch has learned the MAC/VLAN to port mapping
* for the pair it will write a FlowMod for. If the mapping has not been learned the
* we will flood the packet.
* @param sw
* @param pi
* @param cntx
* @return
*/
private Command processPacketInMessage(IOFSwitch sw, OFPacketIn pi, FloodlightContext cntx) {
// Read in packet data headers by using OFMatch
OFMatch match = new OFMatch();
match.loadFromPacket(pi.getPacketData(), pi.getInPort());
Long sourceMac = Ethernet.toLong(match.getDataLayerSource());
Long destMac = Ethernet.toLong(match.getDataLayerDestination());
Short vlan = match.getDataLayerVirtualLan();
if ((destMac & 0xfffffffffff0L) == 0x0180c2000000L) {
if (log.isTraceEnabled()) {
log.trace("ignoring packet addressed to 802.1D/Q reserved addr: switch {} vlan {} dest MAC {}",
new Object[]{ sw, vlan, HexString.toHexString(destMac) });
}
return Command.STOP;
}
if ((sourceMac & 0x010000000000L) == 0) {
// If source MAC is a unicast address, learn the port for this MAC/VLAN
this.addToPortMap(sw, sourceMac, vlan, pi.getInPort());
}
// Now output flow-mod and/or packet
Short outPort = getFromPortMap(sw, destMac, vlan);
if (outPort == null) {
// If we haven't learned the port for the dest MAC/VLAN, flood it
// Don't flood broadcast packets if the broadcast is disabled.
// XXX For LearningSwitch this doesn't do much. The sourceMac is removed
// from port map whenever a flow expires, so you would still see
// a lot of floods.
this.writePacketOutForPacketIn(sw, pi, OFPort.OFPP_FLOOD.getValue());
} else if (outPort == match.getInputPort()) {
log.trace("ignoring packet that arrived on same port as learned destination:"
+ " switch {} vlan {} dest MAC {} port {}",
new Object[]{ sw, vlan, HexString.toHexString(destMac), outPort });
} else {
// Add flow table entry matching source MAC, dest MAC, VLAN and input port
// that sends to the port we previously learned for the dest MAC/VLAN. Also
// add a flow table entry with source and destination MACs reversed, and
// input and output ports reversed. When either entry expires due to idle
// timeout, remove the other one. This ensures that if a device moves to
// a different port, a constant stream of packets headed to the device at
// its former location does not keep the stale entry alive forever.
// FIXME: current HP switches ignore DL_SRC and DL_DST fields, so we have to match on
// NW_SRC and NW_DST as well
match.setWildcards(((Integer)sw.getAttribute(IOFSwitch.PROP_FASTWILDCARDS)).intValue()
& ~OFMatch.OFPFW_IN_PORT
& ~OFMatch.OFPFW_DL_VLAN & ~OFMatch.OFPFW_DL_SRC & ~OFMatch.OFPFW_DL_DST
& ~OFMatch.OFPFW_NW_SRC_MASK & ~OFMatch.OFPFW_NW_DST_MASK);
this.writeFlowMod(sw, OFFlowMod.OFPFC_ADD, pi.getBufferId(), match, outPort);
if (LEARNING_SWITCH_REVERSE_FLOW) {
this.writeFlowMod(sw, OFFlowMod.OFPFC_ADD, -1, match.clone()
.setDataLayerSource(match.getDataLayerDestination())
.setDataLayerDestination(match.getDataLayerSource())
.setNetworkSource(match.getNetworkDestination())
.setNetworkDestination(match.getNetworkSource())
.setTransportSource(match.getTransportDestination())
.setTransportDestination(match.getTransportSource())
.setInputPort(outPort),
match.getInputPort());
}
}
return Command.CONTINUE;
}
/**
* Processes a flow removed message. We will delete the learned MAC/VLAN mapping from
* the switch's table.
* @param sw The switch that sent the flow removed message.
* @param flowRemovedMessage The flow removed message.
* @return Whether to continue processing this message or stop.
*/
private Command processFlowRemovedMessage(IOFSwitch sw, OFFlowRemoved flowRemovedMessage) {
if (flowRemovedMessage.getCookie() != LearningSwitch.LEARNING_SWITCH_COOKIE) {
return Command.CONTINUE;
}
if (log.isTraceEnabled()) {
log.trace("{} flow entry removed {}", sw, flowRemovedMessage);
}
OFMatch match = flowRemovedMessage.getMatch();
// When a flow entry expires, it means the device with the matching source
// MAC address and VLAN either stopped sending packets or moved to a different
// port. If the device moved, we can't know where it went until it sends
// another packet, allowing us to re-learn its port. Meanwhile we remove
// it from the macVlanToPortMap to revert to flooding packets to this device.
this.removeFromPortMap(sw, Ethernet.toLong(match.getDataLayerSource()),
match.getDataLayerVirtualLan());
// Also, if packets keep coming from another device (e.g. from ping), the
// corresponding reverse flow entry will never expire on its own and will
// send the packets to the wrong port (the matching input port of the
// expired flow entry), so we must delete the reverse entry explicitly.
this.writeFlowMod(sw, OFFlowMod.OFPFC_DELETE, -1, match.clone()
.setWildcards(((Integer)sw.getAttribute(IOFSwitch.PROP_FASTWILDCARDS)).intValue()
& ~OFMatch.OFPFW_DL_VLAN & ~OFMatch.OFPFW_DL_SRC & ~OFMatch.OFPFW_DL_DST
& ~OFMatch.OFPFW_NW_SRC_MASK & ~OFMatch.OFPFW_NW_DST_MASK)
.setDataLayerSource(match.getDataLayerDestination())
.setDataLayerDestination(match.getDataLayerSource())
.setNetworkSource(match.getNetworkDestination())
.setNetworkDestination(match.getNetworkSource())
.setTransportSource(match.getTransportDestination())
.setTransportDestination(match.getTransportSource()),
match.getInputPort());
return Command.CONTINUE;
}
// IOFMessageListener
@Override
public Command receive(IOFSwitch sw, OFMessage msg, FloodlightContext cntx) {
switch (msg.getType()) {
case PACKET_IN:
return this.processPacketInMessage(sw, (OFPacketIn) msg, cntx);
case FLOW_REMOVED:
return this.processFlowRemovedMessage(sw, (OFFlowRemoved) msg);
case ERROR:
log.info("received an error {} from switch {}", (OFError) msg, sw);
return Command.CONTINUE;
default:
break;
}
log.error("received an unexpected message {} from switch {}", msg, sw);
return Command.CONTINUE;
}
@Override
public boolean isCallbackOrderingPrereq(OFType type, String name) {
return false;
}
@Override
public boolean isCallbackOrderingPostreq(OFType type, String name) {
return false;
}
// IFloodlightModule
@Override
public Collection<Class<? extends IFloodlightService>> getModuleServices() {
Collection<Class<? extends IFloodlightService>> l =
new ArrayList<Class<? extends IFloodlightService>>();
l.add(ILearningSwitchService.class);
return l;
}
@Override
public Map<Class<? extends IFloodlightService>, IFloodlightService>
getServiceImpls() {
Map<Class<? extends IFloodlightService>,
IFloodlightService> m =
new HashMap<Class<? extends IFloodlightService>,
IFloodlightService>();
m.put(ILearningSwitchService.class, this);
return m;
}
@Override
public Collection<Class<? extends IFloodlightService>>
getModuleDependencies() {
Collection<Class<? extends IFloodlightService>> l =
new ArrayList<Class<? extends IFloodlightService>>();
l.add(IFloodlightProviderService.class);
l.add(ICounterStoreService.class);
l.add(IRestApiService.class);
return l;
}
@Override
public void init(FloodlightModuleContext context)
throws FloodlightModuleException {
macVlanToSwitchPortMap =
new ConcurrentHashMap<IOFSwitch, Map<MacVlanPair,Short>>();
floodlightProvider =
context.getServiceImpl(IFloodlightProviderService.class);
counterStore =
context.getServiceImpl(ICounterStoreService.class);
restApi =
context.getServiceImpl(IRestApiService.class);
}
@Override
public void startUp(FloodlightModuleContext context) {
floodlightProvider.addOFMessageListener(OFType.PACKET_IN, this);
floodlightProvider.addOFMessageListener(OFType.FLOW_REMOVED, this);
floodlightProvider.addOFMessageListener(OFType.ERROR, this);
restApi.addRestletRoutable(new LearningSwitchWebRoutable());
}
}