blob: 4ed59d700e75287bd9720aa6d4ebf613cd9191dc [file] [log] [blame]
package net.floodlightcontroller.staticflowentry;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import net.floodlightcontroller.core.FloodlightContext;
import net.floodlightcontroller.core.IFloodlightProviderService;
import net.floodlightcontroller.core.IFloodlightProviderService.Role;
import net.floodlightcontroller.core.IHAListener;
import net.floodlightcontroller.core.IOFMessageListener;
import net.floodlightcontroller.core.IOFSwitch;
import net.floodlightcontroller.core.IOFSwitchListener;
import net.floodlightcontroller.core.annotations.LogMessageCategory;
import net.floodlightcontroller.core.annotations.LogMessageDoc;
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.util.AppCookie;
import net.floodlightcontroller.restserver.IRestApiService;
import net.floodlightcontroller.staticflowentry.web.StaticFlowEntryWebRoutable;
import net.floodlightcontroller.staticflowentry.IStaticFlowEntryPusherService;
import net.floodlightcontroller.storage.IResultSet;
import net.floodlightcontroller.storage.IStorageSourceService;
import net.floodlightcontroller.storage.IStorageSourceListener;
import net.floodlightcontroller.storage.StorageException;
import org.openflow.protocol.OFFlowMod;
import org.openflow.protocol.OFFlowRemoved;
import org.openflow.protocol.OFMatch;
import org.openflow.protocol.OFMessage;
import org.openflow.protocol.OFType;
import org.openflow.protocol.factory.BasicFactory;
import org.openflow.util.HexString;
import org.openflow.util.U16;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@LogMessageCategory("Static Flow Pusher")
/**
* This module is responsible for maintaining a set of static flows on
* switches. This is just a big 'ol dumb list of flows and something external
* is responsible for ensuring they make sense for the network.
*/
public class StaticFlowEntryPusher
implements IOFSwitchListener, IFloodlightModule, IStaticFlowEntryPusherService,
IStorageSourceListener, IOFMessageListener, IHAListener {
protected static Logger log = LoggerFactory.getLogger(StaticFlowEntryPusher.class);
public static final String StaticFlowName = "staticflowentry";
public static final int STATIC_FLOW_APP_ID = 10;
public static final String TABLE_NAME = "controller_staticflowtableentry";
public static final String COLUMN_NAME = "name";
public static final String COLUMN_SWITCH = "switch_id";
public static final String COLUMN_ACTIVE = "active";
public static final String COLUMN_IDLE_TIMEOUT = "idle_timeout";
public static final String COLUMN_HARD_TIMEOUT = "hard_timeout";
public static final String COLUMN_PRIORITY = "priority";
public static final String COLUMN_COOKIE = "cookie";
public static final String COLUMN_WILDCARD = "wildcards";
public static final String COLUMN_IN_PORT = "in_port";
public static final String COLUMN_DL_SRC = "dl_src";
public static final String COLUMN_DL_DST = "dl_dst";
public static final String COLUMN_DL_VLAN = "dl_vlan";
public static final String COLUMN_DL_VLAN_PCP = "dl_vlan_pcp";
public static final String COLUMN_DL_TYPE = "dl_type";
public static final String COLUMN_NW_TOS = "nw_tos";
public static final String COLUMN_NW_PROTO = "nw_proto";
public static final String COLUMN_NW_SRC = "nw_src"; // includes CIDR-style
// netmask, e.g.
// "128.8.128.0/24"
public static final String COLUMN_NW_DST = "nw_dst";
public static final String COLUMN_TP_DST = "tp_dst";
public static final String COLUMN_TP_SRC = "tp_src";
public static final String COLUMN_ACTIONS = "actions";
public static String ColumnNames[] = { COLUMN_NAME, COLUMN_SWITCH,
COLUMN_ACTIVE, COLUMN_IDLE_TIMEOUT, COLUMN_HARD_TIMEOUT,
COLUMN_PRIORITY, COLUMN_COOKIE, COLUMN_WILDCARD, COLUMN_IN_PORT,
COLUMN_DL_SRC, COLUMN_DL_DST, COLUMN_DL_VLAN, COLUMN_DL_VLAN_PCP,
COLUMN_DL_TYPE, COLUMN_NW_TOS, COLUMN_NW_PROTO, COLUMN_NW_SRC,
COLUMN_NW_DST, COLUMN_TP_DST, COLUMN_TP_SRC, COLUMN_ACTIONS };
protected IFloodlightProviderService floodlightProvider;
protected IStorageSourceService storageSource;
protected IRestApiService restApi;
// Map<DPID, Map<Name, FlowMod>> ; FlowMod can be null to indicate non-active
protected Map<String, Map<String, OFFlowMod>> entriesFromStorage;
// Entry Name -> DPID of Switch it's on
protected Map<String, String> entry2dpid;
private BasicFactory ofMessageFactory;
// Class to sort FlowMod's by priority, from lowest to highest
class FlowModSorter implements Comparator<String> {
private String dpid;
public FlowModSorter(String dpid) {
this.dpid = dpid;
}
@Override
public int compare(String o1, String o2) {
OFFlowMod f1 = entriesFromStorage.get(dpid).get(o1);
OFFlowMod f2 = entriesFromStorage.get(dpid).get(o2);
if (f1 == null || f2 == null) // sort active=false flows by key
return o1.compareTo(o2);
return U16.f(f1.getPriority()) - U16.f(f2.getPriority());
}
};
/**
* used for debugging and unittests
* @return the number of static flow entries as cached from storage
*/
public int countEntries() {
int size = 0;
if (entriesFromStorage == null)
return 0;
for (String ofswitch : entriesFromStorage.keySet())
size += entriesFromStorage.get(ofswitch).size();
return size;
}
public IFloodlightProviderService getFloodlightProvider() {
return floodlightProvider;
}
public void setFloodlightProvider(IFloodlightProviderService floodlightProvider) {
this.floodlightProvider = floodlightProvider;
}
public void setStorageSource(IStorageSourceService storageSource) {
this.storageSource = storageSource;
}
/**
* Reads from our entriesFromStorage for the specified switch and
* sends the FlowMods down to the controller in <b>sorted</b> order.
*
* Sorted is important to maintain correctness of the switch:
* if a packet would match both a lower and a higher priority
* rule, then we want it to match the higher priority or nothing,
* but never just the lower priority one. Inserting from high to
* low priority fixes this.
*
* TODO consider adding a "block all" flow mod and then removing it
* while starting up.
*
* @param sw The switch to send entries to
*/
protected void sendEntriesToSwitch(IOFSwitch sw) {
String dpid = sw.getStringId();
if ((entriesFromStorage != null) && (entriesFromStorage.containsKey(dpid))) {
Map<String, OFFlowMod> entries = entriesFromStorage.get(dpid);
List<String> sortedList = new ArrayList<String>(entries.keySet());
// weird that Collections.sort() returns void
Collections.sort( sortedList, new FlowModSorter(dpid));
for (String entryName : sortedList) {
OFFlowMod flowMod = entries.get(entryName);
if (flowMod != null) {
if (log.isDebugEnabled()) {
log.debug("Pushing static entry {} for {}", dpid, entryName);
}
writeFlowModToSwitch(sw, flowMod);
}
}
}
}
/**
* Used only for bundle-local indexing
*
* @param map
* @return
*/
protected Map<String, String> computeEntry2DpidMap(
Map<String, Map<String, OFFlowMod>> map) {
Map<String, String> ret = new HashMap<String, String>();
for(String dpid : map.keySet()) {
for( String entry: map.get(dpid).keySet())
ret.put(entry, dpid);
}
return ret;
}
/**
* Read entries from storageSource, and store them in a hash
*
* @return
*/
@LogMessageDoc(level="ERROR",
message="failed to access storage: {reason}",
explanation="Could not retrieve static flows from the system " +
"database",
recommendation=LogMessageDoc.CHECK_CONTROLLER)
private Map<String, Map<String, OFFlowMod>> readEntriesFromStorage() {
Map<String, Map<String, OFFlowMod>> entries = new ConcurrentHashMap<String, Map<String, OFFlowMod>>();
try {
Map<String, Object> row;
// null1=no predicate, null2=no ordering
IResultSet resultSet = storageSource.executeQuery(TABLE_NAME,
ColumnNames, null, null);
for (Iterator<IResultSet> it = resultSet.iterator(); it.hasNext();) {
row = it.next().getRow();
parseRow(row, entries);
}
} catch (StorageException e) {
log.error("failed to access storage: {}", e.getMessage());
// if the table doesn't exist, then wait to populate later via
// setStorageSource()
}
return entries;
}
/**
* Take a single row, turn it into a flowMod, and add it to the
* entries{$dpid}.{$entryName}=FlowMod
*
* IF an entry is in active, mark it with FlowMod = null
*
* @param row
* @param entries
*/
void parseRow(Map<String, Object> row,
Map<String, Map<String, OFFlowMod>> entries) {
String switchName = null;
String entryName = null;
StringBuffer matchString = new StringBuffer();
if (ofMessageFactory == null) // lazy init
ofMessageFactory = new BasicFactory();
OFFlowMod flowMod = (OFFlowMod) ofMessageFactory
.getMessage(OFType.FLOW_MOD);
if (!row.containsKey(COLUMN_SWITCH) || !row.containsKey(COLUMN_NAME)) {
log.debug(
"skipping entry with missing required 'switch' or 'name' entry: {}",
row);
return;
}
// most error checking done with ClassCastException
try {
// first, snag the required entries, for debugging info
switchName = (String) row.get(COLUMN_SWITCH);
entryName = (String) row.get(COLUMN_NAME);
if (!entries.containsKey(switchName))
entries.put(switchName, new HashMap<String, OFFlowMod>());
StaticFlowEntries.initDefaultFlowMod(flowMod, entryName);
for (String key : row.keySet()) {
if (row.get(key) == null)
continue;
if ( key.equals(COLUMN_SWITCH) || key.equals(COLUMN_NAME)
|| key.equals("id"))
continue; // already handled
// explicitly ignore timeouts and wildcards
if ( key.equals(COLUMN_HARD_TIMEOUT) || key.equals(COLUMN_IDLE_TIMEOUT) ||
key.equals(COLUMN_WILDCARD))
continue;
if ( key.equals(COLUMN_ACTIVE)) {
if (! Boolean.valueOf((String) row.get(COLUMN_ACTIVE))) {
log.debug("skipping inactive entry {} for switch {}",
entryName, switchName);
entries.get(switchName).put(entryName, null); // mark this an inactive
return;
}
} else if ( key.equals(COLUMN_ACTIONS)){
StaticFlowEntries.parseActionString(flowMod, (String) row.get(COLUMN_ACTIONS), log);
} else if ( key.equals(COLUMN_COOKIE)) {
flowMod.setCookie(
StaticFlowEntries.computeEntryCookie(flowMod,
Integer.valueOf((String) row.get(COLUMN_COOKIE)),
entryName)
);
} else if ( key.equals(COLUMN_PRIORITY)) {
flowMod.setPriority(U16.t(Integer.valueOf((String) row.get(COLUMN_PRIORITY))));
} else { // the rest of the keys are for OFMatch().fromString()
if (matchString.length() > 0)
matchString.append(",");
matchString.append(key + "=" + row.get(key).toString());
}
}
} catch (ClassCastException e) {
if (entryName != null && switchName != null)
log.debug(
"skipping entry {} on switch {} with bad data : "
+ e.getMessage(), entryName, switchName);
else
log.debug("skipping entry with bad data: {} :: {} ",
e.getMessage(), e.getStackTrace());
}
OFMatch ofMatch = new OFMatch();
String match = matchString.toString();
try {
ofMatch.fromString(match);
} catch (IllegalArgumentException e) {
log.debug(
"ignoring flow entry {} on switch {} with illegal OFMatch() key: "
+ match, entryName, switchName);
return;
}
flowMod.setMatch(ofMatch);
entries.get(switchName).put(entryName, flowMod);
}
@Override
public void addedSwitch(IOFSwitch sw) {
log.debug("addedSwitch {}; processing its static entries", sw);
sendEntriesToSwitch(sw);
}
@Override
public void removedSwitch(IOFSwitch sw) {
log.debug("removedSwitch {}", sw);
// do NOT delete from our internal state; we're tracking the rules,
// not the switches
}
@Override
public void switchPortChanged(Long switchId) {
// no-op
}
/**
* This handles both rowInsert() and rowUpdate()
*/
@Override
public void rowsModified(String tableName, Set<Object> rowKeys) {
log.debug("Modifying Table {}", tableName);
HashMap<String, Map<String, OFFlowMod>> entriesToAdd =
new HashMap<String, Map<String, OFFlowMod>>();
// build up list of what was added
for(Object key: rowKeys) {
IResultSet resultSet = storageSource.getRow(tableName, key);
for (Iterator<IResultSet> it = resultSet.iterator(); it.hasNext();) {
Map<String, Object> row = it.next().getRow();
parseRow(row, entriesToAdd);
}
}
// batch updates by switch and blast them out
for (String dpid : entriesToAdd.keySet()) {
if (!entriesFromStorage.containsKey(dpid))
entriesFromStorage.put(dpid, new HashMap<String, OFFlowMod>());
List<OFMessage> outQueue = new ArrayList<OFMessage>();
for(String entry : entriesToAdd.get(dpid).keySet()) {
OFFlowMod newFlowMod = entriesToAdd.get(dpid).get(entry);
OFFlowMod oldFlowMod = entriesFromStorage.get(dpid).get(entry);
if (oldFlowMod != null) { // remove any pre-existing rule
oldFlowMod.setCommand(OFFlowMod.OFPFC_DELETE_STRICT);
outQueue.add(oldFlowMod);
}
if (newFlowMod != null) {
entriesFromStorage.get(dpid).put(entry, newFlowMod);
outQueue.add(newFlowMod);
entry2dpid.put(entry, dpid);
} else {
entriesFromStorage.get(dpid).remove(entry);
entry2dpid.remove(entry);
}
}
writeOFMessagesToSwitch(HexString.toLong(dpid), outQueue);
}
}
@Override
public void rowsDeleted(String tableName, Set<Object> rowKeys) {
if (log.isDebugEnabled()) {
log.debug("deleting from Table {}", tableName);
}
for(Object obj : rowKeys) {
if (!(obj instanceof String)) {
log.debug("tried to delete non-string key {}; ignoring", obj);
continue;
}
deleteStaticFlowEntry((String) obj);
}
}
@LogMessageDoc(level="ERROR",
message="inconsistent internal state: no switch has rule {rule}",
explanation="Inconsistent internat state discovered while " +
"deleting a static flow rule",
recommendation=LogMessageDoc.REPORT_CONTROLLER_BUG)
private boolean deleteStaticFlowEntry(String entryName) {
String dpid = entry2dpid.get(entryName);
if (log.isDebugEnabled()) {
log.debug("Deleting flow {} for switch {}", entryName, dpid);
}
if (dpid == null) {
log.error("inconsistent internal state: no switch has rule {}",
entryName);
return false;
}
// send flow_mod delete
OFFlowMod flowMod = entriesFromStorage.get(dpid).get(entryName);
flowMod.setCommand(OFFlowMod.OFPFC_DELETE_STRICT);
if (entriesFromStorage.containsKey(dpid) &&
entriesFromStorage.get(dpid).containsKey(entryName)) {
entriesFromStorage.get(dpid).remove(entryName);
} else {
log.debug("Tried to delete non-existent entry {} for switch {}",
entryName, dpid);
return false;
}
writeFlowModToSwitch(HexString.toLong(dpid), flowMod);
return true;
}
/**
* Writes a list of OFMessages to a switch
* @param dpid The datapath ID of the switch to write to
* @param messages The list of OFMessages to write.
*/
@LogMessageDoc(level="ERROR",
message="Tried to write to switch {switch} but got {error}",
explanation="An I/O error occured while trying to write a " +
"static flow to a switch",
recommendation=LogMessageDoc.CHECK_SWITCH)
private void writeOFMessagesToSwitch(long dpid, List<OFMessage> messages) {
IOFSwitch ofswitch = floodlightProvider.getSwitches().get(dpid);
if (ofswitch != null) { // is the switch connected
try {
if (log.isDebugEnabled()) {
log.debug("Sending {} new entries to {}", messages.size(), dpid);
}
ofswitch.write(messages, null);
ofswitch.flush();
} catch (IOException e) {
log.error("Tried to write to switch {} but got {}", dpid, e.getMessage());
}
}
}
/**
* Writes an OFFlowMod to a switch. It checks to make sure the switch
* exists before it sends
* @param dpid The data to write the flow mod to
* @param flowMod The OFFlowMod to write
*/
private void writeFlowModToSwitch(long dpid, OFFlowMod flowMod) {
Map<Long,IOFSwitch> switches = floodlightProvider.getSwitches();
IOFSwitch ofSwitch = switches.get(dpid);
if (ofSwitch == null) {
if (log.isDebugEnabled()) {
log.debug("Not deleting key {} :: switch {} not connected",
dpid);
}
return;
}
writeFlowModToSwitch(ofSwitch, flowMod);
}
/**
* Writes an OFFlowMod to a switch
* @param sw The IOFSwitch to write to
* @param flowMod The OFFlowMod to write
*/
@LogMessageDoc(level="ERROR",
message="Tried to write OFFlowMod to {switch} but got {error}",
explanation="An I/O error occured while trying to write a " +
"static flow to a switch",
recommendation=LogMessageDoc.CHECK_SWITCH)
private void writeFlowModToSwitch(IOFSwitch sw, OFFlowMod flowMod) {
try {
sw.write(flowMod, null);
sw.flush();
} catch (IOException e) {
log.error("Tried to write OFFlowMod to {} but failed: {}",
HexString.toHexString(sw.getId()), e.getMessage());
}
}
@Override
public String getName() {
return StaticFlowName;
}
@Override
@LogMessageDoc(level="ERROR",
message="Got a FlowRemove message for a infinite " +
"timeout flow: {flow} from switch {switch}",
explanation="Flows with infinite timeouts should not expire. " +
"The switch has expired the flow anyway.",
recommendation=LogMessageDoc.REPORT_SWITCH_BUG)
public Command receive(IOFSwitch sw, OFMessage msg, FloodlightContext cntx) {
switch (msg.getType()) {
case FLOW_REMOVED:
break;
default:
return Command.CONTINUE;
}
OFFlowRemoved flowRemoved = (OFFlowRemoved) msg;
long cookie = flowRemoved.getCookie();
/**
* This is just to sanity check our assumption that static flows
* never expire.
*/
if( AppCookie.extractApp(cookie) == STATIC_FLOW_APP_ID) {
if (flowRemoved.getReason() !=
OFFlowRemoved.OFFlowRemovedReason.OFPRR_DELETE)
log.error("Got a FlowRemove message for a infinite " +
"timeout flow: {} from switch {}", msg, sw);
return Command.STOP; // only for us
} else
return Command.CONTINUE;
}
@Override
public boolean isCallbackOrderingPrereq(OFType type, String name) {
return false; // no dependency for non-packet in
}
@Override
public boolean isCallbackOrderingPostreq(OFType type, String name) {
return false; // no dependency for non-packet in
}
// IFloodlightModule
@Override
public Collection<Class<? extends IFloodlightService>> getModuleServices() {
Collection<Class<? extends IFloodlightService>> l =
new ArrayList<Class<? extends IFloodlightService>>();
l.add(IStaticFlowEntryPusherService.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(IStaticFlowEntryPusherService.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(IStorageSourceService.class);
l.add(IRestApiService.class);
return l;
}
@Override
public void init(FloodlightModuleContext context)
throws FloodlightModuleException {
floodlightProvider =
context.getServiceImpl(IFloodlightProviderService.class);
storageSource =
context.getServiceImpl(IStorageSourceService.class);
restApi =
context.getServiceImpl(IRestApiService.class);
}
@Override
public void startUp(FloodlightModuleContext context) {
floodlightProvider.addOFMessageListener(OFType.FLOW_REMOVED, this);
floodlightProvider.addOFSwitchListener(this);
floodlightProvider.addHAListener(this);
// assumes no switches connected at startup()
storageSource.createTable(TABLE_NAME, null);
storageSource.setTablePrimaryKeyName(TABLE_NAME, COLUMN_NAME);
storageSource.addListener(TABLE_NAME, this);
entriesFromStorage = readEntriesFromStorage();
entry2dpid = computeEntry2DpidMap(entriesFromStorage);
restApi.addRestletRoutable(new StaticFlowEntryWebRoutable());
}
// IStaticFlowEntryPusherService methods
@Override
public void addFlow(String name, OFFlowMod fm, String swDpid) {
Map<String, Object> fmMap = StaticFlowEntries.flowModToStorageEntry(fm, swDpid, name);
entry2dpid.put(name, swDpid);
Map<String, OFFlowMod> switchEntries = entriesFromStorage.get(swDpid);
if (switchEntries == null) {
switchEntries = new HashMap<String, OFFlowMod>();
entriesFromStorage.put(swDpid, switchEntries);
}
switchEntries.put(name, fm);
storageSource.insertRowAsync(TABLE_NAME, fmMap);
}
@Override
public void deleteFlow(String name) {
storageSource.deleteRowAsync(TABLE_NAME, name);
// TODO - What if there is a delay in storage?
}
@Override
public void deleteAllFlows() {
for (String entry : entry2dpid.keySet()) {
deleteFlow(entry);
}
}
@Override
public void deleteFlowsForSwitch(long dpid) {
String sDpid = HexString.toHexString(dpid);
for (Entry<String, String> e : entry2dpid.entrySet()) {
if (e.getValue().equals(sDpid))
deleteFlow(e.getKey());
}
}
@Override
public Map<String, Map<String, OFFlowMod>> getFlows() {
return entriesFromStorage;
}
@Override
public Map<String, OFFlowMod> getFlows(String dpid) {
return entriesFromStorage.get(dpid);
}
// IHAListener
@Override
public void roleChanged(Role oldRole, Role newRole) {
switch(newRole) {
case MASTER:
if (oldRole == Role.SLAVE) {
log.debug("Re-reading static flows from storage due " +
"to HA change from SLAVE->MASTER");
entriesFromStorage = readEntriesFromStorage();
entry2dpid = computeEntry2DpidMap(entriesFromStorage);
}
break;
case SLAVE:
log.debug("Clearing in-memory flows due to " +
"HA change to SLAVE");
entry2dpid.clear();
entriesFromStorage.clear();
break;
default:
break;
}
}
@Override
public void controllerNodeIPsChanged(
Map<String, String> curControllerNodeIPs,
Map<String, String> addedControllerNodeIPs,
Map<String, String> removedControllerNodeIPs) {
// ignore
}
}